merge net5

This commit is contained in:
goat 2022-08-05 20:16:52 +02:00
commit 23c73da5c8
No known key found for this signature in database
GPG key ID: 49E2AA8C6A76498B
61 changed files with 2749 additions and 533 deletions

View file

@ -38,7 +38,7 @@ jobs:
deploy_stg:
name: Deploy dalamud-distrib staging
if: ${{ github.event_name == 'push' }}
if: ${{ github.repository_owner == 'goatcorp' && github.event_name == 'push' }}
needs: build
runs-on: windows-latest
steps:

View file

@ -75,6 +75,7 @@
"Compile",
"CompileDalamud",
"CompileDalamudBoot",
"CompileDalamudCrashHandler",
"CompileInjector",
"CompileInjectorBoot",
"Restore",
@ -96,6 +97,7 @@
"Compile",
"CompileDalamud",
"CompileDalamudBoot",
"CompileDalamudCrashHandler",
"CompileInjector",
"CompileInjectorBoot",
"Restore",

View file

@ -0,0 +1,71 @@
// Microsoft Visual C++ generated resource script.
//
#include "resource.h"
#define APSTUDIO_READONLY_SYMBOLS
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 2 resource.
//
#include "winres.h"
/////////////////////////////////////////////////////////////////////////////
#undef APSTUDIO_READONLY_SYMBOLS
/////////////////////////////////////////////////////////////////////////////
// English (United Kingdom) resources
#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENG)
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_UK
#pragma code_page(1252)
#ifdef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// TEXTINCLUDE
//
1 TEXTINCLUDE
BEGIN
"resource.h\0"
END
2 TEXTINCLUDE
BEGIN
"#include ""winres.h""\r\n"
"\0"
END
3 TEXTINCLUDE
BEGIN
"\r\n"
"\0"
END
#endif // APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// Icon
//
// Icon with lowest ID value placed first to ensure application icon
// remains consistent on all systems.
IDI_ICON1 ICON "dalamud.ico"
#endif // English (United Kingdom) resources
/////////////////////////////////////////////////////////////////////////////
#ifndef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 3 resource.
//
/////////////////////////////////////////////////////////////////////////////
#endif // not APSTUDIO_INVOKED

View file

@ -33,10 +33,12 @@
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<LibraryPath>$(LibraryPath)</LibraryPath>
<UseDebugLibraries>true</UseDebugLibraries>
<LibraryPath>$(SolutionDir)bin\lib\$(Configuration)\libMinHook\;$(VC_LibraryPath_x64);$(WindowsSDK_LibraryPath_x64)</LibraryPath>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<LibraryPath>$(SolutionDir)bin\lib\$(Configuration)\libMinHook\;$(LibraryPath)</LibraryPath>
<UseDebugLibraries>false</UseDebugLibraries>
<LibraryPath>$(SolutionDir)bin\lib\$(Configuration)\libMinHook\;$(VC_LibraryPath_x64);$(WindowsSDK_LibraryPath_x64)</LibraryPath>
</PropertyGroup>
<ItemDefinitionGroup>
<ClCompile>
@ -53,7 +55,7 @@
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<EnableUAC>false</EnableUAC>
<AdditionalDependencies>dbghelp.lib;Version.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalDependencies>Version.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalLibraryDirectories>..\lib\CoreCLR;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
</Link>
</ItemDefinitionGroup>
@ -162,15 +164,23 @@
<ClInclude Include="..\lib\TsudaKageyu-minhook\src\HDE\table32.h" />
<ClInclude Include="..\lib\TsudaKageyu-minhook\src\HDE\table64.h" />
<ClInclude Include="..\lib\TsudaKageyu-minhook\src\trampoline.h" />
<ClInclude Include="crashhandler_shared.h" />
<ClInclude Include="DalamudStartInfo.h" />
<ClInclude Include="hooks.h" />
<ClInclude Include="logging.h" />
<ClInclude Include="resource.h" />
<ClInclude Include="unicode.h" />
<ClInclude Include="utils.h" />
<ClInclude Include="pch.h" />
<ClInclude Include="veh.h" />
<ClInclude Include="xivfixes.h" />
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="Dalamud.Boot.rc" />
</ItemGroup>
<ItemGroup>
<Image Include="dalamud.ico" />
</ItemGroup>
<Target Name="RemoveExtraFiles" AfterTargets="PostBuildEvent">
<Delete Files="$(OutDir)$(TargetName).lib" />
<Delete Files="$(OutDir)$(TargetName).exp" />

View file

@ -138,5 +138,13 @@
<ClInclude Include="..\lib\TsudaKageyu-minhook\src\HDE\table64.h">
<Filter>MinHook</Filter>
</ClInclude>
<ClInclude Include="resource.h" />
<ClInclude Include="crashhandler_shared.h" />
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="Dalamud.Boot.rc" />
</ItemGroup>
<ItemGroup>
<Image Include="dalamud.ico" />
</ItemGroup>
</Project>

View file

@ -8,6 +8,9 @@ struct DalamudStartInfo {
BeforeDalamudConstruct = 1 << 2,
};
friend void from_json(const nlohmann::json&, WaitMessageboxFlags&);
friend WaitMessageboxFlags operator &(WaitMessageboxFlags a, WaitMessageboxFlags b) {
return static_cast<WaitMessageboxFlags>(static_cast<int>(a) & static_cast<int>(b));
}
enum class DotNetOpenProcessHookMode : int {
ImportHooks = 0,

View file

@ -0,0 +1,18 @@
#pragma once
#include <cinttypes>
#define NOMINMAX
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
struct exception_info
{
LPEXCEPTION_POINTERS pExceptionPointers;
EXCEPTION_POINTERS ExceptionPointers;
EXCEPTION_RECORD ExceptionRecord;
CONTEXT ContextRecord;
uint64_t nLifetime;
HANDLE hThreadHandle;
DWORD dwStackTraceLength;
};

BIN
Dalamud.Boot/dalamud.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

View file

@ -87,7 +87,7 @@ DWORD WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) {
logging::I("Dalamud.Boot Injectable, (c) 2021 XIVLauncher Contributors");
logging::I("Built at: " __DATE__ "@" __TIME__);
if (static_cast<int>(g_startInfo.BootWaitMessageBox) & static_cast<int>(DalamudStartInfo::WaitMessageboxFlags::BeforeInitialize))
if ((g_startInfo.BootWaitMessageBox & DalamudStartInfo::WaitMessageboxFlags::BeforeInitialize) != DalamudStartInfo::WaitMessageboxFlags::None)
MessageBoxW(nullptr, L"Press OK to continue (BeforeInitialize)", L"Dalamud Boot", MB_OK);
if (minHookLoaded) {
@ -136,7 +136,7 @@ DWORD WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) {
if (utils::is_running_on_linux()) {
logging::I("=> VEH was disabled, running on linux");
} else if (g_startInfo.BootVehEnabled) {
if (veh::add_handler(g_startInfo.BootVehFull))
if (veh::add_handler(g_startInfo.BootVehFull, g_startInfo.WorkingDirectory))
logging::I("=> Done!");
else
logging::I("=> Failed!");
@ -172,6 +172,9 @@ BOOL APIENTRY DllMain(const HMODULE hModule, const DWORD dwReason, LPVOID lpRese
break;
case DLL_PROCESS_DETACH:
// do not show debug message boxes on abort() here
_set_abort_behavior(0, _WRITE_ABORT_MSG);
// process is terminating; don't bother cleaning up
if (lpReserved)
return TRUE;

View file

@ -11,15 +11,16 @@
#define WIN32_LEAN_AND_MEAN
#define NOMINMAX
// Windows Header Files
#include <windows.h>
#include <DbgHelp.h>
// Windows Header Files (1)
#include <Windows.h>
// Windows Header Files (2)
#include <Dbt.h>
#include <PathCch.h>
#include <Psapi.h>
#include <Shlobj.h>
#include <TlHelp32.h>
#include <Dbt.h>
#include <ShlObj.h>
#include <SubAuth.h>
#include <TlHelp32.h>
// MSVC Compiler Intrinsic
#include <intrin.h>
@ -33,11 +34,11 @@
#include <fstream>
#include <functional>
#include <iostream>
#include <mutex>
#include <ranges>
#include <set>
#include <span>
#include <string>
#include <mutex>
#include <type_traits>
// https://www.akenotsuki.com/misc/srell/en/
@ -50,8 +51,8 @@
#include "../lib/Nomade040-nmd/nmd_assembly.h"
// https://github.com/dotnet/coreclr
#include "../lib/CoreCLR/CoreCLR.h"
#include "../lib/CoreCLR/boot.h"
#include "../lib/CoreCLR/CoreCLR.h"
// https://github.com/nlohmann/json
#include "../lib/nlohmann-json/json.hpp"

16
Dalamud.Boot/resource.h Normal file
View file

@ -0,0 +1,16 @@
//{{NO_DEPENDENCIES}}
// Microsoft Visual C++ generated include file.
// Used by Dalamud.Boot.rc
//
#define IDI_ICON1 101
// Next default values for new objects
//
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE 102
#define _APS_NEXT_COMMAND_VALUE 40001
#define _APS_NEXT_CONTROL_VALUE 1001
#define _APS_NEXT_SYMED_VALUE 101
#endif
#endif

View file

@ -520,3 +520,36 @@ void utils::wait_for_game_window() {
};
SendMessageW(game_window, WM_NULL, 0, 0);
}
std::wstring utils::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);
} else {
res.push_back(L'"');
for (auto it = arg.begin(); ; ++it) {
size_t bsCount = 0;
while (it != arg.end() && *it == L'\\') {
++it;
++bsCount;
}
if (it == arg.end()) {
res.append(bsCount * 2, L'\\');
break;
} else if (*it == L'"') {
res.append(bsCount * 2 + 1, L'\\');
res.push_back(*it);
} else {
res.append(bsCount, L'\\');
res.push_back(*it);
}
}
res.push_back(L'"');
}
return res;
}

View file

@ -260,4 +260,6 @@ namespace utils {
HWND try_find_game_window();
void wait_for_game_window();
std::wstring escape_shell_arg(const std::wstring& arg);
}

View file

@ -2,8 +2,32 @@
#include "veh.h"
#include <shellapi.h>
#include "logging.h"
#include "utils.h"
#include "crashhandler_shared.h"
#include "DalamudStartInfo.h"
#pragma comment(lib, "comctl32.lib")
#if defined _M_IX86
#pragma comment(linker, "/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='x86' publicKeyToken='6595b64144ccf1df' language='*'\"")
#elif defined _M_IA64
#pragma comment(linker, "/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='ia64' publicKeyToken='6595b64144ccf1df' language='*'\"")
#elif defined _M_X64
#pragma comment(linker, "/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='amd64' publicKeyToken='6595b64144ccf1df' language='*'\"")
#else
#pragma comment(linker, "/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"")
#endif
PVOID g_veh_handle = nullptr;
bool g_veh_do_full_dump = false;
HANDLE g_crashhandler_process = nullptr;
HANDLE g_crashhandler_pipe_write = nullptr;
std::chrono::time_point<std::chrono::system_clock> g_time_start;
bool is_whitelist_exception(const DWORD code)
{
@ -47,7 +71,6 @@ bool is_whitelist_exception(const DWORD code)
}
}
bool get_module_file_and_base(const DWORD64 address, DWORD64& module_base, std::filesystem::path& module_file)
{
HMODULE handle;
@ -65,242 +88,231 @@ bool get_module_file_and_base(const DWORD64 address, DWORD64& module_base, std::
return false;
}
bool is_ffxiv_address(const DWORD64 address)
bool is_ffxiv_address(const wchar_t* module_name, const DWORD64 address)
{
DWORD64 module_base;
if (std::filesystem::path module_path; get_module_file_and_base(address, module_base, module_path))
return _wcsicmp(module_path.filename().c_str(), L"ffxiv_dx11.exe") == 0;
return _wcsicmp(module_path.filename().c_str(), module_name) == 0;
return false;
}
bool get_sym_from_addr(const DWORD64 address, DWORD64& displacement, std::wstring& symbol_name)
static void append_injector_launch_args(std::vector<std::wstring>& args)
{
union {
char buffer[sizeof(SYMBOL_INFOW) + MAX_SYM_NAME * sizeof(wchar_t)]{};
SYMBOL_INFOW symbol;
};
symbol.SizeOfStruct = sizeof(SYMBOL_INFO);
symbol.MaxNameLen = MAX_SYM_NAME;
args.emplace_back(L"-g");
args.emplace_back(utils::loaded_module::current_process().path().wstring());
if (g_startInfo.BootShowConsole)
args.emplace_back(L"--console");
if (g_startInfo.BootEnableEtw)
args.emplace_back(L"--etw");
if (g_startInfo.BootVehEnabled)
args.emplace_back(L"--veh");
if (g_startInfo.BootVehFull)
args.emplace_back(L"--veh-full");
if ((g_startInfo.BootWaitMessageBox & DalamudStartInfo::WaitMessageboxFlags::BeforeInitialize) != DalamudStartInfo::WaitMessageboxFlags::None)
args.emplace_back(L"--msgbox1");
if ((g_startInfo.BootWaitMessageBox & DalamudStartInfo::WaitMessageboxFlags::BeforeDalamudEntrypoint) != DalamudStartInfo::WaitMessageboxFlags::None)
args.emplace_back(L"--msgbox2");
if ((g_startInfo.BootWaitMessageBox & DalamudStartInfo::WaitMessageboxFlags::BeforeDalamudConstruct) != DalamudStartInfo::WaitMessageboxFlags::None)
args.emplace_back(L"--msgbox3");
if (SymFromAddrW(GetCurrentProcess(), address, &displacement, &symbol) && symbol.Name[0])
{
symbol_name = symbol.Name;
return true;
args.emplace_back(L"--");
if (int nArgs; LPWSTR * szArgList = CommandLineToArgvW(GetCommandLineW(), &nArgs)) {
for (auto i = 1; i < nArgs; i++)
args.emplace_back(szArgList[i]);
LocalFree(szArgList);
}
return false;
}
std::wstring to_address_string(const DWORD64 address, const bool try_ptrderef = true)
{
DWORD64 module_base;
std::filesystem::path module_path;
bool is_mod_addr = get_module_file_and_base(address, module_base, module_path);
DWORD64 value = 0;
if(try_ptrderef && address > 0x10000 && address < 0x7FFFFFFE0000)
{
ReadProcessMemory(GetCurrentProcess(), reinterpret_cast<void*>(address), &value, sizeof value, nullptr);
}
std::wstring addr_str = is_mod_addr ?
std::format(L"{}+{:X}", module_path.filename().c_str(), address - module_base) :
std::format(L"{:X}", address);
DWORD64 displacement;
if (std::wstring symbol; get_sym_from_addr(address, displacement, symbol))
return std::format(L"{}\t({})", addr_str, displacement != 0 ? std::format(L"{}+0x{:X}", symbol, displacement) : std::format(L"{}", symbol));
return value != 0 ? std::format(L"{} [{}]", addr_str, to_address_string(value, false)) : addr_str;
}
void print_exception_info(const EXCEPTION_POINTERS* ex, std::wostringstream& log)
{
size_t rec_index = 0;
for (auto rec = ex->ExceptionRecord; rec; rec = rec->ExceptionRecord)
{
log << std::format(L"\nException Info #{}\n", ++rec_index);
log << std::format(L"Address: {:X}\n", rec->ExceptionCode);
log << std::format(L"Flags: {:X}\n", rec->ExceptionFlags);
log << std::format(L"Address: {:X}\n", reinterpret_cast<size_t>(rec->ExceptionAddress));
if (!rec->NumberParameters)
continue;
log << L"Parameters: ";
for (DWORD i = 0; i < rec->NumberParameters; ++i)
{
if (i != 0)
log << L", ";
log << std::format(L"{:X}", rec->ExceptionInformation[i]);
}
}
log << L"\nCall Stack\n{";
STACKFRAME64 sf;
sf.AddrPC.Offset = ex->ContextRecord->Rip;
sf.AddrPC.Mode = AddrModeFlat;
sf.AddrStack.Offset = ex->ContextRecord->Rsp;
sf.AddrStack.Mode = AddrModeFlat;
sf.AddrFrame.Offset = ex->ContextRecord->Rbp;
sf.AddrFrame.Mode = AddrModeFlat;
CONTEXT ctx = *ex->ContextRecord;
int frame_index = 0;
log << std::format(L"\n [{}]\t{}", frame_index++, to_address_string(sf.AddrPC.Offset, false));
do
{
if (!StackWalk64(IMAGE_FILE_MACHINE_AMD64, GetCurrentProcess(), GetCurrentThread(), &sf, &ctx, nullptr, SymFunctionTableAccess64, SymGetModuleBase64, nullptr))
break;
log << std::format(L"\n [{}]\t{}", frame_index++, to_address_string(sf.AddrPC.Offset, false));
} while (sf.AddrReturn.Offset != 0 && sf.AddrPC.Offset != sf.AddrReturn.Offset);
log << L"\n}\n";
ctx = *ex->ContextRecord;
log << L"\nRegisters\n{";
log << std::format(L"\n RAX:\t{}", to_address_string(ctx.Rax));
log << std::format(L"\n RBX:\t{}", to_address_string(ctx.Rbx));
log << std::format(L"\n RCX:\t{}", to_address_string(ctx.Rcx));
log << std::format(L"\n RDX:\t{}", to_address_string(ctx.Rdx));
log << std::format(L"\n R8:\t{}", to_address_string(ctx.R8));
log << std::format(L"\n R9:\t{}", to_address_string(ctx.R9));
log << std::format(L"\n R10:\t{}", to_address_string(ctx.R10));
log << std::format(L"\n R11:\t{}", to_address_string(ctx.R11));
log << std::format(L"\n R12:\t{}", to_address_string(ctx.R12));
log << std::format(L"\n R13:\t{}", to_address_string(ctx.R13));
log << std::format(L"\n R14:\t{}", to_address_string(ctx.R14));
log << std::format(L"\n R15:\t{}", to_address_string(ctx.R15));
log << std::format(L"\n RSI:\t{}", to_address_string(ctx.Rsi));
log << std::format(L"\n RDI:\t{}", to_address_string(ctx.Rdi));
log << std::format(L"\n RBP:\t{}", to_address_string(ctx.Rbp));
log << std::format(L"\n RSP:\t{}", to_address_string(ctx.Rsp));
log << std::format(L"\n RIP:\t{}", to_address_string(ctx.Rip));
log << L"\n}" << std::endl;
if(0x10000 < ctx.Rsp && ctx.Rsp < 0x7FFFFFFE0000)
{
log << L"\nStack\n{";
for(DWORD64 i = 0; i < 16; i++)
log << std::format(L"\n [RSP+{:X}]\t{}", i * 8, to_address_string(*reinterpret_cast<DWORD64*>(ctx.Rsp + i * 8ull)));
log << L"\n}\n";
}
log << L"\nModules\n{";
if(HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, GetCurrentProcessId()); snap != INVALID_HANDLE_VALUE)
{
MODULEENTRY32 mod;
mod.dwSize = sizeof MODULEENTRY32;
if(Module32First(snap, &mod))
{
do
{
log << std::format(L"\n {:08X}\t{}", reinterpret_cast<DWORD64>(mod.modBaseAddr), mod.szExePath);
}
while (Module32Next(snap, &mod));
}
CloseHandle(snap);
}
log << L"\n}\n";
}
LONG exception_handler(EXCEPTION_POINTERS* ex)
{
static std::mutex s_exception_handler_mutex;
static std::recursive_mutex s_exception_handler_mutex;
if (!is_whitelist_exception(ex->ExceptionRecord->ExceptionCode))
return EXCEPTION_CONTINUE_SEARCH;
if (ex->ExceptionRecord->ExceptionCode == 0x12345678)
{
// pass
}
else
{
if (!is_whitelist_exception(ex->ExceptionRecord->ExceptionCode))
return EXCEPTION_CONTINUE_SEARCH;
if (!is_ffxiv_address(ex->ContextRecord->Rip))
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
const auto lock = std::lock_guard(s_exception_handler_mutex);
DWORD64 module_base;
std::filesystem::path module_path;
exception_info exinfo{};
exinfo.pExceptionPointers = ex;
exinfo.ExceptionPointers = *ex;
exinfo.ContextRecord = *ex->ContextRecord;
exinfo.ExceptionRecord = ex->ExceptionRecord ? *ex->ExceptionRecord : EXCEPTION_RECORD{};
const auto time_now = std::chrono::system_clock::now();
auto lifetime = std::chrono::duration_cast<std::chrono::seconds>(
time_now.time_since_epoch()).count()
- std::chrono::duration_cast<std::chrono::seconds>(
g_time_start.time_since_epoch()).count();
exinfo.nLifetime = lifetime;
DuplicateHandle(GetCurrentProcess(), GetCurrentThread(), g_crashhandler_process, &exinfo.hThreadHandle, 0, TRUE, DUPLICATE_SAME_ACCESS);
get_module_file_and_base(reinterpret_cast<DWORD64>(&exception_handler), module_base, module_path);
#ifndef NDEBUG
std::wstring dmp_path = module_path.replace_filename(L"dalamud_appcrashd.dmp").wstring();
#else
std::wstring dmp_path = module_path.replace_filename(L"dalamud_appcrash.dmp").wstring();
#endif
std::wstring log_path = module_path.replace_filename(L"dalamud_appcrash.log").wstring();
std::wostringstream log;
log << std::format(L"Unhandled native exception occurred at {}", to_address_string(ex->ContextRecord->Rip, false)) << std::endl;
log << std::format(L"Code: {:X}", ex->ExceptionRecord->ExceptionCode) << std::endl;
log << std::format(L"Dump at: {}", dmp_path) << std::endl;
log << L"Time: " << std::chrono::zoned_time{ std::chrono::current_zone(), std::chrono::system_clock::now() } << std::endl;
SymRefreshModuleList(GetCurrentProcess());
print_exception_info(ex, log);
MINIDUMP_EXCEPTION_INFORMATION ex_info;
ex_info.ClientPointers = false;
ex_info.ExceptionPointers = ex;
ex_info.ThreadId = GetCurrentThreadId();
auto miniDumpType = MiniDumpWithDataSegs;
if (g_veh_do_full_dump)
miniDumpType = MiniDumpWithFullMemory;
HANDLE file = CreateFileW(dmp_path.c_str(), GENERIC_WRITE, FILE_SHARE_WRITE, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), file, miniDumpType, &ex_info, nullptr, nullptr);
CloseHandle(file);
void* fn;
if (const auto err = static_cast<DWORD>(g_clr->get_function_pointer(
std::wstring stackTrace;
if (void* fn; const auto err = static_cast<DWORD>(g_clr->get_function_pointer(
L"Dalamud.EntryPoint, Dalamud",
L"VehCallback",
L"Dalamud.EntryPoint+VehDelegate, Dalamud",
nullptr, nullptr, &fn)))
{
const auto formatted = std::format(
L"An error within the game has occurred.\n\n"
L"This may be caused by a faulty plugin, a broken TexTools modification, any other third-party tool or simply a bug in the game.\n"
L"Please try \"Start Over\" or \"Download Index Backup\" in TexTools, an integrity check in the XIVLauncher settings, and disabling plugins you don't need.\n\n"
L"The log file is located at:\n"
L"{1}\n\n"
L"Press OK to exit the application.\n\nFailed to read stack trace: {2:08x}",
dmp_path, log_path, err);
// show in another thread to prevent messagebox from pumping messages of current thread
std::thread([&]() {
MessageBoxW(nullptr, formatted.c_str(), L"Dalamud Error", MB_OK | MB_ICONERROR | MB_TOPMOST);
}).join();
stackTrace = std::format(L"Failed to read stack trace: 0x{:08x}", err);
}
else
{
((void(__stdcall*)(const void*, const void*, const void*))fn)(dmp_path.c_str(), log_path.c_str(), log.str().c_str());
stackTrace = static_cast<wchar_t*(*)()>(fn)();
// Don't free it, as the program's going to be quit anyway
}
exinfo.dwStackTraceLength = static_cast<DWORD>(stackTrace.size());
if (DWORD written; !WriteFile(g_crashhandler_pipe_write, &exinfo, static_cast<DWORD>(sizeof exinfo), &written, nullptr) || sizeof exinfo != written)
return EXCEPTION_CONTINUE_SEARCH;
if (DWORD written; !WriteFile(g_crashhandler_pipe_write, &stackTrace[0], static_cast<DWORD>(std::span(stackTrace).size_bytes()), &written, nullptr) || std::span(stackTrace).size_bytes() != written)
return EXCEPTION_CONTINUE_SEARCH;
SuspendThread(GetCurrentThread());
return EXCEPTION_CONTINUE_SEARCH;
}
bool veh::add_handler(bool doFullDump)
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_do_full_dump = doFullDump;
g_time_start = std::chrono::system_clock::now();
return g_veh_handle != nullptr;
std::optional<std::unique_ptr<std::remove_pointer_t<HANDLE>, decltype(&CloseHandle)>> hWritePipe;
std::optional<std::unique_ptr<std::remove_pointer_t<HANDLE>, decltype(&CloseHandle)>> hReadPipeInheritable;
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);
}
else
{
logging::W("Failed to launch DalamudCrashHandler.exe: DuplicateHandle(1) error 0x{:x}", GetLastError());
return false;
}
}
else
{
logging::W("Failed to launch DalamudCrashHandler.exe: CreatePipe error 0x{:x}", GetLastError());
return false;
}
// additional information
STARTUPINFOEXW siex{};
PROCESS_INFORMATION pi{};
siex.StartupInfo.cb = sizeof siex;
siex.StartupInfo.dwFlags = STARTF_USESHOWWINDOW;
#ifdef NDEBUG
siex.StartupInfo.wShowWindow = SW_HIDE;
#else
siex.StartupInfo.wShowWindow = SW_SHOW;
#endif
// set up list of handles to inherit to child process
std::vector<char> attributeListBuf;
if (SIZE_T size = 0; !InitializeProcThreadAttributeList(nullptr, 1, 0, &size))
{
if (const auto err = GetLastError(); err != ERROR_INSUFFICIENT_BUFFER)
{
logging::W("Failed to launch DalamudCrashHandler.exe: InitializeProcThreadAttributeList(1) error 0x{:x}", err);
return false;
}
attributeListBuf.resize(size);
siex.lpAttributeList = reinterpret_cast<PPROC_THREAD_ATTRIBUTE_LIST>(&attributeListBuf[0]);
if (!InitializeProcThreadAttributeList(siex.lpAttributeList, 1, 0, &size))
{
logging::W("Failed to launch DalamudCrashHandler.exe: InitializeProcThreadAttributeList(2) error 0x{:x}", GetLastError());
return false;
}
}
else
{
logging::W("Failed to launch DalamudCrashHandler.exe: InitializeProcThreadAttributeList(0) was supposed to fail");
return false;
}
std::unique_ptr<std::remove_pointer_t<LPPROC_THREAD_ATTRIBUTE_LIST>, decltype(&DeleteProcThreadAttributeList)> cleanAttributeList(siex.lpAttributeList, &DeleteProcThreadAttributeList);
std::vector<HANDLE> handles;
HANDLE hInheritableCurrentProcess;
if (!DuplicateHandle(GetCurrentProcess(), GetCurrentProcess(), GetCurrentProcess(), &hInheritableCurrentProcess, 0, TRUE, DUPLICATE_SAME_ACCESS))
{
logging::W("Failed to launch DalamudCrashHandler.exe: DuplicateHandle(2) error 0x{:x}", GetLastError());
return false;
}
handles.push_back(hInheritableCurrentProcess);
handles.push_back(hReadPipeInheritable->get());
std::vector<std::wstring> args;
std::wstring argstr;
args.emplace_back((std::filesystem::path(workingDirectory) / "DalamudCrashHandler.exe").wstring());
args.emplace_back(std::format(L"--process-handle={}", reinterpret_cast<size_t>(hInheritableCurrentProcess)));
args.emplace_back(std::format(L"--exception-info-pipe-read-handle={}", reinterpret_cast<size_t>(hReadPipeInheritable->get())));
args.emplace_back(std::format(L"--asset-directory={}", unicode::convert<std::wstring>(g_startInfo.AssetDirectory)));
args.emplace_back(std::format(L"--log-directory={}", g_startInfo.BootLogPath.empty()
? utils::loaded_module(g_hModule).path().parent_path().wstring()
: std::filesystem::path(unicode::convert<std::wstring>(g_startInfo.BootLogPath)).parent_path().wstring()));
args.emplace_back(L"--");
append_injector_launch_args(args);
for (const auto& arg : args)
{
argstr.append(utils::escape_shell_arg(arg));
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());
return false;
}
if (!CreateProcessW(
args[0].c_str(), // The path
&argstr[0], // Command line
nullptr, // Process handle not inheritable
nullptr, // Thread handle not inheritable
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
&siex.StartupInfo, // Pointer to STARTUPINFO structure
&pi // Pointer to PROCESS_INFORMATION structure (removed extra parentheses)
))
{
logging::W("Failed to launch DalamudCrashHandler.exe: CreateProcessW error 0x{:x}", GetLastError());
return false;
}
CloseHandle(pi.hThread);
g_crashhandler_process = pi.hProcess;
g_crashhandler_pipe_write = hWritePipe->release();
logging::I("Launched DalamudCrashHandler.exe: PID {}", pi.dwProcessId);
return true;
}
bool veh::remove_handler()

View file

@ -2,6 +2,6 @@
namespace veh
{
bool add_handler(bool doFullDump);
bool add_handler(bool doFullDump, const std::string& workingDirectory);
bool remove_handler();
}

View file

@ -493,6 +493,49 @@ void xivfixes::backup_userdata_save(bool bApply) {
}
}
void xivfixes::clr_failfast_hijack(bool bApply)
{
static const char* LogTag = "[xivfixes:clr_failfast_hijack]";
static std::optional<hooks::import_hook<decltype(RaiseFailFastException)>> s_HookClrFatalError;
static std::optional<hooks::import_hook<decltype(SetUnhandledExceptionFilter)>> s_HookSetUnhandledExceptionFilter;
if (bApply)
{
if (!g_startInfo.BootEnabledGameFixes.contains("clr_failfast_hijack")) {
logging::I("{} Turned off via environment variable.", LogTag);
return;
}
s_HookClrFatalError.emplace("kernel32.dll!RaiseFailFastException (import, backup_userdata_save)", "kernel32.dll", "RaiseFailFastException", 0);
s_HookSetUnhandledExceptionFilter.emplace("kernel32.dll!SetUnhandledExceptionFilter (lpTopLevelExceptionFilter)", "kernel32.dll", "SetUnhandledExceptionFilter", 0);
s_HookClrFatalError->set_detour([](PEXCEPTION_RECORD pExceptionRecord,
_In_opt_ PCONTEXT pContextRecord,
_In_ DWORD dwFlags)
{
MessageBoxW(nullptr, L"An error in a Dalamud plugin was detected and the game cannot continue.\n\nPlease take a screenshot of this error message and let us know about it.", L"Dalamud", MB_OK | MB_ICONERROR);
return s_HookClrFatalError->call_original(pExceptionRecord, pContextRecord, dwFlags);
});
s_HookSetUnhandledExceptionFilter->set_detour([](LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter) -> LPTOP_LEVEL_EXCEPTION_FILTER
{
logging::I("{} SetUnhandledExceptionFilter", LogTag);
return nullptr;
});
logging::I("{} Enable", LogTag);
}
else
{
if (s_HookClrFatalError) {
logging::I("{} Disable ClrFatalError", LogTag);
s_HookClrFatalError.reset();
s_HookSetUnhandledExceptionFilter.reset();
}
}
}
void xivfixes::apply_all(bool bApply) {
for (const auto& [taskName, taskFunction] : std::initializer_list<std::pair<const char*, void(*)(bool)>>
{
@ -501,6 +544,7 @@ 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 }
}
) {
try {

View file

@ -6,6 +6,7 @@ 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 apply_all(bool bApply);
}

View file

@ -60,7 +60,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Iced" Version="1.13.0" />
<PackageReference Include="Iced" Version="1.17.0" />
<PackageReference Include="JetBrains.Annotations" Version="2022.1.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="PeNet" Version="2.6.4" />

View file

@ -7,6 +7,7 @@ using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
using Dalamud.Game;
using Newtonsoft.Json;
@ -317,12 +318,13 @@ namespace Dalamud.Injector
startInfo.BootShowConsole = args.Contains("--console");
startInfo.BootEnableEtw = args.Contains("--etw");
startInfo.BootLogPath = GetLogPath("dalamud.boot");
startInfo.BootEnabledGameFixes = new List<string> { "prevent_devicechange_crashes", "disable_game_openprocess_access_check", "redirect_openprocess", "backup_userdata_save" };
startInfo.BootEnabledGameFixes = new List<string> { "prevent_devicechange_crashes", "disable_game_openprocess_access_check", "redirect_openprocess", "backup_userdata_save", "clr_failfast_hijack" };
startInfo.BootDotnetOpenProcessHookMode = 0;
startInfo.BootWaitMessageBox |= args.Contains("--msgbox1") ? 1 : 0;
startInfo.BootWaitMessageBox |= args.Contains("--msgbox2") ? 2 : 0;
startInfo.BootWaitMessageBox |= args.Contains("--msgbox3") ? 4 : 0;
startInfo.BootVehEnabled = args.Contains("--veh");
// startInfo.BootVehEnabled = args.Contains("--veh");
startInfo.BootVehEnabled = true;
startInfo.BootVehFull = args.Contains("--veh-full");
startInfo.NoLoadPlugins = args.Contains("--no-plugin");
startInfo.NoLoadThirdPartyPlugins = args.Contains("--no-third-plugin");
@ -483,6 +485,7 @@ namespace Dalamud.Injector
var withoutDalamud = false;
var noFixAcl = false;
var waitForGameWindow = true;
var encryptArguments = false;
var parsingGameArgument = false;
for (var i = 2; i < args.Count; i++)
@ -519,6 +522,43 @@ namespace Dalamud.Injector
throw new CommandLineException($"\"{args[i]}\" is not a command line argument.");
}
var checksumTable = "fX1pGtdS5CAP4_VL";
var argDelimiterRegex = new Regex(" (?<!(?:^|[^ ])(?: )*)/");
var kvDelimiterRegex = new Regex(" (?<!(?:^|[^ ])(?: )*)=");
gameArguments = gameArguments.SelectMany(x =>
{
if (!x.StartsWith("//**sqex0003") || !x.EndsWith("**//"))
return new List<string>() { x };
var checksum = checksumTable.IndexOf(x[x.Length - 5]);
if (checksum == -1)
return new List<string>() { x };
var encData = Convert.FromBase64String(x.Substring(12, x.Length - 12 - 5).Replace('-', '+').Replace('_', '/').Replace('*', '='));
var rawData = new byte[encData.Length];
for (var i = (uint)checksum; i < 0x10000u; i += 0x10)
{
var bf = new LegacyBlowfish(Encoding.UTF8.GetBytes($"{i << 16:x08}"));
Buffer.BlockCopy(encData, 0, rawData, 0, rawData.Length);
bf.Decrypt(ref rawData);
var rawString = Encoding.UTF8.GetString(rawData).Split('\0', 2).First();
encryptArguments = true;
var args = argDelimiterRegex.Split(rawString).Skip(1).Select(y => string.Join('=', kvDelimiterRegex.Split(y, 2)).Replace(" ", " ")).ToList();
if (!args.Any())
continue;
if (!args.First().StartsWith("T="))
continue;
if (!uint.TryParse(args.First().Substring(2), out var tickCount))
continue;
if (tickCount >> 16 != i)
continue;
return args.Skip(1);
}
return new List<string>() { x };
}).ToList();
if (showHelp)
{
ProcessHelpCommand(args, "launch");
@ -602,7 +642,39 @@ namespace Dalamud.Injector
});
}
var gameArgumentString = string.Join(" ", gameArguments.Select(x => EncodeParameterArgument(x)));
string gameArgumentString;
if (encryptArguments)
{
var rawTickCount = (uint)Environment.TickCount;
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
[System.Runtime.InteropServices.DllImport("c")]
static extern ulong clock_gettime_nsec_np(int clock_id);
const int CLOCK_MONOTONIC_RAW = 4;
var rawTickCountFixed = (clock_gettime_nsec_np(CLOCK_MONOTONIC_RAW) / 1000000);
Log.Information("ArgumentBuilder::DeriveKey() fixing up rawTickCount from {0} to {1} on macOS", rawTickCount, rawTickCountFixed);
rawTickCount = (uint)rawTickCountFixed;
}
var ticks = rawTickCount & 0xFFFF_FFFFu;
var key = ticks & 0xFFFF_0000u;
gameArguments.Insert(0, $"T={ticks}");
var escapeValue = (string x) => x.Replace(" ", " ");
gameArgumentString = gameArguments.Select(x => x.Split('=', 2)).Aggregate(new StringBuilder(), (whole, part) => whole.Append($" /{escapeValue(part[0])} ={escapeValue(part.Length > 1 ? part[1] : string.Empty)}")).ToString();
var bf = new LegacyBlowfish(Encoding.UTF8.GetBytes($"{key:x08}"));
var ciphertext = bf.Encrypt(Encoding.UTF8.GetBytes(gameArgumentString));
var base64Str = Convert.ToBase64String(ciphertext).Replace('+', '-').Replace('/', '_').Replace('=', '*');
var checksum = checksumTable[(int)(key >> 16) & 0xF];
gameArgumentString = $"//**sqex0003{base64Str}{checksum}**//";
}
else
{
gameArgumentString = string.Join(" ", gameArguments.Select(x => EncodeParameterArgument(x)));
}
var process = GameStart.LaunchGame(Path.GetDirectoryName(gamePath), gamePath, gameArgumentString, noFixAcl, (Process p) =>
{
if (!withoutDalamud && mode == "entrypoint")
@ -731,7 +803,7 @@ namespace Dalamud.Injector
using var startInfoBuffer = new MemoryBufferHelper(process).CreatePrivateMemoryBuffer(startInfoBytes.Length + 0x8);
var startInfoAddress = startInfoBuffer.Add(startInfoBytes);
if (startInfoAddress == UIntPtr.Zero)
if (startInfoAddress == 0)
throw new Exception("Unable to allocate start info JSON");
injector.GetFunctionAddress(bootModule, "Initialize", out var initAddress);

View file

@ -32,11 +32,11 @@ namespace Dalamud.Injector
private readonly CircularBuffer circularBuffer;
private readonly PrivateMemoryBuffer memoryBuffer;
private UIntPtr loadLibraryShellPtr;
private UIntPtr loadLibraryRetPtr;
private nuint loadLibraryShellPtr;
private nuint loadLibraryRetPtr;
private UIntPtr getProcAddressShellPtr;
private UIntPtr getProcAddressRetPtr;
private nuint getProcAddressShellPtr;
private nuint getProcAddressRetPtr;
/// <summary>
/// Initializes a new instance of the <see cref="Injector"/> class.
@ -81,16 +81,16 @@ namespace Dalamud.Injector
/// </summary>
/// <param name="modulePath">Absolute file path.</param>
/// <param name="address">Address to the module.</param>
public void LoadLibrary(string modulePath, out UIntPtr address)
public void LoadLibrary(string modulePath, out IntPtr address)
{
var lpParameter = this.WriteNullTerminatedUnicodeString(modulePath);
if (lpParameter == UIntPtr.Zero)
if (lpParameter == 0)
throw new Exception("Unable to allocate LoadLibraryW parameter");
this.CallRemoteFunction(this.loadLibraryShellPtr, lpParameter, out var err);
this.extMemory.Read(this.loadLibraryRetPtr, out address);
if (address == UIntPtr.Zero)
this.extMemory.Read<IntPtr>(this.loadLibraryRetPtr, out address);
if (address == IntPtr.Zero)
throw new Exception($"LoadLibraryW(\"{modulePath}\") failure: {new Win32Exception((int)err).Message} ({err})");
}
@ -100,17 +100,17 @@ namespace Dalamud.Injector
/// <param name="module">Module address.</param>
/// <param name="functionName">Name of the exported method.</param>
/// <param name="address">Address to the function.</param>
public void GetFunctionAddress(UIntPtr module, string functionName, out UIntPtr address)
public void GetFunctionAddress(IntPtr module, string functionName, out nuint address)
{
var functionNamePtr = this.WriteNullTerminatedASCIIString(functionName);
var getProcAddressParams = new GetProcAddressParams(module, functionNamePtr);
var lpParameter = this.circularBuffer.Add(ref getProcAddressParams);
if (lpParameter == UIntPtr.Zero)
if (lpParameter == 0)
throw new Exception("Unable to allocate GetProcAddress parameter ptr");
this.CallRemoteFunction(this.getProcAddressShellPtr, lpParameter, out var err);
this.extMemory.Read(this.getProcAddressRetPtr, out address);
if (address == UIntPtr.Zero)
this.extMemory.Read<nuint>(this.getProcAddressRetPtr, out address);
if (address == 0)
throw new Exception($"GetProcAddress(0x{module:X}, \"{functionName}\") failure: {new Win32Exception((int)err).Message} ({err})");
}
@ -120,7 +120,7 @@ namespace Dalamud.Injector
/// <param name="methodAddress">Method address.</param>
/// <param name="parameterAddress">Parameter address.</param>
/// <param name="exitCode">Thread exit code.</param>
public void CallRemoteFunction(UIntPtr methodAddress, UIntPtr parameterAddress, out uint exitCode)
public void CallRemoteFunction(nuint methodAddress, nuint parameterAddress, out uint exitCode)
{
// Create and initialize a thread at our address and parameter address.
var threadHandle = CreateRemoteThread(
@ -150,27 +150,24 @@ namespace Dalamud.Injector
var functionAddr = kernel32Module.BaseAddress + (int)this.GetExportedFunctionOffset(kernel32Exports, "LoadLibraryW");
Log.Verbose($"LoadLibraryW: 0x{functionAddr.ToInt64():X}");
var functionPtr = (UIntPtr)this.memoryBuffer.Add(ref functionAddr);
Log.Verbose($"LoadLibraryPtr: 0x{functionPtr.ToUInt64():X}");
var functionPtr = this.memoryBuffer.Add(ref functionAddr);
Log.Verbose($"LoadLibraryPtr: 0x{functionPtr:X}");
if (functionPtr == UIntPtr.Zero)
if (functionPtr == 0)
throw new Exception("Unable to allocate LoadLibraryW function ptr");
var dummy = IntPtr.Zero;
this.loadLibraryRetPtr = this.memoryBuffer.Add(ref dummy);
Log.Verbose($"LoadLibraryRetPtr: 0x{this.loadLibraryRetPtr.ToUInt64():X}");
Log.Verbose($"LoadLibraryRetPtr: 0x{this.loadLibraryRetPtr:X}");
if (this.loadLibraryRetPtr == UIntPtr.Zero)
if (this.loadLibraryRetPtr == 0)
throw new Exception("Unable to allocate LoadLibraryW return value");
var func = functionPtr.ToUInt64();
var retVal = this.loadLibraryRetPtr.ToUInt64();
var asm = new Assembler(64);
asm.sub(rsp, 40); // sub rsp, 40 // Re-align stack to 16 byte boundary + shadow space.
asm.call(__qword_ptr[__qword_ptr[func]]); // call qword [qword func] // CreateRemoteThread lpParameter with string already in ECX.
asm.mov(__qword_ptr[__qword_ptr[retVal]], rax); // mov qword [qword retVal], rax //
asm.call(__qword_ptr[__qword_ptr[functionPtr]]); // call qword [qword func] // CreateRemoteThread lpParameter with string already in ECX.
asm.mov(__qword_ptr[__qword_ptr[this.loadLibraryRetPtr]], rax); // mov qword [qword retVal], rax //
asm.add(rsp, 40); // add rsp, 40 // Re-align stack to 16 byte boundary + shadow space.
asm.mov(rax, (ulong)getLastErrorAddr); // mov rax, pfnGetLastError // Change return address to GetLastError.
asm.push(rax); // push rax //
@ -178,9 +175,9 @@ namespace Dalamud.Injector
var bytes = this.Assemble(asm);
this.loadLibraryShellPtr = this.memoryBuffer.Add(bytes);
Log.Verbose($"LoadLibraryShellPtr: 0x{this.loadLibraryShellPtr.ToUInt64():X}");
Log.Verbose($"LoadLibraryShellPtr: 0x{this.loadLibraryShellPtr:X}");
if (this.loadLibraryShellPtr == UIntPtr.Zero)
if (this.loadLibraryShellPtr == 0)
throw new Exception("Unable to allocate LoadLibraryW shellcode");
this.extMemory.ChangePermission(this.loadLibraryShellPtr, bytes.Length, Reloaded.Memory.Kernel32.Kernel32.MEM_PROTECTION.PAGE_EXECUTE_READWRITE);
@ -193,7 +190,7 @@ namespace Dalamud.Injector
Log.Verbose($"LoadLibraryRet: {this.GetResultMarker(dummy == outRetPtr)}");
this.extMemory.ReadRaw(this.loadLibraryShellPtr, out var outBytes, bytes.Length);
Log.Verbose($"LoadLibraryShellPtr: {this.GetResultMarker(bytes.SequenceEqual(outBytes))}");
Log.Verbose($"LoadLibraryShellPtr: {this.GetResultMarker(Enumerable.SequenceEqual(bytes, outBytes))}");
#endif
}
@ -206,29 +203,26 @@ namespace Dalamud.Injector
var functionAddr = kernel32Module.BaseAddress + (int)offset;
Log.Verbose($"GetProcAddress: 0x{functionAddr.ToInt64():X}");
var functionPtr = (UIntPtr)this.memoryBuffer.Add(ref functionAddr);
Log.Verbose($"GetProcAddressPtr: 0x{functionPtr.ToUInt64():X}");
var functionPtr = this.memoryBuffer.Add(ref functionAddr);
Log.Verbose($"GetProcAddressPtr: 0x{functionPtr:X}");
if (functionPtr == UIntPtr.Zero)
if (functionPtr == 0)
throw new Exception("Unable to allocate GetProcAddress function ptr");
var dummy = IntPtr.Zero;
this.getProcAddressRetPtr = this.memoryBuffer.Add(ref dummy);
Log.Verbose($"GetProcAddressRetPtr: 0x{this.loadLibraryRetPtr.ToUInt64():X}");
Log.Verbose($"GetProcAddressRetPtr: 0x{this.loadLibraryRetPtr:X}");
if (this.getProcAddressRetPtr == UIntPtr.Zero)
if (this.getProcAddressRetPtr == 0)
throw new Exception("Unable to allocate GetProcAddress return value");
var func = functionPtr.ToUInt64();
var retVal = this.getProcAddressRetPtr.ToUInt64();
var asm = new Assembler(64);
asm.sub(rsp, 40); // sub rsp, 40 // Re-align stack to 16 byte boundary +32 shadow space
asm.mov(rdx, __qword_ptr[__qword_ptr[rcx + 8]]); // mov rdx, qword [qword rcx + 8] // lpProcName
asm.mov(rcx, __qword_ptr[__qword_ptr[rcx + 0]]); // mov rcx, qword [qword rcx + 0] // hModule
asm.call(__qword_ptr[__qword_ptr[func]]); // call qword [qword func] //
asm.mov(__qword_ptr[__qword_ptr[retVal]], rax); // mov qword [qword retVal] //
asm.call(__qword_ptr[__qword_ptr[functionPtr]]); // call qword [qword func] //
asm.mov(__qword_ptr[__qword_ptr[this.getProcAddressRetPtr]], rax); // mov qword [qword retVal] //
asm.add(rsp, 40); // add rsp, 40 // Re-align stack to 16 byte boundary + shadow space.
asm.mov(rax, (ulong)getLastErrorAddr); // mov rax, pfnGetLastError // Change return address to GetLastError.
asm.push(rax); // push rax //
@ -236,9 +230,9 @@ namespace Dalamud.Injector
var bytes = this.Assemble(asm);
this.getProcAddressShellPtr = this.memoryBuffer.Add(bytes);
Log.Verbose($"GetProcAddressShellPtr: 0x{this.getProcAddressShellPtr.ToUInt64():X}");
Log.Verbose($"GetProcAddressShellPtr: 0x{this.getProcAddressShellPtr:X}");
if (this.getProcAddressShellPtr == UIntPtr.Zero)
if (this.getProcAddressShellPtr == 0)
throw new Exception("Unable to allocate GetProcAddress shellcode");
this.extMemory.ChangePermission(this.getProcAddressShellPtr, bytes.Length, Reloaded.Memory.Kernel32.Kernel32.MEM_PROTECTION.PAGE_EXECUTE_READWRITE);
@ -251,7 +245,7 @@ namespace Dalamud.Injector
Log.Verbose($"GetProcAddressRet: {this.GetResultMarker(dummy == outRetPtr)}");
this.extMemory.ReadRaw(this.getProcAddressShellPtr, out var outBytes, bytes.Length);
Log.Verbose($"GetProcAddressShellPtr: {this.GetResultMarker(bytes.SequenceEqual(outBytes))}");
Log.Verbose($"GetProcAddressShellPtr: {this.GetResultMarker(Enumerable.SequenceEqual(bytes, outBytes))}");
#endif
}
@ -298,33 +292,33 @@ namespace Dalamud.Injector
return exportFunction.Address;
}
private UIntPtr WriteNullTerminatedASCIIString(string value)
private nuint WriteNullTerminatedASCIIString(string value)
{
var bytes = Encoding.ASCII.GetBytes(value + '\0');
var address = (UIntPtr)this.circularBuffer.Add(bytes);
var address = this.circularBuffer.Add(bytes);
if (address == UIntPtr.Zero)
if (address == 0)
throw new Exception("Unable to write ASCII string to buffer");
#if DEBUG
this.extMemory.ReadRaw(address, out var outBytes, bytes.Length);
Log.Verbose($"WriteASCII: {this.GetResultMarker(bytes.SequenceEqual(outBytes))} 0x{address.ToUInt64():X} {value}");
Log.Verbose($"WriteASCII: {this.GetResultMarker(Enumerable.SequenceEqual(bytes, outBytes))} 0x{address:X} {value}");
#endif
return address;
}
private UIntPtr WriteNullTerminatedUnicodeString(string value)
private nuint WriteNullTerminatedUnicodeString(string value)
{
var bytes = Encoding.Unicode.GetBytes(value + '\0');
var address = (UIntPtr)this.circularBuffer.Add(bytes);
var address = this.circularBuffer.Add(bytes);
if (address == UIntPtr.Zero)
if (address == 0)
throw new Exception("Unable to write Unicode string to buffer");
#if DEBUG
this.extMemory.ReadRaw(address, out var outBytes, bytes.Length);
Log.Verbose($"WriteUnicode: {this.GetResultMarker(bytes.SequenceEqual(outBytes))} 0x{address.ToUInt64():X} {value}");
Log.Verbose($"WriteUnicode: {this.GetResultMarker(Enumerable.SequenceEqual(bytes, outBytes))} 0x{address:X} {value}");
#endif
return address;
@ -337,15 +331,15 @@ namespace Dalamud.Injector
[StructLayout(LayoutKind.Sequential)]
private struct GetProcAddressParams
{
public GetProcAddressParams(UIntPtr hModule, UIntPtr lPProcName)
public GetProcAddressParams(IntPtr hModule, nuint lPProcName)
{
this.HModule = (long)hModule.ToUInt64();
this.LPProcName = (long)lPProcName.ToUInt64();
this.HModule = hModule.ToInt64();
this.LPProcName = lPProcName;
}
public long HModule { get; set; }
public long LPProcName { get; set; }
public nuint LPProcName { get; set; }
}
}
}

View file

@ -0,0 +1,319 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Dalamud.Injector
{
internal class LegacyBlowfish
{
#region P-Array and S-Boxes
private readonly uint[] p =
{
0x243f6a88, 0x85a308d3, 0x13198a2e, 0x03707344, 0xa4093822, 0x299f31d0,
0x082efa98, 0xec4e6c89, 0x452821e6, 0x38d01377, 0xbe5466cf, 0x34e90c6c,
0xc0ac29b7, 0xc97c50dd, 0x3f84d5b5, 0xb5470917, 0x9216d5d9, 0x8979fb1b
};
private readonly uint[,] s =
{
{
0xd1310ba6, 0x98dfb5ac, 0x2ffd72db, 0xd01adfb7, 0xb8e1afed, 0x6a267e96,
0xba7c9045, 0xf12c7f99, 0x24a19947, 0xb3916cf7, 0x0801f2e2, 0x858efc16,
0x636920d8, 0x71574e69, 0xa458fea3, 0xf4933d7e, 0x0d95748f, 0x728eb658,
0x718bcd58, 0x82154aee, 0x7b54a41d, 0xc25a59b5, 0x9c30d539, 0x2af26013,
0xc5d1b023, 0x286085f0, 0xca417918, 0xb8db38ef, 0x8e79dcb0, 0x603a180e,
0x6c9e0e8b, 0xb01e8a3e, 0xd71577c1, 0xbd314b27, 0x78af2fda, 0x55605c60,
0xe65525f3, 0xaa55ab94, 0x57489862, 0x63e81440, 0x55ca396a, 0x2aab10b6,
0xb4cc5c34, 0x1141e8ce, 0xa15486af, 0x7c72e993, 0xb3ee1411, 0x636fbc2a,
0x2ba9c55d, 0x741831f6, 0xce5c3e16, 0x9b87931e, 0xafd6ba33, 0x6c24cf5c,
0x7a325381, 0x28958677, 0x3b8f4898, 0x6b4bb9af, 0xc4bfe81b, 0x66282193,
0x61d809cc, 0xfb21a991, 0x487cac60, 0x5dec8032, 0xef845d5d, 0xe98575b1,
0xdc262302, 0xeb651b88, 0x23893e81, 0xd396acc5, 0x0f6d6ff3, 0x83f44239,
0x2e0b4482, 0xa4842004, 0x69c8f04a, 0x9e1f9b5e, 0x21c66842, 0xf6e96c9a,
0x670c9c61, 0xabd388f0, 0x6a51a0d2, 0xd8542f68, 0x960fa728, 0xab5133a3,
0x6eef0b6c, 0x137a3be4, 0xba3bf050, 0x7efb2a98, 0xa1f1651d, 0x39af0176,
0x66ca593e, 0x82430e88, 0x8cee8619, 0x456f9fb4, 0x7d84a5c3, 0x3b8b5ebe,
0xe06f75d8, 0x85c12073, 0x401a449f, 0x56c16aa6, 0x4ed3aa62, 0x363f7706,
0x1bfedf72, 0x429b023d, 0x37d0d724, 0xd00a1248, 0xdb0fead3, 0x49f1c09b,
0x075372c9, 0x80991b7b, 0x25d479d8, 0xf6e8def7, 0xe3fe501a, 0xb6794c3b,
0x976ce0bd, 0x04c006ba, 0xc1a94fb6, 0x409f60c4, 0x5e5c9ec2, 0x196a2463,
0x68fb6faf, 0x3e6c53b5, 0x1339b2eb, 0x3b52ec6f, 0x6dfc511f, 0x9b30952c,
0xcc814544, 0xaf5ebd09, 0xbee3d004, 0xde334afd, 0x660f2807, 0x192e4bb3,
0xc0cba857, 0x45c8740f, 0xd20b5f39, 0xb9d3fbdb, 0x5579c0bd, 0x1a60320a,
0xd6a100c6, 0x402c7279, 0x679f25fe, 0xfb1fa3cc, 0x8ea5e9f8, 0xdb3222f8,
0x3c7516df, 0xfd616b15, 0x2f501ec8, 0xad0552ab, 0x323db5fa, 0xfd238760,
0x53317b48, 0x3e00df82, 0x9e5c57bb, 0xca6f8ca0, 0x1a87562e, 0xdf1769db,
0xd542a8f6, 0x287effc3, 0xac6732c6, 0x8c4f5573, 0x695b27b0, 0xbbca58c8,
0xe1ffa35d, 0xb8f011a0, 0x10fa3d98, 0xfd2183b8, 0x4afcb56c, 0x2dd1d35b,
0x9a53e479, 0xb6f84565, 0xd28e49bc, 0x4bfb9790, 0xe1ddf2da, 0xa4cb7e33,
0x62fb1341, 0xcee4c6e8, 0xef20cada, 0x36774c01, 0xd07e9efe, 0x2bf11fb4,
0x95dbda4d, 0xae909198, 0xeaad8e71, 0x6b93d5a0, 0xd08ed1d0, 0xafc725e0,
0x8e3c5b2f, 0x8e7594b7, 0x8ff6e2fb, 0xf2122b64, 0x8888b812, 0x900df01c,
0x4fad5ea0, 0x688fc31c, 0xd1cff191, 0xb3a8c1ad, 0x2f2f2218, 0xbe0e1777,
0xea752dfe, 0x8b021fa1, 0xe5a0cc0f, 0xb56f74e8, 0x18acf3d6, 0xce89e299,
0xb4a84fe0, 0xfd13e0b7, 0x7cc43b81, 0xd2ada8d9, 0x165fa266, 0x80957705,
0x93cc7314, 0x211a1477, 0xe6ad2065, 0x77b5fa86, 0xc75442f5, 0xfb9d35cf,
0xebcdaf0c, 0x7b3e89a0, 0xd6411bd3, 0xae1e7e49, 0x00250e2d, 0x2071b35e,
0x226800bb, 0x57b8e0af, 0x2464369b, 0xf009b91e, 0x5563911d, 0x59dfa6aa,
0x78c14389, 0xd95a537f, 0x207d5ba2, 0x02e5b9c5, 0x83260376, 0x6295cfa9,
0x11c81968, 0x4e734a41, 0xb3472dca, 0x7b14a94a, 0x1b510052, 0x9a532915,
0xd60f573f, 0xbc9bc6e4, 0x2b60a476, 0x81e67400, 0x08ba6fb5, 0x571be91f,
0xf296ec6b, 0x2a0dd915, 0xb6636521, 0xe7b9f9b6, 0xff34052e, 0xc5855664,
0x53b02d5d, 0xa99f8fa1, 0x08ba4799, 0x6e85076a
},
{
0x4b7a70e9, 0xb5b32944, 0xdb75092e, 0xc4192623, 0xad6ea6b0, 0x49a7df7d,
0x9cee60b8, 0x8fedb266, 0xecaa8c71, 0x699a17ff, 0x5664526c, 0xc2b19ee1,
0x193602a5, 0x75094c29, 0xa0591340, 0xe4183a3e, 0x3f54989a, 0x5b429d65,
0x6b8fe4d6, 0x99f73fd6, 0xa1d29c07, 0xefe830f5, 0x4d2d38e6, 0xf0255dc1,
0x4cdd2086, 0x8470eb26, 0x6382e9c6, 0x021ecc5e, 0x09686b3f, 0x3ebaefc9,
0x3c971814, 0x6b6a70a1, 0x687f3584, 0x52a0e286, 0xb79c5305, 0xaa500737,
0x3e07841c, 0x7fdeae5c, 0x8e7d44ec, 0x5716f2b8, 0xb03ada37, 0xf0500c0d,
0xf01c1f04, 0x0200b3ff, 0xae0cf51a, 0x3cb574b2, 0x25837a58, 0xdc0921bd,
0xd19113f9, 0x7ca92ff6, 0x94324773, 0x22f54701, 0x3ae5e581, 0x37c2dadc,
0xc8b57634, 0x9af3dda7, 0xa9446146, 0x0fd0030e, 0xecc8c73e, 0xa4751e41,
0xe238cd99, 0x3bea0e2f, 0x3280bba1, 0x183eb331, 0x4e548b38, 0x4f6db908,
0x6f420d03, 0xf60a04bf, 0x2cb81290, 0x24977c79, 0x5679b072, 0xbcaf89af,
0xde9a771f, 0xd9930810, 0xb38bae12, 0xdccf3f2e, 0x5512721f, 0x2e6b7124,
0x501adde6, 0x9f84cd87, 0x7a584718, 0x7408da17, 0xbc9f9abc, 0xe94b7d8c,
0xec7aec3a, 0xdb851dfa, 0x63094366, 0xc464c3d2, 0xef1c1847, 0x3215d908,
0xdd433b37, 0x24c2ba16, 0x12a14d43, 0x2a65c451, 0x50940002, 0x133ae4dd,
0x71dff89e, 0x10314e55, 0x81ac77d6, 0x5f11199b, 0x043556f1, 0xd7a3c76b,
0x3c11183b, 0x5924a509, 0xf28fe6ed, 0x97f1fbfa, 0x9ebabf2c, 0x1e153c6e,
0x86e34570, 0xeae96fb1, 0x860e5e0a, 0x5a3e2ab3, 0x771fe71c, 0x4e3d06fa,
0x2965dcb9, 0x99e71d0f, 0x803e89d6, 0x5266c825, 0x2e4cc978, 0x9c10b36a,
0xc6150eba, 0x94e2ea78, 0xa5fc3c53, 0x1e0a2df4, 0xf2f74ea7, 0x361d2b3d,
0x1939260f, 0x19c27960, 0x5223a708, 0xf71312b6, 0xebadfe6e, 0xeac31f66,
0xe3bc4595, 0xa67bc883, 0xb17f37d1, 0x018cff28, 0xc332ddef, 0xbe6c5aa5,
0x65582185, 0x68ab9802, 0xeecea50f, 0xdb2f953b, 0x2aef7dad, 0x5b6e2f84,
0x1521b628, 0x29076170, 0xecdd4775, 0x619f1510, 0x13cca830, 0xeb61bd96,
0x0334fe1e, 0xaa0363cf, 0xb5735c90, 0x4c70a239, 0xd59e9e0b, 0xcbaade14,
0xeecc86bc, 0x60622ca7, 0x9cab5cab, 0xb2f3846e, 0x648b1eaf, 0x19bdf0ca,
0xa02369b9, 0x655abb50, 0x40685a32, 0x3c2ab4b3, 0x319ee9d5, 0xc021b8f7,
0x9b540b19, 0x875fa099, 0x95f7997e, 0x623d7da8, 0xf837889a, 0x97e32d77,
0x11ed935f, 0x16681281, 0x0e358829, 0xc7e61fd6, 0x96dedfa1, 0x7858ba99,
0x57f584a5, 0x1b227263, 0x9b83c3ff, 0x1ac24696, 0xcdb30aeb, 0x532e3054,
0x8fd948e4, 0x6dbc3128, 0x58ebf2ef, 0x34c6ffea, 0xfe28ed61, 0xee7c3c73,
0x5d4a14d9, 0xe864b7e3, 0x42105d14, 0x203e13e0, 0x45eee2b6, 0xa3aaabea,
0xdb6c4f15, 0xfacb4fd0, 0xc742f442, 0xef6abbb5, 0x654f3b1d, 0x41cd2105,
0xd81e799e, 0x86854dc7, 0xe44b476a, 0x3d816250, 0xcf62a1f2, 0x5b8d2646,
0xfc8883a0, 0xc1c7b6a3, 0x7f1524c3, 0x69cb7492, 0x47848a0b, 0x5692b285,
0x095bbf00, 0xad19489d, 0x1462b174, 0x23820e00, 0x58428d2a, 0x0c55f5ea,
0x1dadf43e, 0x233f7061, 0x3372f092, 0x8d937e41, 0xd65fecf1, 0x6c223bdb,
0x7cde3759, 0xcbee7460, 0x4085f2a7, 0xce77326e, 0xa6078084, 0x19f8509e,
0xe8efd855, 0x61d99735, 0xa969a7aa, 0xc50c06c2, 0x5a04abfc, 0x800bcadc,
0x9e447a2e, 0xc3453484, 0xfdd56705, 0x0e1e9ec9, 0xdb73dbd3, 0x105588cd,
0x675fda79, 0xe3674340, 0xc5c43465, 0x713e38d8, 0x3d28f89e, 0xf16dff20,
0x153e21e7, 0x8fb03d4a, 0xe6e39f2b, 0xdb83adf7
},
{
0xe93d5a68, 0x948140f7, 0xf64c261c, 0x94692934, 0x411520f7, 0x7602d4f7,
0xbcf46b2e, 0xd4a20068, 0xd4082471, 0x3320f46a, 0x43b7d4b7, 0x500061af,
0x1e39f62e, 0x97244546, 0x14214f74, 0xbf8b8840, 0x4d95fc1d, 0x96b591af,
0x70f4ddd3, 0x66a02f45, 0xbfbc09ec, 0x03bd9785, 0x7fac6dd0, 0x31cb8504,
0x96eb27b3, 0x55fd3941, 0xda2547e6, 0xabca0a9a, 0x28507825, 0x530429f4,
0x0a2c86da, 0xe9b66dfb, 0x68dc1462, 0xd7486900, 0x680ec0a4, 0x27a18dee,
0x4f3ffea2, 0xe887ad8c, 0xb58ce006, 0x7af4d6b6, 0xaace1e7c, 0xd3375fec,
0xce78a399, 0x406b2a42, 0x20fe9e35, 0xd9f385b9, 0xee39d7ab, 0x3b124e8b,
0x1dc9faf7, 0x4b6d1856, 0x26a36631, 0xeae397b2, 0x3a6efa74, 0xdd5b4332,
0x6841e7f7, 0xca7820fb, 0xfb0af54e, 0xd8feb397, 0x454056ac, 0xba489527,
0x55533a3a, 0x20838d87, 0xfe6ba9b7, 0xd096954b, 0x55a867bc, 0xa1159a58,
0xcca92963, 0x99e1db33, 0xa62a4a56, 0x3f3125f9, 0x5ef47e1c, 0x9029317c,
0xfdf8e802, 0x04272f70, 0x80bb155c, 0x05282ce3, 0x95c11548, 0xe4c66d22,
0x48c1133f, 0xc70f86dc, 0x07f9c9ee, 0x41041f0f, 0x404779a4, 0x5d886e17,
0x325f51eb, 0xd59bc0d1, 0xf2bcc18f, 0x41113564, 0x257b7834, 0x602a9c60,
0xdff8e8a3, 0x1f636c1b, 0x0e12b4c2, 0x02e1329e, 0xaf664fd1, 0xcad18115,
0x6b2395e0, 0x333e92e1, 0x3b240b62, 0xeebeb922, 0x85b2a20e, 0xe6ba0d99,
0xde720c8c, 0x2da2f728, 0xd0127845, 0x95b794fd, 0x647d0862, 0xe7ccf5f0,
0x5449a36f, 0x877d48fa, 0xc39dfd27, 0xf33e8d1e, 0x0a476341, 0x992eff74,
0x3a6f6eab, 0xf4f8fd37, 0xa812dc60, 0xa1ebddf8, 0x991be14c, 0xdb6e6b0d,
0xc67b5510, 0x6d672c37, 0x2765d43b, 0xdcd0e804, 0xf1290dc7, 0xcc00ffa3,
0xb5390f92, 0x690fed0b, 0x667b9ffb, 0xcedb7d9c, 0xa091cf0b, 0xd9155ea3,
0xbb132f88, 0x515bad24, 0x7b9479bf, 0x763bd6eb, 0x37392eb3, 0xcc115979,
0x8026e297, 0xf42e312d, 0x6842ada7, 0xc66a2b3b, 0x12754ccc, 0x782ef11c,
0x6a124237, 0xb79251e7, 0x06a1bbe6, 0x4bfb6350, 0x1a6b1018, 0x11caedfa,
0x3d25bdd8, 0xe2e1c3c9, 0x44421659, 0x0a121386, 0xd90cec6e, 0xd5abea2a,
0x64af674e, 0xda86a85f, 0xbebfe988, 0x64e4c3fe, 0x9dbc8057, 0xf0f7c086,
0x60787bf8, 0x6003604d, 0xd1fd8346, 0xf6381fb0, 0x7745ae04, 0xd736fccc,
0x83426b33, 0xf01eab71, 0xb0804187, 0x3c005e5f, 0x77a057be, 0xbde8ae24,
0x55464299, 0xbf582e61, 0x4e58f48f, 0xf2ddfda2, 0xf474ef38, 0x8789bdc2,
0x5366f9c3, 0xc8b38e74, 0xb475f255, 0x46fcd9b9, 0x7aeb2661, 0x8b1ddf84,
0x846a0e79, 0x915f95e2, 0x466e598e, 0x20b45770, 0x8cd55591, 0xc902de4c,
0xb90bace1, 0xbb8205d0, 0x11a86248, 0x7574a99e, 0xb77f19b6, 0xe0a9dc09,
0x662d09a1, 0xc4324633, 0xe85a1f02, 0x09f0be8c, 0x4a99a025, 0x1d6efe10,
0x1ab93d1d, 0x0ba5a4df, 0xa186f20f, 0x2868f169, 0xdcb7da83, 0x573906fe,
0xa1e2ce9b, 0x4fcd7f52, 0x50115e01, 0xa70683fa, 0xa002b5c4, 0x0de6d027,
0x9af88c27, 0x773f8641, 0xc3604c06, 0x61a806b5, 0xf0177a28, 0xc0f586e0,
0x006058aa, 0x30dc7d62, 0x11e69ed7, 0x2338ea63, 0x53c2dd94, 0xc2c21634,
0xbbcbee56, 0x90bcb6de, 0xebfc7da1, 0xce591d76, 0x6f05e409, 0x4b7c0188,
0x39720a3d, 0x7c927c24, 0x86e3725f, 0x724d9db9, 0x1ac15bb4, 0xd39eb8fc,
0xed545578, 0x08fca5b5, 0xd83d7cd3, 0x4dad0fc4, 0x1e50ef5e, 0xb161e6f8,
0xa28514d9, 0x6c51133c, 0x6fd5c7e7, 0x56e14ec4, 0x362abfce, 0xddc6c837,
0xd79a3234, 0x92638212, 0x670efa8e, 0x406000e0
},
{
0x3a39ce37, 0xd3faf5cf, 0xabc27737, 0x5ac52d1b, 0x5cb0679e, 0x4fa33742,
0xd3822740, 0x99bc9bbe, 0xd5118e9d, 0xbf0f7315, 0xd62d1c7e, 0xc700c47b,
0xb78c1b6b, 0x21a19045, 0xb26eb1be, 0x6a366eb4, 0x5748ab2f, 0xbc946e79,
0xc6a376d2, 0x6549c2c8, 0x530ff8ee, 0x468dde7d, 0xd5730a1d, 0x4cd04dc6,
0x2939bbdb, 0xa9ba4650, 0xac9526e8, 0xbe5ee304, 0xa1fad5f0, 0x6a2d519a,
0x63ef8ce2, 0x9a86ee22, 0xc089c2b8, 0x43242ef6, 0xa51e03aa, 0x9cf2d0a4,
0x83c061ba, 0x9be96a4d, 0x8fe51550, 0xba645bd6, 0x2826a2f9, 0xa73a3ae1,
0x4ba99586, 0xef5562e9, 0xc72fefd3, 0xf752f7da, 0x3f046f69, 0x77fa0a59,
0x80e4a915, 0x87b08601, 0x9b09e6ad, 0x3b3ee593, 0xe990fd5a, 0x9e34d797,
0x2cf0b7d9, 0x022b8b51, 0x96d5ac3a, 0x017da67d, 0xd1cf3ed6, 0x7c7d2d28,
0x1f9f25cf, 0xadf2b89b, 0x5ad6b472, 0x5a88f54c, 0xe029ac71, 0xe019a5e6,
0x47b0acfd, 0xed93fa9b, 0xe8d3c48d, 0x283b57cc, 0xf8d56629, 0x79132e28,
0x785f0191, 0xed756055, 0xf7960e44, 0xe3d35e8c, 0x15056dd4, 0x88f46dba,
0x03a16125, 0x0564f0bd, 0xc3eb9e15, 0x3c9057a2, 0x97271aec, 0xa93a072a,
0x1b3f6d9b, 0x1e6321f5, 0xf59c66fb, 0x26dcf319, 0x7533d928, 0xb155fdf5,
0x03563482, 0x8aba3cbb, 0x28517711, 0xc20ad9f8, 0xabcc5167, 0xccad925f,
0x4de81751, 0x3830dc8e, 0x379d5862, 0x9320f991, 0xea7a90c2, 0xfb3e7bce,
0x5121ce64, 0x774fbe32, 0xa8b6e37e, 0xc3293d46, 0x48de5369, 0x6413e680,
0xa2ae0810, 0xdd6db224, 0x69852dfd, 0x09072166, 0xb39a460a, 0x6445c0dd,
0x586cdecf, 0x1c20c8ae, 0x5bbef7dd, 0x1b588d40, 0xccd2017f, 0x6bb4e3bb,
0xdda26a7e, 0x3a59ff45, 0x3e350a44, 0xbcb4cdd5, 0x72eacea8, 0xfa6484bb,
0x8d6612ae, 0xbf3c6f47, 0xd29be463, 0x542f5d9e, 0xaec2771b, 0xf64e6370,
0x740e0d8d, 0xe75b1357, 0xf8721671, 0xaf537d5d, 0x4040cb08, 0x4eb4e2cc,
0x34d2466a, 0x0115af84, 0xe1b00428, 0x95983a1d, 0x06b89fb4, 0xce6ea048,
0x6f3f3b82, 0x3520ab82, 0x011a1d4b, 0x277227f8, 0x611560b1, 0xe7933fdc,
0xbb3a792b, 0x344525bd, 0xa08839e1, 0x51ce794b, 0x2f32c9b7, 0xa01fbac9,
0xe01cc87e, 0xbcc7d1f6, 0xcf0111c3, 0xa1e8aac7, 0x1a908749, 0xd44fbd9a,
0xd0dadecb, 0xd50ada38, 0x0339c32a, 0xc6913667, 0x8df9317c, 0xe0b12b4f,
0xf79e59b7, 0x43f5bb3a, 0xf2d519ff, 0x27d9459c, 0xbf97222c, 0x15e6fc2a,
0x0f91fc71, 0x9b941525, 0xfae59361, 0xceb69ceb, 0xc2a86459, 0x12baa8d1,
0xb6c1075e, 0xe3056a0c, 0x10d25065, 0xcb03a442, 0xe0ec6e0e, 0x1698db3b,
0x4c98a0be, 0x3278e964, 0x9f1f9532, 0xe0d392df, 0xd3a0342b, 0x8971f21e,
0x1b0a7441, 0x4ba3348c, 0xc5be7120, 0xc37632d8, 0xdf359f8d, 0x9b992f2e,
0xe60b6f47, 0x0fe3f11d, 0xe54cda54, 0x1edad891, 0xce6279cf, 0xcd3e7e6f,
0x1618b166, 0xfd2c1d05, 0x848fd2c5, 0xf6fb2299, 0xf523f357, 0xa6327623,
0x93a83531, 0x56cccd02, 0xacf08162, 0x5a75ebb5, 0x6e163697, 0x88d273cc,
0xde966292, 0x81b949d0, 0x4c50901b, 0x71c65614, 0xe6c6c7bd, 0x327a140a,
0x45e1d006, 0xc3f27b9a, 0xc9aa53fd, 0x62a80f00, 0xbb25bfe2, 0x35bdd2f6,
0x71126905, 0xb2040222, 0xb6cbcf7c, 0xcd769c2b, 0x53113ec0, 0x1640e3d3,
0x38abbd60, 0x2547adf0, 0xba38209c, 0xf746ce76, 0x77afa1c5, 0x20756060,
0x85cbfe4e, 0x8ae88dd8, 0x7aaaf9b0, 0x4cf9aa7e, 0x1948c25c, 0x02fb8a8c,
0x01c36ae4, 0xd6ebe1f9, 0x90d4f869, 0xa65cdea0, 0x3f09252d, 0xc208e69f,
0xb74e6132, 0xce77e25b, 0x578fdfe3, 0x3ac372e6
}
};
#endregion
private static readonly int Rounds = 16;
/// <summary>
/// Initialize a new blowfish.
/// </summary>
/// <param name="key">The key to use.</param>
/// <param name="fucked">Whether or not a sign confusion should be introduced during key init. This is needed for SE's implementation of blowfish.</param>
public LegacyBlowfish(byte[] key)
{
foreach (var (i, keyFragment) in WrappingUInt32(key, this.p.Length))
this.p[i] ^= keyFragment;
uint l = 0, r = 0;
for (int i = 0; i < this.p.Length; i += 2)
(l, r) = (this.p[i], this.p[i + 1]) = Encrypt(l, r);
for (int i = 0; i < this.s.GetLength(0); i++)
for (int j = 0; j < this.s.GetLength(1); j += 2)
(l, r) = (this.s[i, j], this.s[i, j + 1]) = Encrypt(l, r);
}
public byte[] Encrypt(byte[] data)
{
var paddedLength = data.Length % 8 == 0 ? data.Length : data.Length + (8 - (data.Length % 8));
var buffer = new byte[paddedLength];
Buffer.BlockCopy(data, 0, buffer, 0, data.Length);
for (int i = 0; i < paddedLength; i += 8)
{
var (l, r) = Encrypt(BitConverter.ToUInt32(buffer, i), BitConverter.ToUInt32(buffer, i + 4));
CopyUInt32IntoArray(buffer, l, i);
CopyUInt32IntoArray(buffer, r, i + 4);
}
return buffer;
}
public void Decrypt(ref byte[] data)
{
for (int i = 0; i < data.Length; i += 8)
{
var (l, r) = Decrypt(BitConverter.ToUInt32(data, i), BitConverter.ToUInt32(data, i + 4));
CopyUInt32IntoArray(data, l, i);
CopyUInt32IntoArray(data, r, i + 4);
}
}
private static void CopyUInt32IntoArray(byte[] dest, uint val, int offset)
{
dest[offset] = (byte)(val & 0xFF);
dest[offset + 1] = (byte)((val >> 8) & 0xFF);
dest[offset + 2] = (byte)((val >> 16) & 0xFF);
dest[offset + 3] = (byte)((val >> 24) & 0xFF);
}
private uint F(uint i)
{
return ((this.s[0, i >> 24]
+ this.s[1, (i >> 16) & 0xFF])
^ this.s[2, (i >> 8) & 0xFF])
+ this.s[3, i & 0xFF];
}
private (uint, uint) Encrypt(uint l, uint r)
{
for (int i = 0; i < Rounds; i += 2)
{
l ^= this.p[i];
r ^= F(l);
r ^= this.p[i + 1];
l ^= F(r);
}
return (r ^ this.p[17], l ^ this.p[16]);
}
private (uint, uint) Decrypt(uint l, uint r)
{
for (int i = Rounds; i > 0; i -= 2)
{
l ^= this.p[i + 1];
r ^= F(l);
r ^= this.p[i];
l ^= F(r);
}
return (r ^ this.p[0], l ^ this.p[1]);
}
private static IEnumerable<TSource> Cycle<TSource>(IEnumerable<TSource> source)
{
while (true)
foreach (TSource t in source)
yield return t;
}
private IEnumerable<(int, uint)> WrappingUInt32(IEnumerable<byte> source, int count)
{
var enumerator = Cycle(source).GetEnumerator();
for (int i = 0; i < count; i++)
{
var n = 0u;
for (var j = 0; j < 4 && enumerator.MoveNext(); j++)
{
n = (uint)((n << 8) | (sbyte)enumerator.Current); // NOTE(goat): THIS IS A BUG! SE's implementation wrongly uses signed numbers for this, so we need to as well.
}
yield return (i, n);
}
}
}
}

View file

@ -661,8 +661,8 @@ namespace Dalamud.Injector
IntPtr hProcess,
IntPtr lpThreadAttributes,
UIntPtr dwStackSize,
UIntPtr lpStartAddress,
UIntPtr lpParameter,
nuint lpStartAddress,
nuint lpParameter,
CreateThreadFlags dwCreationFlags,
out uint lpThreadId);

View file

@ -34,6 +34,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FFXIVClientStructs", "lib\F
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FFXIVClientStructs.Generators", "lib\FFXIVClientStructs\FFXIVClientStructs.Generators\FFXIVClientStructs.Generators.csproj", "{05AB2F46-268B-4915-806F-DDF813E2D59D}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "DalamudCrashHandler", "DalamudCrashHandler\DalamudCrashHandler.vcxproj", "{317A264C-920B-44A1-8A34-F3A6827B0705}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -186,6 +188,18 @@ Global
{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
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View file

@ -332,6 +332,11 @@ namespace Dalamud.Configuration.Internal
/// </summary>
public bool ShowDevBarInfo { get; set; } = true;
/// <summary>
/// Gets or sets the last-used contact details for the plugin feedback form.
/// </summary>
public string LastFeedbackContactDetails { get; set; } = string.Empty;
/// <summary>
/// Load a configuration from the provided path.
/// </summary>

View file

@ -28,7 +28,6 @@ namespace Dalamud
#region Internals
private readonly ManualResetEvent unloadSignal;
private bool hasDisposedPlugins = false;
#endregion
@ -117,8 +116,6 @@ namespace Dalamud
/// </summary>
public void DisposePlugins()
{
this.hasDisposedPlugins = true;
// this must be done before unloading interface manager, in order to do rebuild
// the correct cascaded WndProc (IME -> RawDX11Scene -> Game). Otherwise the game
// will not receive any windows messages

View file

@ -8,7 +8,7 @@
</PropertyGroup>
<PropertyGroup Label="Feature">
<DalamudVersion>6.4.0.36</DalamudVersion>
<DalamudVersion>6.4.0.42</DalamudVersion>
<Description>XIV Launcher addon framework</Description>
<AssemblyVersion>$(DalamudVersion)</AssemblyVersion>
<Version>$(DalamudVersion)</Version>
@ -74,7 +74,7 @@
<PackageReference Include="Serilog.Sinks.Async" Version="1.5.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="4.0.1" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageReference Include="Reloaded.Hooks" Version="4.0.1" />
<PackageReference Include="goaaats.Reloaded.Hooks" Version="4.2.0-goat.2" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.333">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View file

@ -10,7 +10,6 @@ using Dalamud.Configuration.Internal;
using Dalamud.Logging.Internal;
using Dalamud.Support;
using Dalamud.Utility;
using ImGuiNET;
using Newtonsoft.Json;
using PInvoke;
using Serilog;
@ -41,10 +40,8 @@ namespace Dalamud
/// <summary>
/// A delegate used from VEH handler on exception which CoreCLR will fast fail by default.
/// </summary>
/// <param name="dumpPath">Path to minidump file created in UTF-16.</param>
/// <param name="logPath">Path to log file to create in UTF-16.</param>
/// <param name="log">Log text in UTF-16.</param>
public delegate void VehDelegate(IntPtr dumpPath, IntPtr logPath, IntPtr log);
/// <returns>HGLOBAL for message.</returns>
public delegate IntPtr VehDelegate();
/// <summary>
/// Initialize Dalamud.
@ -63,54 +60,19 @@ namespace Dalamud
}
/// <summary>
/// Show error message along with stack trace and exit.
/// Returns stack trace.
/// </summary>
/// <param name="dumpPath">Path to minidump file created in UTF-16.</param>
/// <param name="logPath">Path to log file to create in UTF-16.</param>
/// <param name="log">Log text in UTF-16.</param>
public static void VehCallback(IntPtr dumpPath, IntPtr logPath, IntPtr log)
/// <returns>HGlobal to wchar_t* stack trace c-string.</returns>
public static IntPtr VehCallback()
{
string stackTrace;
try
{
stackTrace = Environment.StackTrace;
return Marshal.StringToHGlobalUni(Environment.StackTrace);
}
catch (Exception e)
{
stackTrace = "Fail: " + e.ToString();
return Marshal.StringToHGlobalUni("Fail: " + e);
}
var msg = "An error within the game has occurred.\n\n"
+ "This may be caused by a faulty plugin, a broken TexTools modification, any other third-party tool or simply a bug in the game.\n"
+ "Please try \"Start Over\" or \"Download Index Backup\" in TexTools, an integrity check in the XIVLauncher settings, and disabling plugins you don't need.\n\n"
+ "The log file is located at:\n"
+ "{1}\n\n"
+ "Press OK to exit the application.\n\nStack trace:\n{2}";
try
{
File.WriteAllText(
Marshal.PtrToStringUni(logPath),
"Stack trace:\n" + stackTrace + "\n\n" + Marshal.PtrToStringUni(log));
}
catch (Exception e)
{
msg += "\n\nAdditionally, failed to write file: " + e.ToString();
}
// Show in another thread to prevent messagebox from pumping messages of current thread.
var msgThread = new Thread(() =>
{
Utility.Util.Fatal(
msg.Format(
Marshal.PtrToStringUni(dumpPath),
Marshal.PtrToStringUni(logPath),
stackTrace),
"Dalamud Error",
false);
});
msgThread.Start();
msgThread.Join();
}
/// <summary>

View file

@ -292,7 +292,7 @@ namespace Dalamud.Game
this.hasAutoUpdatedPlugins = true;
Task.Run(() => pluginManager.UpdatePluginsAsync(!this.configuration.AutoUpdatePlugins)).ContinueWith(task =>
Task.Run(() => pluginManager.UpdatePluginsAsync(true, !this.configuration.AutoUpdatePlugins)).ContinueWith(task =>
{
if (task.IsFaulted)
{
@ -301,11 +301,11 @@ namespace Dalamud.Game
}
var updatedPlugins = task.Result;
if (updatedPlugins != null && updatedPlugins.Any())
if (updatedPlugins.Any())
{
if (this.configuration.AutoUpdatePlugins)
{
PluginManager.PrintUpdatedPlugins(updatedPlugins, Loc.Localize("DalamudPluginAutoUpdate", "Auto-update:"));
Service<PluginManager>.Get().PrintUpdatedPlugins(updatedPlugins, Loc.Localize("DalamudPluginAutoUpdate", "Auto-update:"));
notifications.AddNotification(Loc.Localize("NotificationUpdatedPlugins", "{0} of your plugins were updated.").Format(updatedPlugins.Count), Loc.Localize("NotificationAutoUpdate", "Auto-Update"), NotificationType.Info);
}
else

View file

@ -326,7 +326,9 @@ namespace Dalamud.Game
if (insnByte == 0xE8 || insnByte == 0xE9)
scanRet = ReadJmpCallSig(scanRet);
if (this.textCache != null)
// If this is below the module, there's bound to be a problem with the sig/resolution... Let's not save it
// TODO: THIS IS A HACK! FIX THE ROOT CAUSE!
if (this.textCache != null && scanRet.ToInt64() >= this.Module.BaseAddress.ToInt64())
{
this.textCache[signature] = scanRet.ToInt64() - this.Module.BaseAddress.ToInt64();
}
@ -377,7 +379,7 @@ namespace Dalamud.Game
}
catch (Exception e)
{
Log.Warning(e, "Failed to save cache to {0}", this.cacheFile);
Log.Warning(e, "Failed to save cache to {CachePath}", this.cacheFile);
}
}
@ -532,7 +534,17 @@ namespace Dalamud.Game
return;
}
this.textCache = JsonConvert.DeserializeObject<ConcurrentDictionary<string, long>>(File.ReadAllText(this.cacheFile.FullName)) ?? new ConcurrentDictionary<string, long>();
try
{
this.textCache =
JsonConvert.DeserializeObject<ConcurrentDictionary<string, long>>(
File.ReadAllText(this.cacheFile.FullName)) ?? new ConcurrentDictionary<string, long>();
}
catch (Exception ex)
{
this.textCache = new ConcurrentDictionary<string, long>();
Log.Error(ex, "Couldn't load cached sigs");
}
}
}
}

View file

@ -3,6 +3,7 @@ using System.Diagnostics;
using System.Reflection;
using System.Runtime.InteropServices;
using Dalamud.Configuration.Internal;
using Dalamud.Hooking.Internal;
namespace Dalamud.Hooking
@ -59,6 +60,9 @@ namespace Dalamud.Hooking
[Obsolete("Use Hook<T>.FromAddress instead.")]
private Hook(IntPtr address, T detour, bool useMinHook, Assembly callingAssembly)
{
if (EnvironmentConfiguration.DalamudForceMinHook)
useMinHook = true;
address = HookManager.FollowJmp(address);
if (useMinHook)
this.compatHookImpl = new MinHookHook<T>(address, detour, callingAssembly);
@ -214,6 +218,9 @@ namespace Dalamud.Hooking
/// <returns>The hook with the supplied parameters.</returns>
public static Hook<T> FromSymbol(string moduleName, string exportName, T detour, bool useMinHook)
{
if (EnvironmentConfiguration.DalamudForceMinHook)
useMinHook = true;
var moduleHandle = NativeFunctions.GetModuleHandleW(moduleName);
if (moduleHandle == IntPtr.Zero)
throw new Exception($"Could not get a handle to module {moduleName}");
@ -240,6 +247,9 @@ namespace Dalamud.Hooking
/// <returns>The hook with the supplied parameters.</returns>
public static Hook<T> FromAddress(IntPtr procAddress, T detour, bool useMinHook = false)
{
if (EnvironmentConfiguration.DalamudForceMinHook)
useMinHook = true;
procAddress = HookManager.FollowJmp(procAddress);
if (useMinHook)
return new MinHookHook<T>(procAddress, detour, Assembly.GetCallingAssembly());

View file

@ -68,6 +68,9 @@ namespace Dalamud.Hooking.Internal
return address;
}
if (address.ToInt64() <= 0)
throw new InvalidOperationException($"Address was <= 0, this can't be happening?! ({address:X})");
var bytes = MemoryHelper.ReadRaw(address, 8);
var codeReader = new ByteArrayCodeReader(bytes);

View file

@ -29,6 +29,8 @@ namespace Dalamud.Hooking.Internal
}
this.hookImpl = ReloadedHooks.Instance.CreateHook<T>(detour, address.ToInt64());
this.hookImpl.Activate();
this.hookImpl.Disable();
HookManager.TrackedHooks.TryAdd(Guid.NewGuid(), new HookInfo(this, detour, callingAssembly));
}
@ -75,9 +77,6 @@ namespace Dalamud.Hooking.Internal
lock (HookManager.HookEnableSyncRoot)
{
if (!this.hookImpl.IsHookActivated)
this.hookImpl.Activate();
if (!this.hookImpl.IsHookEnabled)
this.hookImpl.Enable();
}

View file

@ -0,0 +1,71 @@
using System.Numerics;
using ImGuiNET;
namespace Dalamud.Interface.Components
{
/// <summary>
/// Component for toggle buttons.
/// </summary>
public static partial class ImGuiComponents
{
/// <summary>
/// Draw a toggle button.
/// </summary>
/// <param name="id">The id of the button.</param>
/// <param name="v">The state of the switch.</param>
/// <returns>If the button has been interacted with this frame.</returns>
public static bool ToggleButton(string id, ref bool v)
{
var colors = ImGui.GetStyle().Colors;
var p = ImGui.GetCursorScreenPos();
var drawList = ImGui.GetWindowDrawList();
var height = ImGui.GetFrameHeight();
var width = height * 1.55f;
var radius = height * 0.50f;
// TODO: animate
var changed = false;
ImGui.InvisibleButton(id, new Vector2(width, height));
if (ImGui.IsItemClicked())
{
v = !v;
changed = true;
}
if (ImGui.IsItemHovered())
drawList.AddRectFilled(p, new Vector2(p.X + width, p.Y + height), ImGui.GetColorU32(!v ? colors[(int)ImGuiCol.ButtonActive] : new Vector4(0.78f, 0.78f, 0.78f, 1.0f)), height * 0.5f);
else
drawList.AddRectFilled(p, new Vector2(p.X + width, p.Y + height), ImGui.GetColorU32(!v ? colors[(int)ImGuiCol.Button] * 0.6f : new Vector4(0.35f, 0.35f, 0.35f, 1.0f)), height * 0.50f);
drawList.AddCircleFilled(new Vector2(p.X + radius + ((v ? 1 : 0) * (width - (radius * 2.0f))), p.Y + radius), radius - 1.5f, ImGui.ColorConvertFloat4ToU32(new Vector4(1, 1, 1, 1)));
return changed;
}
/// <summary>
/// Draw a disabled toggle button.
/// </summary>
/// <param name="id">The id of the button.</param>
/// <param name="v">The state of the switch.</param>
public static void DisabledToggleButton(string id, bool v)
{
var colors = ImGui.GetStyle().Colors;
var p = ImGui.GetCursorScreenPos();
var drawList = ImGui.GetWindowDrawList();
var height = ImGui.GetFrameHeight();
var width = height * 1.55f;
var radius = height * 0.50f;
// TODO: animate
ImGui.InvisibleButton(id, new Vector2(width, height));
var dimFactor = 0.5f;
drawList.AddRectFilled(p, new Vector2(p.X + width, p.Y + height), ImGui.GetColorU32(v ? colors[(int)ImGuiCol.Button] * dimFactor : new Vector4(0.55f, 0.55f, 0.55f, 1.0f) * dimFactor), height * 0.50f);
drawList.AddCircleFilled(new Vector2(p.X + radius + ((v ? 1 : 0) * (width - (radius * 2.0f))), p.Y + radius), radius - 1.5f, ImGui.ColorConvertFloat4ToU32(new Vector4(1, 1, 1, 1) * dimFactor));
}
}
}

View file

@ -6,6 +6,7 @@ using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using Dalamud.Data;
using Dalamud.Game;
using Dalamud.Interface.Internal;
using Dalamud.Utility.Timing;
using ImGuiNET;
@ -184,16 +185,9 @@ namespace Dalamud.Interface.GameFonts
needRebuild = !this.fonts.ContainsKey(style);
if (needRebuild)
{
if (interfaceManager.IsBuildingFontsBeforeAtlasBuild && this.isBetweenBuildFontsAndRightAfterImGuiIoFontsBuild)
{
Log.Information("[GameFontManager] NewFontRef: Building {0} right now, as it is called while BuildFonts is already in progress yet atlas build has not been called yet.", style.ToString());
this.EnsureFont(style);
}
else
{
Log.Information("[GameFontManager] NewFontRef: Calling RebuildFonts because {0} has been requested.", style.ToString());
interfaceManager.RebuildFonts();
}
Log.Information("[GameFontManager] NewFontRef: Queueing RebuildFonts because {0} has been requested.", style.ToString());
Service<Framework>.GetAsync()
.ContinueWith(task => task.Result.RunOnTick(() => interfaceManager.RebuildFonts()));
}
return new(this, style);

View file

@ -4,7 +4,9 @@ using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using Dalamud.Game.ClientState.Keys;
using ImGuiNET;
using ImGuiScene;
namespace Dalamud.Interface
{
@ -222,6 +224,56 @@ namespace Dalamud.Interface
target.Value!.BuildLookupTable();
}
/// <summary>
/// Map a VirtualKey keycode to an ImGuiKey enum value.
/// </summary>
/// <param name="key">The VirtualKey value to retrieve the ImGuiKey counterpart for.</param>
/// <returns>The ImGuiKey that corresponds to this VirtualKey, or <c>ImGuiKey.None</c> otherwise.</returns>
public static ImGuiKey VirtualKeyToImGuiKey(VirtualKey key)
{
return ImGui_Input_Impl_Direct.VirtualKeyToImGuiKey((int)key);
}
/// <summary>
/// Map an ImGuiKey enum value to a VirtualKey code.
/// </summary>
/// <param name="key">The ImGuiKey value to retrieve the VirtualKey counterpart for.</param>
/// <returns>The VirtualKey that corresponds to this ImGuiKey, or <c>VirtualKey.NO_KEY</c> otherwise.</returns>
public static VirtualKey ImGuiKeyToVirtualKey(ImGuiKey key)
{
return (VirtualKey)ImGui_Input_Impl_Direct.ImGuiKeyToVirtualKey(key);
}
/// <summary>
/// Show centered text.
/// </summary>
/// <param name="text">Text to show.</param>
public static void CenteredText(string text)
{
CenterCursorForText(text);
ImGui.TextUnformatted(text);
}
/// <summary>
/// Center the ImGui cursor for a certain text.
/// </summary>
/// <param name="text">The text to center for.</param>
public static void CenterCursorForText(string text)
{
var textWidth = ImGui.CalcTextSize(text).X;
CenterCursorFor((int)textWidth);
}
/// <summary>
/// Center the ImGui cursor for an item with a certain width.
/// </summary>
/// <param name="itemWidth">The width to center for.</param>
public static void CenterCursorFor(int itemWidth)
{
var window = (int)ImGui.GetWindowWidth();
ImGui.SetCursorPosX((window / 2) - (itemWidth / 2));
}
/// <summary>
/// Get data needed for each new frame.
/// </summary>

View file

@ -72,6 +72,18 @@ namespace Dalamud.Interface.Internal
ShowInHelp = false,
});
commandManager.AddHandler("/xlstats", new CommandInfo(this.OnTogglePluginStats)
{
HelpMessage = Loc.Localize("DalamudPluginStats", "Draw plugin statistics window"),
ShowInHelp = false,
});
commandManager.AddHandler("/xlbranch", new CommandInfo(this.OnToggleBranchSwitcher)
{
HelpMessage = Loc.Localize("DalamudBranchSwitcher", "Draw branch switcher"),
ShowInHelp = false,
});
commandManager.AddHandler("/xldata", new CommandInfo(this.OnDebugDrawDataMenu)
{
HelpMessage = Loc.Localize("DalamudDevDataMenuHelp", "Draw dev data menu DEBUG. Usage: /xldata [Data Dropdown Type]"),
@ -267,6 +279,16 @@ namespace Dalamud.Interface.Internal
Service<DalamudInterface>.Get().ToggleDevMenu();
}
private void OnTogglePluginStats(string command, string arguments)
{
Service<DalamudInterface>.Get().TogglePluginStatsWindow();
}
private void OnToggleBranchSwitcher(string command, string arguments)
{
Service<DalamudInterface>.Get().ToggleBranchSwitcher();
}
private void OnDebugDrawDataMenu(string command, string arguments)
{
var dalamudInterface = Service<DalamudInterface>.Get();

View file

@ -12,6 +12,7 @@ using Dalamud.Configuration.Internal;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.Gui;
using Dalamud.Game.Internal;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Internal.ManagedAsserts;
using Dalamud.Interface.Internal.Windows;
using Dalamud.Interface.Internal.Windows.PluginInstaller;
@ -249,6 +250,11 @@ namespace Dalamud.Interface.Internal
/// </summary>
public void OpenPluginInstaller() => this.pluginWindow.IsOpen = true;
/// <summary>
/// Opens the <see cref="PluginInstallerWindow"/> on the plugin changelogs.
/// </summary>
public void OpenPluginInstallerPluginChangelogs() => this.pluginWindow.OpenPluginChangelogs();
/// <summary>
/// Opens the <see cref="SettingsWindow"/>.
/// </summary>
@ -369,12 +375,17 @@ namespace Dalamud.Interface.Internal
/// Toggles the <see cref="StyleEditorWindow"/>.
/// </summary>
public void ToggleStyleEditorWindow() => this.selfTestWindow.Toggle();
/// <summary>
/// Toggles the <see cref="ProfilerWindow"/>.
/// </summary>
public void ToggleProfilerWindow() => this.profilerWindow.Toggle();
/// <summary>
/// Toggles the <see cref="BranchSwitcherWindow"/>.
/// </summary>
public void ToggleBranchSwitcher() => this.branchSwitcherWindow.Toggle();
#endregion
private void OnDraw()
@ -438,12 +449,9 @@ namespace Dalamud.Interface.Internal
if (!this.isImGuiDrawDevMenu && !condition.Any())
{
var config = Service<DalamudConfiguration>.Get();
ImGui.PushStyleColor(ImGuiCol.Button, Vector4.Zero);
ImGui.PushStyleColor(ImGuiCol.ButtonActive, Vector4.Zero);
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, Vector4.Zero);
ImGui.PushStyleColor(ImGuiCol.Text, new Vector4(0, 0, 0, 1));
ImGui.PushStyleColor(ImGuiCol.TextSelectedBg, new Vector4(0, 0, 0, 1));
ImGui.PushStyleColor(ImGuiCol.Border, new Vector4(0, 0, 0, 1));
ImGui.PushStyleColor(ImGuiCol.BorderShadow, new Vector4(0, 0, 0, 1));
@ -467,8 +475,26 @@ namespace Dalamud.Interface.Internal
ImGui.End();
}
if (EnvironmentConfiguration.DalamudForceMinHook)
{
ImGui.SetNextWindowPos(windowPos, ImGuiCond.Always);
ImGui.SetNextWindowBgAlpha(1);
if (ImGui.Begin(
"Disclaimer",
ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoBackground |
ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.NoMove |
ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoMouseInputs |
ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoSavedSettings))
{
ImGui.TextColored(ImGuiColors.DalamudRed, "Is force MinHook!");
}
ImGui.End();
}
ImGui.PopStyleVar(4);
ImGui.PopStyleColor(8);
ImGui.PopStyleColor(7);
}
}
@ -594,6 +620,16 @@ namespace Dalamud.Interface.Internal
Service<Dalamud>.Get().Unload();
}
if (ImGui.MenuItem("Restart game"))
{
[DllImport("kernel32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
static extern void RaiseException(uint dwExceptionCode, uint dwExceptionFlags, uint nNumberOfArguments, IntPtr lpArguments);
RaiseException(0x12345678, 0, 0, IntPtr.Zero);
Process.GetCurrentProcess().Kill();
}
if (ImGui.MenuItem("Kill game"))
{
Process.GetCurrentProcess().Kill();

View file

@ -1030,22 +1030,43 @@ namespace Dalamud.Interface.Internal
if (gamepadEnabled && (ImGui.GetIO().ConfigFlags & ImGuiConfigFlags.NavEnableGamepad) > 0)
{
ImGui.GetIO().NavInputs[(int)ImGuiNavInput.Activate] = gamepadState.Raw(GamepadButtons.South);
ImGui.GetIO().NavInputs[(int)ImGuiNavInput.Cancel] = gamepadState.Raw(GamepadButtons.East);
ImGui.GetIO().NavInputs[(int)ImGuiNavInput.Input] = gamepadState.Raw(GamepadButtons.North);
ImGui.GetIO().NavInputs[(int)ImGuiNavInput.Menu] = gamepadState.Raw(GamepadButtons.West);
ImGui.GetIO().NavInputs[(int)ImGuiNavInput.DpadLeft] = gamepadState.Raw(GamepadButtons.DpadLeft);
ImGui.GetIO().NavInputs[(int)ImGuiNavInput.DpadRight] = gamepadState.Raw(GamepadButtons.DpadRight);
ImGui.GetIO().NavInputs[(int)ImGuiNavInput.DpadUp] = gamepadState.Raw(GamepadButtons.DpadUp);
ImGui.GetIO().NavInputs[(int)ImGuiNavInput.DpadDown] = gamepadState.Raw(GamepadButtons.DpadDown);
ImGui.GetIO().NavInputs[(int)ImGuiNavInput.LStickLeft] = gamepadState.LeftStickLeft;
ImGui.GetIO().NavInputs[(int)ImGuiNavInput.LStickRight] = gamepadState.LeftStickRight;
ImGui.GetIO().NavInputs[(int)ImGuiNavInput.LStickUp] = gamepadState.LeftStickUp;
ImGui.GetIO().NavInputs[(int)ImGuiNavInput.LStickDown] = gamepadState.LeftStickDown;
ImGui.GetIO().NavInputs[(int)ImGuiNavInput.FocusPrev] = gamepadState.Raw(GamepadButtons.L1);
ImGui.GetIO().NavInputs[(int)ImGuiNavInput.FocusNext] = gamepadState.Raw(GamepadButtons.R1);
ImGui.GetIO().NavInputs[(int)ImGuiNavInput.TweakSlow] = gamepadState.Raw(GamepadButtons.L2);
ImGui.GetIO().NavInputs[(int)ImGuiNavInput.TweakFast] = gamepadState.Raw(GamepadButtons.R2);
var northButton = gamepadState.Raw(GamepadButtons.North) != 0;
var eastButton = gamepadState.Raw(GamepadButtons.East) != 0;
var southButton = gamepadState.Raw(GamepadButtons.South) != 0;
var westButton = gamepadState.Raw(GamepadButtons.West) != 0;
var dPadUp = gamepadState.Raw(GamepadButtons.DpadUp) != 0;
var dPadRight = gamepadState.Raw(GamepadButtons.DpadRight) != 0;
var dPadDown = gamepadState.Raw(GamepadButtons.DpadDown) != 0;
var dPadLeft = gamepadState.Raw(GamepadButtons.DpadLeft) != 0;
var leftStickUp = gamepadState.LeftStickUp;
var leftStickRight = gamepadState.LeftStickRight;
var leftStickDown = gamepadState.LeftStickDown;
var leftStickLeft = gamepadState.LeftStickLeft;
var l1Button = gamepadState.Raw(GamepadButtons.L1) != 0;
var l2Button = gamepadState.Raw(GamepadButtons.L2) != 0;
var r1Button = gamepadState.Raw(GamepadButtons.R1) != 0;
var r2Button = gamepadState.Raw(GamepadButtons.R2) != 0;
var io = ImGui.GetIO();
io.AddKeyEvent(ImGuiKey.GamepadFaceUp, northButton);
io.AddKeyEvent(ImGuiKey.GamepadFaceRight, eastButton);
io.AddKeyEvent(ImGuiKey.GamepadFaceDown, southButton);
io.AddKeyEvent(ImGuiKey.GamepadFaceLeft, westButton);
io.AddKeyEvent(ImGuiKey.GamepadDpadUp, dPadUp);
io.AddKeyEvent(ImGuiKey.GamepadDpadRight, dPadRight);
io.AddKeyEvent(ImGuiKey.GamepadDpadDown, dPadDown);
io.AddKeyEvent(ImGuiKey.GamepadDpadLeft, dPadLeft);
io.AddKeyAnalogEvent(ImGuiKey.GamepadLStickUp, leftStickUp != 0, leftStickUp);
io.AddKeyAnalogEvent(ImGuiKey.GamepadLStickRight, leftStickRight != 0, leftStickRight);
io.AddKeyAnalogEvent(ImGuiKey.GamepadLStickDown, leftStickDown != 0, leftStickDown);
io.AddKeyAnalogEvent(ImGuiKey.GamepadLStickLeft, leftStickLeft != 0, leftStickLeft);
io.AddKeyEvent(ImGuiKey.GamepadL1, l1Button);
io.AddKeyEvent(ImGuiKey.GamepadL2, l2Button);
io.AddKeyEvent(ImGuiKey.GamepadR1, r1Button);
io.AddKeyEvent(ImGuiKey.GamepadR2, r2Button);
if (gamepadState.Pressed(GamepadButtons.R3) > 0)
{

View file

@ -43,6 +43,7 @@ namespace Dalamud.Interface.Internal.Windows
public const int PluginIconHeight = 512;
private const string MainRepoImageUrl = "https://raw.githubusercontent.com/goatcorp/DalamudPlugins/api6/{0}/{1}/images/{2}";
private const string MainRepoDip17ImageUrl = "https://raw.githubusercontent.com/goatcorp/PluginDistD17/main/{0}/{1}/images/{2}";
private readonly BlockingCollection<Tuple<ulong, Func<Task>>> downloadQueue = new();
private readonly BlockingCollection<Func<Task>> loadQueue = new();
@ -54,6 +55,7 @@ namespace Dalamud.Interface.Internal.Windows
private readonly ConcurrentDictionary<string, TextureWrap?[]?> pluginImagesMap = new();
private readonly Task<TextureWrap> emptyTextureTask;
private readonly Task<TextureWrap> disabledIconTask;
private readonly Task<TextureWrap> defaultIconTask;
private readonly Task<TextureWrap> troubleIconTask;
private readonly Task<TextureWrap> updateIconTask;
@ -71,6 +73,7 @@ namespace Dalamud.Interface.Internal.Windows
this.emptyTextureTask = imwst.ContinueWith(task => task.Result.Manager.LoadImageRaw(new byte[64], 8, 8, 4)!);
this.defaultIconTask = imwst.ContinueWith(task => TaskWrapIfNonNull(task.Result.Manager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "defaultIcon.png"))) ?? this.emptyTextureTask).Unwrap();
this.disabledIconTask = imwst.ContinueWith(task => TaskWrapIfNonNull(task.Result.Manager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "disabledIcon.png"))) ?? this.emptyTextureTask).Unwrap();
this.troubleIconTask = imwst.ContinueWith(task => TaskWrapIfNonNull(task.Result.Manager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "troubleIcon.png"))) ?? this.emptyTextureTask).Unwrap();
this.updateIconTask = imwst.ContinueWith(task => TaskWrapIfNonNull(task.Result.Manager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "updateIcon.png"))) ?? this.emptyTextureTask).Unwrap();
this.installedIconTask = imwst.ContinueWith(task => TaskWrapIfNonNull(task.Result.Manager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "installedIcon.png"))) ?? this.emptyTextureTask).Unwrap();
@ -91,6 +94,13 @@ namespace Dalamud.Interface.Internal.Windows
? this.emptyTextureTask.Result
: this.emptyTextureTask.GetAwaiter().GetResult();
/// <summary>
/// Gets the default plugin icon.
/// </summary>
public TextureWrap DisabledIcon => this.disabledIconTask.IsCompleted
? this.disabledIconTask.Result
: this.disabledIconTask.GetAwaiter().GetResult();
/// <summary>
/// Gets the default plugin icon.
/// </summary>
@ -644,6 +654,9 @@ namespace Dalamud.Interface.Internal.Windows
if (isThirdParty)
return manifest.IconUrl;
if (manifest.IsDip17Plugin)
return MainRepoDip17ImageUrl.Format(manifest.Dip17Channel!, manifest.InternalName, "icon.png");
return MainRepoImageUrl.Format(isTesting ? "testing" : "plugins", manifest.InternalName, "icon.png");
}
@ -663,7 +676,14 @@ namespace Dalamud.Interface.Internal.Windows
var output = new List<string>();
for (var i = 1; i <= 5; i++)
{
output.Add(MainRepoImageUrl.Format(isTesting ? "testing" : "plugins", manifest.InternalName, $"image{i}.png"));
if (manifest.IsDip17Plugin)
{
output.Add(MainRepoDip17ImageUrl.Format(manifest.Dip17Channel!, manifest.InternalName, $"image{i}.png"));
}
else
{
output.Add(MainRepoImageUrl.Format(isTesting ? "testing" : "plugins", manifest.InternalName, $"image{i}.png"));
}
}
return output;

View file

@ -21,10 +21,7 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
if (plugin.Manifest.Changelog.IsNullOrEmpty())
throw new ArgumentException("Manifest has no changelog.");
var version = plugin.AssemblyName?.Version;
version ??= plugin.Manifest.Testing
? plugin.Manifest.TestingAssemblyVersion
: plugin.Manifest.AssemblyVersion;
var version = plugin.Manifest.EffectiveVersion;
this.Version = version!.ToString();
}

View file

@ -14,6 +14,7 @@ using Dalamud.Game.Command;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Components;
using Dalamud.Interface.Internal.Notifications;
using Dalamud.Interface.Style;
using Dalamud.Interface.Windowing;
using Dalamud.Logging.Internal;
using Dalamud.Plugin;
@ -59,14 +60,17 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
private bool errorModalDrawing = true;
private bool errorModalOnNextFrame = false;
private string errorModalMessage = string.Empty;
private TaskCompletionSource? errorModalTaskCompletionSource;
private bool feedbackModalDrawing = true;
private bool feedbackModalOnNextFrame = false;
private bool feedbackModalOnNextFrameDontClear = false;
private string feedbackModalBody = string.Empty;
private string feedbackModalContact = string.Empty;
private bool feedbackModalIncludeException = false;
private PluginManifest? feedbackPlugin = null;
private bool feedbackIsTesting = false;
private bool feedbackIsAnonymous = false;
private int updatePluginCount = 0;
private List<PluginUpdateStatus>? updatedPlugins;
@ -83,6 +87,9 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
private OperationStatus installStatus = OperationStatus.Idle;
private OperationStatus updateStatus = OperationStatus.Idle;
private OperationStatus enableDisableStatus = OperationStatus.Idle;
private LoadingIndicatorKind loadingIndicatorKind = LoadingIndicatorKind.Unknown;
/// <summary>
/// Initializes a new instance of the <see cref="PluginInstallerWindow"/> class.
@ -130,6 +137,17 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
Complete,
}
private enum LoadingIndicatorKind
{
Unknown,
EnablingSingle,
DisablingSingle,
UpdatingSingle,
UpdatingAll,
Installing,
Manager,
}
private enum PluginSortKind
{
Alphabetical,
@ -139,6 +157,10 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
NotInstalled,
}
private bool AnyOperationInProgress => this.installStatus == OperationStatus.InProgress ||
this.updateStatus == OperationStatus.InProgress ||
this.enableDisableStatus == OperationStatus.InProgress;
/// <inheritdoc/>
public void Dispose()
{
@ -184,6 +206,7 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
this.DrawFooter();
this.DrawErrorModal();
this.DrawFeedbackModal();
this.DrawProgressOverlay();
}
/// <summary>
@ -194,6 +217,119 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
this.imageCache.ClearIconCache();
}
/// <summary>
/// Open the window on the plugin changelogs.
/// </summary>
public void OpenPluginChangelogs()
{
this.categoryManager.CurrentGroupIdx = 3;
this.categoryManager.CurrentCategoryIdx = 2;
this.IsOpen = true;
}
private void DrawProgressOverlay()
{
var pluginManager = Service<PluginManager>.Get();
var isWaitingManager = !pluginManager.PluginsReady ||
!pluginManager.ReposReady;
var isLoading = this.AnyOperationInProgress ||
isWaitingManager;
if (isWaitingManager)
this.loadingIndicatorKind = LoadingIndicatorKind.Manager;
if (!isLoading)
return;
ImGui.SetCursorPos(Vector2.Zero);
var windowSize = ImGui.GetWindowSize();
var titleHeight = ImGui.GetFontSize() + (ImGui.GetStyle().FramePadding.Y * 2);
if (ImGui.BeginChild("###installerLoadingFrame", new Vector2(-1, -1), false))
{
ImGui.GetWindowDrawList().PushClipRectFullScreen();
ImGui.GetWindowDrawList().AddRectFilled(
ImGui.GetWindowPos() + new Vector2(0, titleHeight),
ImGui.GetWindowPos() + windowSize,
0xCC000000,
ImGui.GetStyle().WindowRounding,
ImDrawFlags.RoundCornersBottom);
ImGui.PopClipRect();
ImGui.SetCursorPosY(windowSize.Y / 2);
switch (this.loadingIndicatorKind)
{
case LoadingIndicatorKind.Unknown:
ImGuiHelpers.CenteredText("Doing something, not sure what!");
break;
case LoadingIndicatorKind.EnablingSingle:
ImGuiHelpers.CenteredText("Enabling plugin...");
break;
case LoadingIndicatorKind.DisablingSingle:
ImGuiHelpers.CenteredText("Disabling plugin...");
break;
case LoadingIndicatorKind.UpdatingSingle:
ImGuiHelpers.CenteredText("Updating plugin...");
break;
case LoadingIndicatorKind.UpdatingAll:
ImGuiHelpers.CenteredText("Updating plugins...");
break;
case LoadingIndicatorKind.Installing:
ImGuiHelpers.CenteredText("Installing plugin...");
break;
case LoadingIndicatorKind.Manager:
{
if (pluginManager.PluginsReady && !pluginManager.ReposReady)
{
ImGuiHelpers.CenteredText("Loading repositories...");
}
else if (!pluginManager.PluginsReady && pluginManager.ReposReady)
{
ImGuiHelpers.CenteredText("Loading installed plugins...");
}
else
{
ImGuiHelpers.CenteredText("Loading repositories and plugins...");
}
var currentProgress = 0;
var total = 0;
var pendingRepos = pluginManager.Repos.ToArray()
.Where(x => (x.State != PluginRepositoryState.Success &&
x.State != PluginRepositoryState.Fail) &&
x.IsEnabled)
.ToArray();
var allRepoCount =
pluginManager.Repos.Count(x => x.State != PluginRepositoryState.Fail && x.IsEnabled);
foreach (var repo in pendingRepos)
{
ImGuiHelpers.CenteredText($"{repo.PluginMasterUrl}: {repo.State}");
}
currentProgress += allRepoCount - pendingRepos.Length;
total += allRepoCount;
if (currentProgress != total)
{
ImGui.SetCursorPosX(windowSize.X / 3);
ImGui.ProgressBar(currentProgress / (float)total, new Vector2(windowSize.X / 3, 50));
}
}
break;
default:
throw new ArgumentOutOfRangeException();
}
ImGui.EndChild();
}
}
private void DrawHeader()
{
var style = ImGui.GetStyle();
@ -201,8 +337,8 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
ImGui.SetCursorPosY(ImGui.GetCursorPosY() - (5 * ImGuiHelpers.GlobalScale));
var searchInputWidth = 240 * ImGuiHelpers.GlobalScale;
var searchClearButtonWidth = 40 * ImGuiHelpers.GlobalScale;
var searchInputWidth = 180 * ImGuiHelpers.GlobalScale;
var searchClearButtonWidth = 25 * ImGuiHelpers.GlobalScale;
var sortByText = Locs.SortBy_Label;
var sortByTextWidth = ImGui.CalcTextSize(sortByText).X;
@ -225,9 +361,10 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
ImGui.SameLine();
// Shift down a little to align with the middle of the header text
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (headerTextSize.Y / 4) - 2);
var downShift = ImGui.GetCursorPosY() + (headerTextSize.Y / 4) - 2;
ImGui.SetCursorPosY(downShift);
ImGui.SetCursorPosX(windowSize.X - sortSelectWidth - style.ItemSpacing.X - searchInputWidth - searchClearButtonWidth);
ImGui.SetCursorPosX(windowSize.X - sortSelectWidth - (style.ItemSpacing.X * 2) - searchInputWidth - searchClearButtonWidth);
var searchTextChanged = false;
ImGui.SetNextItemWidth(searchInputWidth);
@ -238,6 +375,7 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
100);
ImGui.SameLine();
ImGui.SetCursorPosY(downShift);
ImGui.SetNextItemWidth(searchClearButtonWidth);
if (ImGuiComponents.IconButton(FontAwesomeIcon.Times))
@ -250,7 +388,7 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
this.UpdateCategoriesOnSearchChange();
ImGui.SameLine();
ImGui.SetCursorPosX(windowSize.X - sortSelectWidth);
ImGui.SetCursorPosY(downShift);
ImGui.SetNextItemWidth(selectableWidth);
if (ImGui.BeginCombo(sortByText, this.filterText, ImGuiComboFlags.NoArrowButton))
{
@ -336,8 +474,9 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
if (ImGui.Button(Locs.FooterButton_UpdatePlugins))
{
this.updateStatus = OperationStatus.InProgress;
this.loadingIndicatorKind = LoadingIndicatorKind.UpdatingAll;
Task.Run(() => pluginManager.UpdatePluginsAsync())
Task.Run(() => pluginManager.UpdatePluginsAsync(true, false))
.ContinueWith(task =>
{
this.updateStatus = OperationStatus.Complete;
@ -372,7 +511,7 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
if (this.updatePluginCount > 0)
{
PluginManager.PrintUpdatedPlugins(this.updatedPlugins, Locs.PluginUpdateHeader_Chatbox);
Service<PluginManager>.Get().PrintUpdatedPlugins(this.updatedPlugins, Locs.PluginUpdateHeader_Chatbox);
notifications.AddNotification(Locs.Notifications_UpdatesInstalled(this.updatePluginCount), Locs.Notifications_UpdatesInstalledTitle, NotificationType.Success);
var installedGroupIdx = this.categoryManager.GroupList.TakeWhile(
@ -404,6 +543,7 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
if (ImGui.Button(Locs.ErrorModalButton_Ok, new Vector2(buttonWidth, 40)))
{
ImGui.CloseCurrentPopup();
errorModalTaskCompletionSource?.SetResult();
}
ImGui.EndPopup();
@ -446,7 +586,43 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
ImGui.Spacing();
ImGui.InputText(Locs.FeedbackModal_ContactInformation, ref this.feedbackModalContact, 100);
if (ImGui.Checkbox(Locs.FeedbackModal_ContactAnonymous, ref this.feedbackIsAnonymous))
{
if (this.feedbackIsAnonymous)
this.feedbackModalContact = string.Empty;
}
if (this.feedbackIsAnonymous)
{
ImGui.BeginDisabled();
ImGui.InputText(Locs.FeedbackModal_ContactInformation, ref this.feedbackModalContact, 0);
ImGui.EndDisabled();
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudRed);
ImGui.Text(Locs.FeedbackModal_ContactAnonymousWarning);
ImGui.PopStyleColor();
}
else
{
ImGui.InputText(Locs.FeedbackModal_ContactInformation, ref this.feedbackModalContact, 100);
ImGui.SameLine();
if (ImGui.Button(Locs.FeedbackModal_ContactInformationDiscordButton))
{
Process.Start(new ProcessStartInfo(Locs.FeedbackModal_ContactInformationDiscordUrl)
{
UseShellExecute = true,
});
}
ImGui.Text(Locs.FeedbackModal_ContactInformationHelp);
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudRed);
ImGui.Text(Locs.FeedbackModal_ContactInformationWarning);
ImGui.PopStyleColor();
}
ImGui.Spacing();
ImGui.Checkbox(Locs.FeedbackModal_IncludeLastError, ref this.feedbackModalIncludeException);
ImGui.TextColored(ImGuiColors.DalamudGrey, Locs.FeedbackModal_IncludeLastErrorHint);
@ -460,25 +636,57 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
if (ImGui.Button(Locs.ErrorModalButton_Ok, new Vector2(buttonWidth, 40)))
{
if (this.feedbackPlugin != null)
if (!this.feedbackIsAnonymous && string.IsNullOrWhiteSpace(this.feedbackModalContact))
{
Task.Run(async () => await BugBait.SendFeedback(this.feedbackPlugin, this.feedbackIsTesting, this.feedbackModalBody, this.feedbackModalContact, this.feedbackModalIncludeException))
.ContinueWith(
t =>
{
var notif = Service<NotificationManager>.Get();
if (t.IsCanceled || t.IsFaulted)
notif.AddNotification(Locs.FeedbackModal_NotificationError, Locs.FeedbackModal_Title, NotificationType.Error);
else
notif.AddNotification(Locs.FeedbackModal_NotificationSuccess, Locs.FeedbackModal_Title, NotificationType.Success);
});
this.ShowErrorModal(Locs.FeedbackModal_ContactInformationRequired)
.ContinueWith(_ =>
{
this.feedbackModalOnNextFrameDontClear = true;
this.feedbackModalOnNextFrame = true;
});
}
else
{
Log.Error("FeedbackPlugin was null.");
}
if (this.feedbackPlugin != null)
{
Task.Run(async () => await BugBait.SendFeedback(
this.feedbackPlugin,
this.feedbackIsTesting,
this.feedbackModalBody,
this.feedbackModalContact,
this.feedbackModalIncludeException))
.ContinueWith(
t =>
{
var notif = Service<NotificationManager>.Get();
if (t.IsCanceled || t.IsFaulted)
{
notif.AddNotification(
Locs.FeedbackModal_NotificationError,
Locs.FeedbackModal_Title,
NotificationType.Error);
}
else
{
notif.AddNotification(
Locs.FeedbackModal_NotificationSuccess,
Locs.FeedbackModal_Title,
NotificationType.Success);
}
});
}
else
{
Log.Error("FeedbackPlugin was null.");
}
ImGui.CloseCurrentPopup();
if (!string.IsNullOrWhiteSpace(this.feedbackModalContact))
{
Service<DalamudConfiguration>.Get().LastFeedbackContactDetails = this.feedbackModalContact;
}
ImGui.CloseCurrentPopup();
}
}
ImGui.EndPopup();
@ -489,9 +697,17 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
ImGui.OpenPopup(modalTitle);
this.feedbackModalOnNextFrame = false;
this.feedbackModalDrawing = true;
this.feedbackModalBody = string.Empty;
this.feedbackModalContact = string.Empty;
this.feedbackModalIncludeException = false;
if (!this.feedbackModalOnNextFrameDontClear)
{
this.feedbackModalBody = string.Empty;
this.feedbackModalContact = Service<DalamudConfiguration>.Get().LastFeedbackContactDetails;
this.feedbackModalIncludeException = false;
this.feedbackIsAnonymous = false;
}
else
{
this.feedbackModalOnNextFrameDontClear = false;
}
}
}
@ -1104,6 +1320,8 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
ImGui.SetCursorPos(startCursor);
var pluginDisabled = plugin is { IsDisabled: true };
var iconSize = ImGuiHelpers.ScaledVector2(64, 64);
var cursorBeforeImage = ImGui.GetCursorPos();
var rectOffset = ImGui.GetWindowContentRegionMin() + ImGui.GetWindowPos();
@ -1116,7 +1334,18 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
iconTex = cachedIconTex;
}
if (pluginDisabled)
{
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0.5f);
}
ImGui.Image(iconTex.ImGuiHandle, iconSize);
if (pluginDisabled)
{
ImGui.PopStyleVar();
}
ImGui.SameLine();
ImGui.SetCursorPos(cursorBeforeImage);
}
@ -1125,8 +1354,10 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
if (updateAvailable)
ImGui.Image(this.imageCache.UpdateIcon.ImGuiHandle, iconSize);
else if (trouble)
else if (trouble && !pluginDisabled)
ImGui.Image(this.imageCache.TroubleIcon.ImGuiHandle, iconSize);
else if (pluginDisabled)
ImGui.Image(this.imageCache.DisabledIcon.ImGuiHandle, iconSize);
else if (isLoaded && isThirdParty)
ImGui.Image(this.imageCache.ThirdInstalledIcon.ImGuiHandle, iconSize);
else if (isThirdParty)
@ -1357,6 +1588,7 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
if (ImGui.Button($"{buttonText}##{buttonText}{index}"))
{
this.installStatus = OperationStatus.InProgress;
this.loadingIndicatorKind = LoadingIndicatorKind.Installing;
Task.Run(() => pluginManager.InstallPluginAsync(manifest, useTesting, PluginLoadReason.Installer))
.ContinueWith(task =>
@ -1450,14 +1682,18 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
{
var configuration = Service<DalamudConfiguration>.Get();
var commandManager = Service<CommandManager>.Get();
var pluginManager = Service<PluginManager>.Get();
var startInfo = Service<DalamudStartInfo>.Get();
var trouble = false;
// Name
var label = plugin.Manifest.Name;
// Dev
if (plugin.IsDev)
{
label += Locs.PluginTitleMod_DevPlugin;
}
// Testing
if (plugin.Manifest.Testing)
{
@ -1478,7 +1714,8 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
}
// Load error
if (plugin.State is PluginState.LoadError or PluginState.DependencyResolutionFailed && plugin.CheckPolicy())
if (plugin.State is PluginState.LoadError or PluginState.DependencyResolutionFailed && plugin.CheckPolicy()
&& !plugin.IsOutdated && !plugin.IsBanned && !plugin.IsOrphaned)
{
label += Locs.PluginTitleMod_LoadError;
trouble = true;
@ -1538,6 +1775,12 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
trouble = true;
}
// Scheduled for deletion
if (plugin.Manifest.ScheduledForDeletion)
{
label += Locs.PluginTitleMod_ScheduledForDeletion;
}
ImGui.PushID($"installed{index}{plugin.Manifest.InternalName}");
var hasChangelog = !plugin.Manifest.Changelog.IsNullOrEmpty();
@ -1616,17 +1859,7 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
this.DrawUpdateSinglePluginButton(availablePluginUpdate);
ImGui.SameLine();
var version = plugin.AssemblyName?.Version;
version ??= plugin.Manifest.Testing
? plugin.Manifest.TestingAssemblyVersion
: plugin.Manifest.AssemblyVersion;
ImGui.TextColored(ImGuiColors.DalamudGrey3, $" v{version}");
if (plugin.IsDev)
{
ImGui.SameLine();
ImGui.TextColored(ImGuiColors.DalamudRed, Locs.PluginBody_DeleteDevPlugin);
}
ImGui.TextColored(ImGuiColors.DalamudGrey3, $" v{plugin.Manifest.EffectiveVersion}");
ImGuiHelpers.ScaledDummy(5);
@ -1637,7 +1870,7 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
if (hasChangelog)
{
if (ImGui.TreeNode($"Changelog (v{plugin.Manifest.AssemblyVersion})"))
if (ImGui.TreeNode($"Changelog (v{plugin.Manifest.EffectiveVersion})"))
{
this.DrawInstalledPluginChangelog(plugin.Manifest);
}
@ -1701,10 +1934,8 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
private void DrawPluginControlButton(LocalPlugin plugin)
{
var configuration = Service<DalamudConfiguration>.Get();
var notifications = Service<NotificationManager>.Get();
var pluginManager = Service<PluginManager>.Get();
var startInfo = Service<DalamudStartInfo>.Get();
// Disable everything if the updater is running or another plugin is operating
var disabled = this.updateStatus == OperationStatus.InProgress || this.installStatus == OperationStatus.InProgress;
@ -1718,92 +1949,101 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
// Disable everything if the plugin failed to load
disabled = disabled || plugin.State == PluginState.LoadError || plugin.State == PluginState.DependencyResolutionFailed;
if (plugin.State == PluginState.Loading || plugin.State == PluginState.Unloading)
{
ImGuiComponents.DisabledButton(Locs.PluginButton_Working);
}
else if (plugin.State == PluginState.Loaded || plugin.State == PluginState.LoadError || plugin.State == PluginState.DependencyResolutionFailed)
{
if (pluginManager.SafeMode)
{
ImGuiComponents.DisabledButton(Locs.PluginButton_SafeMode);
}
else if (disabled)
{
ImGuiComponents.DisabledButton(Locs.PluginButton_Disable);
}
else
{
if (ImGui.Button(Locs.PluginButton_Disable))
{
Task.Run(() =>
{
var unloadTask = Task.Run(() => plugin.UnloadAsync())
.ContinueWith(this.DisplayErrorContinuation, Locs.ErrorModal_UnloadFail(plugin.Name));
// Disable everything if we're working
disabled = disabled || plugin.State == PluginState.Loading || plugin.State == PluginState.Unloading;
unloadTask.Wait();
if (!unloadTask.Result)
return;
var toggleId = plugin.Manifest.InternalName;
var isLoadedAndUnloadable = plugin.State == PluginState.Loaded ||
plugin.State == PluginState.DependencyResolutionFailed;
var disableTask = Task.Run(() => plugin.Disable())
.ContinueWith(this.DisplayErrorContinuation, Locs.ErrorModal_DisableFail(plugin.Name));
StyleModelV1.DalamudStandard.Push();
disableTask.Wait();
if (!disableTask.Result)
return;
if (!plugin.IsDev)
{
pluginManager.RemovePlugin(plugin);
}
notifications.AddNotification(Locs.Notifications_PluginDisabled(plugin.Manifest.Name), Locs.Notifications_PluginDisabledTitle, NotificationType.Success);
});
}
}
if (plugin.State == PluginState.Loaded)
{
// Only if the plugin isn't broken.
this.DrawOpenPluginSettingsButton(plugin);
}
}
else if (plugin.State == PluginState.Unloaded)
{
if (disabled)
{
ImGuiComponents.DisabledButton(Locs.PluginButton_Load);
}
else
{
if (ImGui.Button(Locs.PluginButton_Load))
{
Task.Run(() =>
{
var enableTask = Task.Run(() => plugin.Enable())
.ContinueWith(this.DisplayErrorContinuation, Locs.ErrorModal_EnableFail(plugin.Name));
enableTask.Wait();
if (!enableTask.Result)
return;
var loadTask = Task.Run(() => plugin.LoadAsync(PluginLoadReason.Installer))
.ContinueWith(this.DisplayErrorContinuation, Locs.ErrorModal_LoadFail(plugin.Name));
loadTask.Wait();
if (!loadTask.Result)
return;
});
}
}
}
else if (plugin.State == PluginState.UnloadError)
if (plugin.State == PluginState.UnloadError)
{
ImGuiComponents.DisabledButton(FontAwesomeIcon.Frown);
if (ImGui.IsItemHovered())
ImGui.SetTooltip(Locs.PluginButtonToolTip_UnloadFailed);
}
else if (disabled)
{
ImGuiComponents.DisabledToggleButton(toggleId, isLoadedAndUnloadable);
}
else
{
if (ImGuiComponents.ToggleButton(toggleId, ref isLoadedAndUnloadable))
{
if (!isLoadedAndUnloadable)
{
this.enableDisableStatus = OperationStatus.InProgress;
this.loadingIndicatorKind = LoadingIndicatorKind.DisablingSingle;
Task.Run(() =>
{
var unloadTask = Task.Run(() => plugin.UnloadAsync())
.ContinueWith(this.DisplayErrorContinuation, Locs.ErrorModal_UnloadFail(plugin.Name));
unloadTask.Wait();
if (!unloadTask.Result)
{
this.enableDisableStatus = OperationStatus.Complete;
return;
}
var disableTask = Task.Run(() => plugin.Disable())
.ContinueWith(this.DisplayErrorContinuation, Locs.ErrorModal_DisableFail(plugin.Name));
disableTask.Wait();
this.enableDisableStatus = OperationStatus.Complete;
if (!disableTask.Result)
return;
notifications.AddNotification(Locs.Notifications_PluginDisabled(plugin.Manifest.Name), Locs.Notifications_PluginDisabledTitle, NotificationType.Success);
});
}
else
{
this.enableDisableStatus = OperationStatus.InProgress;
this.loadingIndicatorKind = LoadingIndicatorKind.EnablingSingle;
Task.Run(() =>
{
var enableTask = Task.Run(() => plugin.Enable())
.ContinueWith(this.DisplayErrorContinuation, Locs.ErrorModal_EnableFail(plugin.Name));
enableTask.Wait();
if (!enableTask.Result)
{
this.enableDisableStatus = OperationStatus.Complete;
return;
}
var loadTask = Task.Run(() => plugin.LoadAsync(PluginLoadReason.Installer))
.ContinueWith(this.DisplayErrorContinuation, Locs.ErrorModal_LoadFail(plugin.Name));
loadTask.Wait();
this.enableDisableStatus = OperationStatus.Complete;
if (!loadTask.Result)
return;
notifications.AddNotification(Locs.Notifications_PluginEnabled(plugin.Manifest.Name), Locs.Notifications_PluginEnabledTitle, NotificationType.Success);
});
}
}
}
StyleModelV1.DalamudStandard.Pop();
ImGui.SameLine();
ImGuiHelpers.ScaledDummy(15, 0);
if (plugin.State == PluginState.Loaded)
{
// Only if the plugin isn't broken.
this.DrawOpenPluginSettingsButton(plugin);
}
}
private void DrawUpdateSinglePluginButton(AvailablePluginUpdate update)
@ -1815,6 +2055,7 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
if (ImGuiComponents.IconButton(FontAwesomeIcon.Download))
{
this.installStatus = OperationStatus.InProgress;
this.loadingIndicatorKind = LoadingIndicatorKind.UpdatingSingle;
Task.Run(async () => await pluginManager.UpdateSinglePluginAsync(update, true, false))
.ContinueWith(task =>
@ -1883,6 +2124,8 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
private void DrawDevPluginButtons(LocalPlugin localPlugin)
{
ImGui.SameLine();
var configuration = Service<DalamudConfiguration>.Get();
if (localPlugin is LocalDevPlugin plugin)
@ -1931,18 +2174,20 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
private void DrawDeletePluginButton(LocalPlugin plugin)
{
var unloaded = plugin.State == PluginState.Unloaded || plugin.State == PluginState.LoadError;
/*var unloaded = plugin.State == PluginState.Unloaded || plugin.State == PluginState.LoadError;
// When policy check fails, the plugin is never loaded
var showButton = unloaded && (plugin.IsDev || plugin.IsOutdated || plugin.IsBanned || plugin.IsOrphaned || !plugin.CheckPolicy());
if (!showButton)
return;
return;*/
var pluginManager = Service<PluginManager>.Get();
var devNotDeletable = plugin.IsDev && plugin.State != PluginState.Unloaded && plugin.State != PluginState.DependencyResolutionFailed;
ImGui.SameLine();
if (plugin.HasEverStartedLoad)
if (plugin.State == PluginState.Loaded || devNotDeletable)
{
ImGui.PushFont(InterfaceManager.IconFont);
ImGuiComponents.DisabledButton(FontAwesomeIcon.TrashAlt.ToIconString());
@ -1950,7 +2195,9 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
if (ImGui.IsItemHovered())
{
ImGui.SetTooltip(Locs.PluginButtonToolTip_DeletePluginRestricted);
ImGui.SetTooltip(plugin.State == PluginState.Loaded
? Locs.PluginButtonToolTip_DeletePluginLoaded
: Locs.PluginButtonToolTip_DeletePluginRestricted);
}
}
else
@ -1959,22 +2206,45 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
{
try
{
plugin.DllFile.Delete();
pluginManager.RemovePlugin(plugin);
if (plugin.IsDev)
{
plugin.DllFile.Delete();
}
else
{
plugin.ScheduleDeletion(!plugin.Manifest.ScheduledForDeletion);
}
if (plugin.State is PluginState.Unloaded or PluginState.DependencyResolutionFailed)
{
pluginManager.RemovePlugin(plugin);
}
}
catch (Exception ex)
{
Log.Error(ex, $"Plugin installer threw an error during removal of {plugin.Name}");
this.errorModalMessage = Locs.ErrorModal_DeleteFail(plugin.Name);
this.errorModalDrawing = true;
this.errorModalOnNextFrame = true;
this.ShowErrorModal(Locs.ErrorModal_DeleteFail(plugin.Name));
}
}
if (ImGui.IsItemHovered())
{
ImGui.SetTooltip(Locs.PluginButtonToolTip_DeletePlugin);
string tooltipMessage;
if (plugin.Manifest.ScheduledForDeletion)
{
tooltipMessage = Locs.PluginButtonToolTip_DeletePluginScheduledCancel;
}
else if (plugin.State is PluginState.Unloaded or PluginState.DependencyResolutionFailed)
{
tooltipMessage = Locs.PluginButtonToolTip_DeletePlugin;
}
else
{
tooltipMessage = Locs.PluginButtonToolTip_DeletePluginScheduled;
}
ImGui.SetTooltip(tooltipMessage);
}
}
}
@ -2202,11 +2472,13 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
return true;
}
private void ShowErrorModal(string message)
private Task ShowErrorModal(string message)
{
this.errorModalMessage = message;
this.errorModalDrawing = true;
this.errorModalOnNextFrame = true;
this.errorModalTaskCompletionSource = new TaskCompletionSource();
return this.errorModalTaskCompletionSource!.Task;
}
private void UpdateCategoriesOnSearchChange()
@ -2299,6 +2571,8 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
public static string PluginTitleMod_TestingVersion => Loc.Localize("InstallerTestingVersion", " (testing version)");
public static string PluginTitleMod_DevPlugin => Loc.Localize("InstallerDevPlugin", " (dev plugin)");
public static string PluginTitleMod_UpdateFailed => Loc.Localize("InstallerUpdateFailed", " (update failed)");
public static string PluginTitleMod_LoadError => Loc.Localize("InstallerLoadError", " (load error)");
@ -2308,9 +2582,11 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
public static string PluginTitleMod_OutdatedError => Loc.Localize("InstallerOutdatedError", " (outdated)");
public static string PluginTitleMod_BannedError => Loc.Localize("InstallerBannedError", " (automatically disabled)");
public static string PluginTitleMod_OrphanedError => Loc.Localize("InstallerOrphanedError", " (unknown repository)");
public static string PluginTitleMod_ScheduledForDeletion => Loc.Localize("InstallerScheduledForDeletion", " (scheduled for deletion)");
public static string PluginTitleMod_New => Loc.Localize("InstallerNewPlugin ", " New!");
#endregion
@ -2340,9 +2616,6 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
public static string PluginBody_Plugin3rdPartyRepo(string url) => Loc.Localize("InstallerPlugin3rdPartyRepo", "From custom plugin repository {0}").Format(url);
public static string PluginBody_AvailableDevPlugin => Loc.Localize("InstallerDevPlugin", " This plugin is available in one of your repos, please remove it from the devPlugins folder.");
public static string PluginBody_DeleteDevPlugin => Loc.Localize("InstallerDeleteDevPlugin ", " To delete this plugin, please remove it from the devPlugins folder.");
public static string PluginBody_Outdated => Loc.Localize("InstallerOutdatedPluginBody ", "This plugin is outdated and incompatible at the moment. Please wait for it to be updated by its author.");
public static string PluginBody_Orphaned => Loc.Localize("InstallerOrphanedPluginBody ", "This plugin's source repository is no longer available. You may need to reinstall it from its repository, or re-add the repository.");
@ -2384,7 +2657,13 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
public static string PluginButtonToolTip_DeletePlugin => Loc.Localize("InstallerDeletePlugin ", "Delete plugin");
public static string PluginButtonToolTip_DeletePluginRestricted => Loc.Localize("InstallerDeletePluginRestricted", "Cannot delete - please try restarting the game.");
public static string PluginButtonToolTip_DeletePluginRestricted => Loc.Localize("InstallerDeletePluginRestricted", "Cannot delete right now - please restart the game.");
public static string PluginButtonToolTip_DeletePluginScheduled => Loc.Localize("InstallerDeletePluginScheduled", "Delete plugin on next restart");
public static string PluginButtonToolTip_DeletePluginScheduledCancel => Loc.Localize("InstallerDeletePluginScheduledCancel", "Cancel scheduled deletion");
public static string PluginButtonToolTip_DeletePluginLoaded => Loc.Localize("InstallerDeletePluginLoaded", "Disable this plugin before deleting it.");
public static string PluginButtonToolTip_VisitPluginUrl => Loc.Localize("InstallerVisitPluginUrl", "Visit plugin URL");
@ -2416,6 +2695,10 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
public static string Notifications_PluginDisabled(string name) => Loc.Localize("NotificationsPluginDisabled", "'{0}' was disabled.").Format(name);
public static string Notifications_PluginEnabledTitle => Loc.Localize("NotificationsPluginEnabledTitle", "Plugin enabled!");
public static string Notifications_PluginEnabled(string name) => Loc.Localize("NotificationsPluginEnabled", "'{0}' was enabled.").Format(name);
#endregion
#region Footer
@ -2478,12 +2761,26 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
public static string FeedbackModal_Title => Loc.Localize("InstallerFeedback", "Send Feedback");
public static string FeedbackModal_Text(string pluginName) => Loc.Localize("InstallerFeedbackInfo", "You can send feedback to the developer of \"{0}\" here.\nYou can include your Discord tag or email address if you wish to give them the opportunity to answer.\n\nIf you put your Discord name into the contact information field, please join the XIVLauncher discord server so we can get in touch.").Format(pluginName);
public static string FeedbackModal_Text(string pluginName) => Loc.Localize("InstallerFeedbackInfo", "You can send feedback to the developer of \"{0}\" here.").Format(pluginName);
public static string FeedbackModal_HasUpdate => Loc.Localize("InstallerFeedbackHasUpdate", "A new version of this plugin is available, please update before reporting bugs.");
public static string FeedbackModal_ContactAnonymous => Loc.Localize("InstallerFeedbackContactAnonymous", "Submit feedback anonymously");
public static string FeedbackModal_ContactAnonymousWarning => Loc.Localize("InstallerFeedbackContactAnonymousWarning", "No response will be forthcoming.\nUntick \"{0}\" and provide contact information if you need help.").Format(FeedbackModal_ContactAnonymous);
public static string FeedbackModal_ContactInformation => Loc.Localize("InstallerFeedbackContactInfo", "Contact information");
public static string FeedbackModal_ContactInformationHelp => Loc.Localize("InstallerFeedbackContactInfoHelp", "Discord usernames and e-mail addresses are accepted.\nIf you submit a Discord username, please join our discord server so that we can reach out to you easier.");
public static string FeedbackModal_ContactInformationWarning => Loc.Localize("InstallerFeedbackContactInfoWarning", "Do not submit in-game character names.");
public static string FeedbackModal_ContactInformationRequired => Loc.Localize("InstallerFeedbackContactInfoRequired", "Contact information has not been provided. If you do not want to provide contact information, tick on \"{0}\" above.").Format(FeedbackModal_ContactAnonymous);
public static string FeedbackModal_ContactInformationDiscordButton => Loc.Localize("ContactInformationDiscordButton", "Join Goat Place Discord");
public static string FeedbackModal_ContactInformationDiscordUrl => Loc.Localize("ContactInformationDiscordUrl", "https://goat.place/");
public static string FeedbackModal_IncludeLastError => Loc.Localize("InstallerFeedbackIncludeLastError", "Include last error message");
public static string FeedbackModal_IncludeLastErrorHint => Loc.Localize("InstallerFeedbackIncludeLastErrorHint", "This option can give the plugin developer useful feedback on what exactly went wrong.");

View file

@ -1,10 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Dalamud.Configuration.Internal;
using Dalamud.Interface.Colors;
using Dalamud.Utility;
using ImGuiNET;
using Newtonsoft.Json;
using Serilog;
@ -15,6 +16,10 @@ namespace Dalamud.Interface.Style
/// </summary>
public abstract class StyleModel
{
private static int NumPushedStyles = 0;
private static int NumPushedColors = 0;
private static bool HasPushedOnce = false;
/// <summary>
/// Gets or sets the name of the style model.
/// </summary>
@ -84,7 +89,7 @@ namespace Dalamud.Interface.Style
configuration.SavedStyles = new List<StyleModel>();
configuration.SavedStyles.AddRange(configuration.SavedStylesOld);
Log.Information("Transferred {0} styles", configuration.SavedStyles.Count);
Log.Information("Transferred {NumStyles} styles", configuration.SavedStyles.Count);
configuration.SavedStylesOld = null;
configuration.Save();
@ -123,6 +128,60 @@ namespace Dalamud.Interface.Style
/// <summary>
/// Pop this style model from the ImGui style/color stack.
/// </summary>
public abstract void Pop();
public void Pop()
{
if (!HasPushedOnce)
throw new InvalidOperationException("Wasn't pushed at least once.");
ImGui.PopStyleVar(NumPushedStyles);
ImGui.PopStyleColor(NumPushedColors);
}
/// <summary>
/// Push a style var.
/// </summary>
/// <param name="style">Style kind.</param>
/// <param name="arg">Style var.</param>
protected void PushStyleHelper(ImGuiStyleVar style, float arg)
{
ImGui.PushStyleVar(style, arg);
if (!HasPushedOnce)
NumPushedStyles++;
}
/// <summary>
/// Push a style var.
/// </summary>
/// <param name="style">Style kind.</param>
/// <param name="arg">Style var.</param>
protected void PushStyleHelper(ImGuiStyleVar style, Vector2 arg)
{
ImGui.PushStyleVar(style, arg);
if (!HasPushedOnce)
NumPushedStyles++;
}
/// <summary>
/// Push a style color.
/// </summary>
/// <param name="color">Color kind.</param>
/// <param name="value">Color value.</param>
protected void PushColorHelper(ImGuiCol color, Vector4 value)
{
ImGui.PushStyleColor(color, value);
if (!HasPushedOnce)
NumPushedColors++;
}
/// <summary>
/// Indicate that you have pushed.
/// </summary>
protected void DonePushing()
{
HasPushedOnce = true;
}
}
}

View file

@ -487,13 +487,41 @@ namespace Dalamud.Interface.Style
/// <inheritdoc/>
public override void Push()
{
throw new NotImplementedException();
}
this.PushStyleHelper(ImGuiStyleVar.Alpha, this.Alpha);
this.PushStyleHelper(ImGuiStyleVar.WindowPadding, this.WindowPadding);
this.PushStyleHelper(ImGuiStyleVar.WindowRounding, this.WindowRounding);
this.PushStyleHelper(ImGuiStyleVar.WindowBorderSize, this.WindowBorderSize);
this.PushStyleHelper(ImGuiStyleVar.WindowTitleAlign, this.WindowTitleAlign);
this.PushStyleHelper(ImGuiStyleVar.ChildRounding, this.ChildRounding);
this.PushStyleHelper(ImGuiStyleVar.ChildBorderSize, this.ChildBorderSize);
this.PushStyleHelper(ImGuiStyleVar.PopupRounding, this.PopupRounding);
this.PushStyleHelper(ImGuiStyleVar.PopupBorderSize, this.PopupBorderSize);
this.PushStyleHelper(ImGuiStyleVar.FramePadding, this.FramePadding);
this.PushStyleHelper(ImGuiStyleVar.FrameRounding, this.FrameRounding);
this.PushStyleHelper(ImGuiStyleVar.FrameBorderSize, this.FrameBorderSize);
this.PushStyleHelper(ImGuiStyleVar.ItemSpacing, this.ItemSpacing);
this.PushStyleHelper(ImGuiStyleVar.ItemInnerSpacing, this.ItemInnerSpacing);
this.PushStyleHelper(ImGuiStyleVar.CellPadding, this.CellPadding);
this.PushStyleHelper(ImGuiStyleVar.IndentSpacing, this.IndentSpacing);
this.PushStyleHelper(ImGuiStyleVar.ScrollbarSize, this.ScrollbarSize);
this.PushStyleHelper(ImGuiStyleVar.ScrollbarRounding, this.ScrollbarRounding);
this.PushStyleHelper(ImGuiStyleVar.GrabMinSize, this.GrabMinSize);
this.PushStyleHelper(ImGuiStyleVar.GrabRounding, this.GrabRounding);
this.PushStyleHelper(ImGuiStyleVar.TabRounding, this.TabRounding);
this.PushStyleHelper(ImGuiStyleVar.ButtonTextAlign, this.ButtonTextAlign);
this.PushStyleHelper(ImGuiStyleVar.SelectableTextAlign, this.SelectableTextAlign);
/// <inheritdoc/>
public override void Pop()
{
throw new NotImplementedException();
foreach (var imGuiCol in Enum.GetValues<ImGuiCol>())
{
if (imGuiCol == ImGuiCol.COUNT)
{
continue;
}
this.PushColorHelper(imGuiCol, this.Colors[imGuiCol.ToString()]);
}
this.DonePushing();
}
}
}

View file

@ -17,6 +17,8 @@ using Dalamud.Game;
using Dalamud.Game.Gui;
using Dalamud.Game.Gui.Dtr;
using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Interface.Internal;
using Dalamud.Logging.Internal;
using Dalamud.Plugin.Internal.Exceptions;
@ -50,6 +52,8 @@ internal partial class PluginManager : IDisposable, IServiceType
private readonly DirectoryInfo devPluginDirectory;
private readonly BannedPlugin[]? bannedPlugins;
private readonly DalamudLinkPayload openInstallerWindowPluginChangelogsLink;
[ServiceManager.ServiceDependency]
private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.Get();
@ -69,6 +73,23 @@ internal partial class PluginManager : IDisposable, IServiceType
this.devPluginDirectory.Create();
this.SafeMode = EnvironmentConfiguration.DalamudNoPlugins || this.configuration.PluginSafeMode || this.startInfo.NoLoadPlugins;
try
{
var appdata = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
var safeModeFile = Path.Combine(appdata, "XIVLauncher", ".dalamud_safemode");
if (File.Exists(safeModeFile))
{
this.SafeMode = true;
File.Delete(safeModeFile);
}
}
catch (Exception ex)
{
Log.Error(ex, "Couldn't check safe mode file");
}
if (this.SafeMode)
{
this.configuration.PluginSafeMode = false;
@ -84,6 +105,11 @@ internal partial class PluginManager : IDisposable, IServiceType
throw new InvalidDataException("Couldn't deserialize banned plugins manifest.");
}
this.openInstallerWindowPluginChangelogsLink = Service<ChatGui>.Get().AddChatLinkHandler("Dalamud", 1003, (i, m) =>
{
Service<DalamudInterface>.GetNullable()?.OpenPluginInstallerPluginChangelogs();
});
this.ApplyPatches();
}
@ -125,7 +151,7 @@ internal partial class PluginManager : IDisposable, IServiceType
/// <summary>
/// Gets a value indicating whether all added repos are not in progress.
/// </summary>
public bool ReposReady => this.Repos.All(repo => repo.State != PluginRepositoryState.InProgress);
public bool ReposReady => this.Repos.All(repo => repo.State != PluginRepositoryState.InProgress || repo.State != PluginRepositoryState.Fail);
/// <summary>
/// Gets a value indicating whether the plugin manager started in safe mode.
@ -152,25 +178,38 @@ internal partial class PluginManager : IDisposable, IServiceType
/// </summary>
/// <param name="updateMetadata">The list of updated plugin metadata.</param>
/// <param name="header">The header text to send to chat prior to any update info.</param>
public static void PrintUpdatedPlugins(List<PluginUpdateStatus>? updateMetadata, string header)
public void PrintUpdatedPlugins(List<PluginUpdateStatus>? updateMetadata, string header)
{
var chatGui = Service<ChatGui>.Get();
if (updateMetadata is { Count: > 0 })
{
chatGui.Print(header);
chatGui.PrintChat(new XivChatEntry
{
Message = new SeString(new List<Payload>()
{
new TextPayload(header),
new TextPayload(" ["),
new UIForegroundPayload(500),
this.openInstallerWindowPluginChangelogsLink,
new TextPayload(Loc.Localize("DalamudInstallerPluginChangelogHelp", "Open plugin changelogs") + " "),
RawPayload.LinkTerminator,
new UIForegroundPayload(0),
new TextPayload("]"),
}),
});
foreach (var metadata in updateMetadata)
{
if (metadata.WasUpdated)
{
chatGui.Print(Locs.DalamudPluginUpdateSuccessful(metadata.Name, metadata.Version));
chatGui.Print(Locs.DalamudPluginUpdateSuccessful(metadata.Name, metadata.Version) + (metadata.HasChangelog ? " " : string.Empty));
}
else
{
chatGui.PrintChat(new XivChatEntry
{
Message = Locs.DalamudPluginUpdateFailed(metadata.Name, metadata.Version),
Message = Locs.DalamudPluginUpdateFailed(metadata.Name, metadata.Version) + (metadata.HasChangelog ? " " : string.Empty),
Type = XivChatType.Urgent,
});
}
@ -226,10 +265,12 @@ internal partial class PluginManager : IDisposable, IServiceType
/// <inheritdoc/>
public void Dispose()
{
if (this.InstalledPlugins.Any())
var disposablePlugins =
this.InstalledPlugins.Where(plugin => plugin.State is PluginState.Loaded or PluginState.LoadError).ToArray();
if (disposablePlugins.Any())
{
// Unload them first, just in case some of plugin codes are still running via callbacks initiated externally.
foreach (var plugin in this.InstalledPlugins.Where(plugin => !plugin.Manifest.CanUnloadAsync))
foreach (var plugin in disposablePlugins.Where(plugin => !plugin.Manifest.CanUnloadAsync))
{
try
{
@ -241,7 +282,7 @@ internal partial class PluginManager : IDisposable, IServiceType
}
}
Task.WaitAll(this.InstalledPlugins
Task.WaitAll(disposablePlugins
.Where(plugin => plugin.Manifest.CanUnloadAsync)
.Select(plugin => Task.Run(async () =>
{
@ -261,7 +302,7 @@ internal partial class PluginManager : IDisposable, IServiceType
// Now that we've waited enough, dispose the whole plugin.
// Since plugins should have been unloaded above, this should be done quickly.
foreach (var plugin in this.InstalledPlugins)
foreach (var plugin in disposablePlugins)
plugin.ExplicitDisposeIgnoreExceptions($"Error disposing {plugin.Name}", Log);
}
@ -307,6 +348,7 @@ internal partial class PluginManager : IDisposable, IServiceType
// Add installed plugins. These are expected to be in a specific format so we can look for exactly that.
foreach (var pluginDir in this.pluginDirectory.GetDirectories())
{
var versionsDefs = new List<PluginDef>();
foreach (var versionDir in pluginDir.GetDirectories())
{
var dllFile = new FileInfo(Path.Combine(versionDir.FullName, $"{pluginDir.Name}.dll"));
@ -317,7 +359,16 @@ internal partial class PluginManager : IDisposable, IServiceType
var manifest = LocalPluginManifest.Load(manifestFile);
pluginDefs.Add(new PluginDef(dllFile, manifest, false));
versionsDefs.Add(new PluginDef(dllFile, manifest, false));
}
try
{
pluginDefs.Add(versionsDefs.OrderByDescending(x => x.Manifest!.EffectiveVersion).First());
}
catch (Exception ex)
{
Log.Error(ex, "Couldn't choose best version for plugin: {Name}", pluginDir.Name);
}
}
@ -482,7 +533,9 @@ internal partial class PluginManager : IDisposable, IServiceType
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public async Task ReloadPluginMastersAsync(bool notify = true)
{
Log.Information("Now reloading all PluginMasters...");
await Task.WhenAll(this.Repos.Select(repo => repo.ReloadPluginMasterAsync()));
Log.Information("PluginMasters reloaded, now refiltering...");
this.RefilterPluginMasters(notify);
}
@ -712,10 +765,14 @@ internal partial class PluginManager : IDisposable, IServiceType
{
try
{
if (plugin.IsDisabled)
plugin.Enable();
await plugin.LoadAsync(reason);
if (!plugin.IsDisabled)
{
await plugin.LoadAsync(reason);
}
else
{
Log.Verbose($"{name} was disabled");
}
}
catch (InvalidPluginException)
{
@ -832,10 +889,17 @@ internal partial class PluginManager : IDisposable, IServiceType
}
else
{
foreach (var versionDir in versionDirs)
for (var i = 0; i < versionDirs.Length; i++)
{
var versionDir = versionDirs[i];
try
{
if (i != 0)
{
Log.Information($"Old version: cleaning up {versionDir.FullName}");
versionDir.Delete(true);
continue;
}
var dllFile = new FileInfo(Path.Combine(versionDir.FullName, $"{pluginDir.Name}.dll"));
if (!dllFile.Exists)
{
@ -849,6 +913,21 @@ internal partial class PluginManager : IDisposable, IServiceType
{
Log.Information($"Missing manifest: cleaning up {versionDir.FullName}");
versionDir.Delete(true);
continue;
}
if (manifestFile.Length == 0)
{
Log.Information($"Manifest empty: cleaning up {versionDir.FullName}");
versionDir.Delete(true);
continue;
}
var manifest = LocalPluginManifest.Load(manifestFile);
if (manifest.ScheduledForDeletion)
{
Log.Information($"Scheduled deletion: cleaning up {versionDir.FullName}");
versionDir.Delete(true);
}
}
catch (Exception ex)
@ -870,7 +949,7 @@ internal partial class PluginManager : IDisposable, IServiceType
/// </summary>
/// <param name="dryRun">Perform a dry run, don't install anything.</param>
/// <returns>Success or failure and a list of updated plugin metadata.</returns>
public async Task<List<PluginUpdateStatus>> UpdatePluginsAsync(bool dryRun = false)
public async Task<List<PluginUpdateStatus>> UpdatePluginsAsync(bool ignoreDisabled, bool dryRun)
{
Log.Information("Starting plugin update");
@ -883,6 +962,12 @@ internal partial class PluginManager : IDisposable, IServiceType
if (plugin.InstalledPlugin.IsDev)
continue;
if (plugin.InstalledPlugin.Manifest.Disabled && ignoreDisabled)
continue;
if (plugin.InstalledPlugin.Manifest.ScheduledForDeletion)
continue;
var result = await this.UpdateSinglePluginAsync(plugin, false, dryRun);
if (result != null)
updatedList.Add(result);
@ -914,6 +999,7 @@ internal partial class PluginManager : IDisposable, IServiceType
? metadata.UpdateManifest.TestingAssemblyVersion
: metadata.UpdateManifest.AssemblyVersion)!,
WasUpdated = true,
HasChangelog = !metadata.UpdateManifest.Changelog.IsNullOrWhitespace(),
};
if (!dryRun)
@ -954,7 +1040,9 @@ internal partial class PluginManager : IDisposable, IServiceType
{
try
{
plugin.Disable();
if (!plugin.IsDisabled)
plugin.Disable();
lock (this.pluginListLock)
{
this.InstalledPlugins = this.InstalledPlugins.Remove(plugin);

View file

@ -138,9 +138,9 @@ internal class LocalDevPlugin : LocalPlugin, IDisposable
return;
}
if (this.State != PluginState.Loaded)
if (this.State != PluginState.Loaded && this.State != PluginState.LoadError)
{
Log.Debug($"Skipping reload of {this.Name}, state ({this.State}) is not {PluginState.Loaded}.");
Log.Debug($"Skipping reload of {this.Name}, state ({this.State}) is not {PluginState.Loaded} nor {PluginState.LoadError}.");
return;
}

View file

@ -560,6 +560,7 @@ internal class LocalPlugin : IDisposable
throw new InvalidPluginOperationException($"Unable to enable {this.Name}, not disabled");
this.Manifest.Disabled = false;
this.Manifest.ScheduledForDeletion = false;
this.SaveManifest();
}
@ -614,6 +615,16 @@ internal class LocalPlugin : IDisposable
this.SaveManifest();
}
/// <summary>
/// Schedule the deletion of this plugin on next cleanup.
/// </summary>
/// <param name="status">Schedule or cancel the deletion.</param>
public void ScheduleDeletion(bool status = true)
{
this.Manifest.ScheduledForDeletion = status;
this.SaveManifest();
}
private static void SetupLoaderConfig(LoaderConfig config)
{
config.IsUnloadable = true;

View file

@ -1,3 +1,4 @@
using System;
using System.IO;
using Dalamud.Utility;
@ -23,6 +24,11 @@ internal record LocalPluginManifest : PluginManifest
/// </summary>
public bool Testing { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the plugin should be deleted during the next cleanup.
/// </summary>
public bool ScheduledForDeletion { get; set; }
/// <summary>
/// Gets or sets the 3rd party repo URL that this plugin was installed from. Used to display where the plugin was
/// sourced from on the installed plugin view. This should not be included in the plugin master. This value is null
@ -36,6 +42,11 @@ internal record LocalPluginManifest : PluginManifest
/// </summary>
public bool IsThirdParty => !this.InstalledFromUrl.IsNullOrEmpty() && this.InstalledFromUrl != PluginRepository.MainRepoUrl;
/// <summary>
/// Gets the effective version of this plugin.
/// </summary>
public Version EffectiveVersion => this.Testing && this.TestingAssemblyVersion != null ? this.TestingAssemblyVersion : this.AssemblyVersion;
/// <summary>
/// Save a plugin manifest to file.
/// </summary>

View file

@ -181,4 +181,17 @@ internal record PluginManifest
/// Gets a message that is shown to users when sending feedback.
/// </summary>
public string? FeedbackMessage { get; init; }
/// <summary>
/// Gets a value indicating whether this plugin is DIP17.
/// To be removed.
/// </summary>
[JsonProperty("_isDip17Plugin")]
public bool IsDip17Plugin { get; init; } = false;
/// <summary>
/// Gets the DIP17 channel name.
/// </summary>
[JsonProperty("_Dip17Channel")]
public string? Dip17Channel { get; init; }
}

View file

@ -24,6 +24,7 @@ internal class PluginRepository
private static readonly HttpClient HttpClient = new()
{
Timeout = TimeSpan.FromSeconds(20),
DefaultRequestHeaders =
{
CacheControl = new CacheControlHeaderValue
@ -109,7 +110,7 @@ internal class PluginRepository
this.PluginMaster = pluginMaster.AsReadOnly();
Log.Debug($"Successfully fetched repo: {this.PluginMasterUrl}");
Log.Information($"Successfully fetched repo: {this.PluginMasterUrl}");
this.State = PluginRepositoryState.Success;
}
catch (Exception ex)

View file

@ -26,4 +26,9 @@ internal class PluginUpdateStatus
/// Gets or sets a value indicating whether the plugin was updated.
/// </summary>
public bool WasUpdated { get; set; }
/// <summary>
/// Gets a value indicating whether the plugin has a changelog if it was updated.
/// </summary>
public bool HasChangelog { get; init; }
}

View file

@ -0,0 +1,633 @@
#include <filesystem>
#include <fstream>
#include <iostream>
#include <map>
#include <optional>
#include <ranges>
#include <span>
#include <sstream>
#include <string>
#include <thread>
#include <vector>
#define WIN32_LEAN_AND_MEAN
#define NOMINMAX
#include <Windows.h>
#include <CommCtrl.h>
#include <DbgHelp.h>
#include <minidumpapiset.h>
#include <PathCch.h>
#include <Psapi.h>
#include <shellapi.h>
#include <winhttp.h>
#pragma comment(lib, "comctl32.lib")
#pragma comment(linker, "/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"")
#include "resource.h"
#include "../Dalamud.Boot/crashhandler_shared.h"
HANDLE g_hProcess = nullptr;
bool g_bSymbolsAvailable = false;
const std::map<HMODULE, size_t>& get_remote_modules() {
static const auto data = [] {
std::map<HMODULE, size_t> data;
std::vector<HMODULE> buf(8192);
for (size_t i = 0; i < 64; i++) {
if (DWORD needed; !EnumProcessModules(g_hProcess, &buf[0], static_cast<DWORD>(std::span(buf).size_bytes()), &needed)) {
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);
} else {
buf.resize(needed / sizeof(HMODULE));
break;
}
}
for (const auto& hModule : buf) {
IMAGE_DOS_HEADER dosh;
IMAGE_NT_HEADERS64 nth64;
if (size_t read; !ReadProcessMemory(g_hProcess, hModule, &dosh, sizeof dosh, &read) || read != sizeof dosh) {
std::cerr << std::format("Failed to read IMAGE_DOS_HEADER for module at 0x{:x}", reinterpret_cast<size_t>(hModule)) << std::endl;
continue;
}
if (size_t read; !ReadProcessMemory(g_hProcess, reinterpret_cast<const char*>(hModule) + dosh.e_lfanew, &nth64, sizeof nth64, &read) || read != sizeof nth64) {
std::cerr << std::format("Failed to read IMAGE_NT_HEADERS64 for module at 0x{:x}", reinterpret_cast<size_t>(hModule)) << std::endl;
continue;
}
data[hModule] = nth64.OptionalHeader.SizeOfImage;
}
return data;
}();
return data;
}
const std::map<HMODULE, std::filesystem::path>& get_remote_module_paths() {
static const auto data = [] {
std::map<HMODULE, std::filesystem::path> data;
std::wstring buf(PATHCCH_MAX_CCH, L'\0');
for (const auto& hModule : get_remote_modules() | std::views::keys) {
buf.resize(PATHCCH_MAX_CCH, L'\0');
buf.resize(GetModuleFileNameExW(g_hProcess, hModule, &buf[0], PATHCCH_MAX_CCH));
if (buf.empty()) {
std::cerr << std::format("Failed to get path for module at 0x{:x}: error 0x{:x}", reinterpret_cast<size_t>(hModule), GetLastError()) << std::endl;
continue;
}
data[hModule] = buf;
}
return data;
}();
return data;
}
bool get_module_file_and_base(const DWORD64 address, DWORD64& module_base, std::filesystem::path& module_file) {
for (const auto& [hModule, path] : get_remote_module_paths()) {
const auto nAddress = reinterpret_cast<DWORD64>(hModule);
if (address < nAddress)
continue;
const auto nAddressTo = nAddress + get_remote_modules().at(hModule);
if (nAddressTo <= address)
continue;
module_base = nAddress;
module_file = path;
return true;
}
return false;
}
bool is_ffxiv_address(const wchar_t* module_name, const DWORD64 address) {
DWORD64 module_base;
if (std::filesystem::path module_path; get_module_file_and_base(address, module_base, module_path))
return _wcsicmp(module_path.filename().c_str(), module_name) == 0;
return false;
}
bool get_sym_from_addr(const DWORD64 address, DWORD64& displacement, std::wstring& symbol_name) {
if (!g_bSymbolsAvailable)
return false;
union {
char buffer[sizeof(SYMBOL_INFOW) + MAX_SYM_NAME * sizeof(wchar_t)]{};
SYMBOL_INFOW symbol;
};
symbol.SizeOfStruct = sizeof(SYMBOL_INFO);
symbol.MaxNameLen = MAX_SYM_NAME;
if (SymFromAddrW(g_hProcess, address, &displacement, &symbol) && symbol.Name[0]) {
symbol_name = symbol.Name;
return true;
}
return false;
}
std::wstring to_address_string(const DWORD64 address, const bool try_ptrderef = true) {
DWORD64 module_base;
std::filesystem::path module_path;
bool is_mod_addr = get_module_file_and_base(address, module_base, module_path);
DWORD64 value = 0;
if (try_ptrderef && address > 0x10000 && address < 0x7FFFFFFE0000) {
ReadProcessMemory(g_hProcess, reinterpret_cast<void*>(address), &value, sizeof value, nullptr);
}
std::wstring addr_str = is_mod_addr ? std::format(L"{}+{:X}", module_path.filename().c_str(), address - module_base) : std::format(L"{:X}", address);
DWORD64 displacement;
if (std::wstring symbol; get_sym_from_addr(address, displacement, symbol))
return std::format(L"{}\t({})", addr_str, displacement != 0 ? std::format(L"{}+0x{:X}", symbol, displacement) : std::format(L"{}", symbol));
return value != 0 ? std::format(L"{} [{}]", addr_str, to_address_string(value, false)) : addr_str;
}
void print_exception_info(HANDLE hThread, const EXCEPTION_POINTERS& ex, const CONTEXT& ctx, std::wostringstream& log) {
std::vector<EXCEPTION_RECORD> exRecs;
if (ex.ExceptionRecord) {
size_t rec_index = 0;
size_t read;
exRecs.emplace_back();
for (auto pRemoteExRec = ex.ExceptionRecord;
pRemoteExRec
&& rec_index < 64
&& ReadProcessMemory(g_hProcess, pRemoteExRec, &exRecs.back(), sizeof exRecs.back(), &read)
&& read >= offsetof(EXCEPTION_RECORD, ExceptionInformation)
&& read >= static_cast<size_t>(reinterpret_cast<const char*>(&exRecs.back().ExceptionInformation[exRecs.back().NumberParameters]) - reinterpret_cast<const char*>(&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<size_t>(exRecs.back().ExceptionAddress));
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{";
STACKFRAME64 sf{};
sf.AddrPC.Offset = ctx.Rip;
sf.AddrPC.Mode = AddrModeFlat;
sf.AddrStack.Offset = ctx.Rsp;
sf.AddrStack.Mode = AddrModeFlat;
sf.AddrFrame.Offset = ctx.Rbp;
sf.AddrFrame.Mode = AddrModeFlat;
int frame_index = 0;
log << std::format(L"\n [{}]\t{}", frame_index++, to_address_string(sf.AddrPC.Offset, false));
const auto appendContextToLog = [&](const CONTEXT& ctxWalk) {
log << std::format(L"\n [{}]\t{}", frame_index++, to_address_string(sf.AddrPC.Offset, false));
};
const auto tryStackWalk = [&] {
__try {
CONTEXT ctxWalk = ctx;
do {
if (!StackWalk64(IMAGE_FILE_MACHINE_AMD64, g_hProcess, hThread, &sf, &ctxWalk, nullptr, &SymFunctionTableAccess64, &SymGetModuleBase64, nullptr))
break;
appendContextToLog(ctxWalk);
} while (sf.AddrReturn.Offset != 0 && sf.AddrPC.Offset != sf.AddrReturn.Offset);
return true;
} __except(EXCEPTION_EXECUTE_HANDLER) {
return false;
}
};
if (!tryStackWalk())
log << L"\n Access violation while walking up the stack.";
log << L"\n}\n";
}
void print_exception_info_extended(const EXCEPTION_POINTERS& ex, const CONTEXT& ctx, std::wostringstream& log)
{
log << L"\nRegisters\n{";
log << std::format(L"\n RAX:\t{}", to_address_string(ctx.Rax));
log << std::format(L"\n RBX:\t{}", to_address_string(ctx.Rbx));
log << std::format(L"\n RCX:\t{}", to_address_string(ctx.Rcx));
log << std::format(L"\n RDX:\t{}", to_address_string(ctx.Rdx));
log << std::format(L"\n R8:\t{}", to_address_string(ctx.R8));
log << std::format(L"\n R9:\t{}", to_address_string(ctx.R9));
log << std::format(L"\n R10:\t{}", to_address_string(ctx.R10));
log << std::format(L"\n R11:\t{}", to_address_string(ctx.R11));
log << std::format(L"\n R12:\t{}", to_address_string(ctx.R12));
log << std::format(L"\n R13:\t{}", to_address_string(ctx.R13));
log << std::format(L"\n R14:\t{}", to_address_string(ctx.R14));
log << std::format(L"\n R15:\t{}", to_address_string(ctx.R15));
log << std::format(L"\n RSI:\t{}", to_address_string(ctx.Rsi));
log << std::format(L"\n RDI:\t{}", to_address_string(ctx.Rdi));
log << std::format(L"\n RBP:\t{}", to_address_string(ctx.Rbp));
log << std::format(L"\n RSP:\t{}", to_address_string(ctx.Rsp));
log << std::format(L"\n RIP:\t{}", to_address_string(ctx.Rip));
log << L"\n}" << std::endl;
if(0x10000 < ctx.Rsp && ctx.Rsp < 0x7FFFFFFE0000)
{
log << L"\nStack\n{";
DWORD64 stackData[16];
size_t read;
ReadProcessMemory(g_hProcess, reinterpret_cast<void*>(ctx.Rsp), stackData, sizeof stackData, &read);
for(DWORD64 i = 0; i < 16 && i * sizeof(size_t) < read; i++)
log << std::format(L"\n [RSP+{:X}]\t{}", i * 8, to_address_string(stackData[i]));
log << L"\n}\n";
}
log << L"\nModules\n{";
for (const auto& [hModule, path] : get_remote_module_paths())
log << std::format(L"\n {:08X}\t{}", reinterpret_cast<DWORD64>(hModule), path.wstring());
log << L"\n}\n";
}
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);
} else {
res.push_back(L'"');
for (auto it = arg.begin(); ; ++it) {
size_t bsCount = 0;
while (it != arg.end() && *it == L'\\') {
++it;
++bsCount;
}
if (it == arg.end()) {
res.append(bsCount * 2, L'\\');
break;
} else if (*it == L'"') {
res.append(bsCount * 2 + 1, L'\\');
res.push_back(*it);
} else {
res.append(bsCount, L'\\');
res.push_back(*it);
}
}
res.push_back(L'"');
}
return res;
}
enum {
IdRadioRestartNormal = 101,
IdRadioRestartWithout3pPlugins,
IdRadioRestartWithoutPlugins,
IdRadioRestartWithoutDalamud,
IdButtonRestart = 201,
IdButtonHelp = IDHELP,
IdButtonExit = IDCANCEL,
};
void restart_game_using_injector(int nRadioButton, const std::vector<std::wstring>& launcherArgs)
{
std::wstring pathStr(PATHCCH_MAX_CCH, L'\0');
pathStr.resize(GetModuleFileNameExW(GetCurrentProcess(), GetModuleHandleW(nullptr), &pathStr[0], PATHCCH_MAX_CCH));
std::vector<std::wstring> args;
args.emplace_back((std::filesystem::path(pathStr).parent_path() / L"Dalamud.Injector.exe").wstring());
args.emplace_back(L"launch");
switch (nRadioButton) {
case IdRadioRestartWithout3pPlugins:
args.emplace_back(L"--no-3rd-plugin");
break;
case IdRadioRestartWithoutPlugins:
args.emplace_back(L"--no-plugin");
break;
case IdRadioRestartWithoutDalamud:
args.emplace_back(L"--without-dalamud");
break;
}
args.emplace_back(L"--");
args.insert(args.end(), launcherArgs.begin(), launcherArgs.end());
std::wstring argstr;
for (const auto& arg : args) {
argstr.append(escape_shell_arg(arg));
argstr.push_back(L' ');
}
argstr.pop_back();
STARTUPINFOW si{};
si.cb = sizeof si;
si.dwFlags = STARTF_USESHOWWINDOW;
#ifndef NDEBUG
si.wShowWindow = SW_HIDE;
#else
si.wShowWindow = SW_SHOW;
#endif
PROCESS_INFORMATION pi{};
if (CreateProcessW(args[0].c_str(), &argstr[0], nullptr, nullptr, FALSE, 0, nullptr, nullptr, &si, &pi)) {
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
} else {
MessageBoxW(nullptr, std::format(L"Failed to restart: 0x{:x}", GetLastError()).c_str(), L"Dalamud Boot", MB_ICONERROR | MB_OK);
}
}
int main() {
enum crash_handler_special_exit_codes {
InvalidParameter = -101,
ProcessExitedUnknownExitCode = -102,
};
HANDLE hPipeRead = nullptr;
std::filesystem::path assetDir, logDir;
std::optional<std::vector<std::wstring>> launcherArgs;
std::vector<std::wstring> args;
if (int argc = 0; const auto argv = CommandLineToArgvW(GetCommandLineW(), &argc)) {
for (auto i = 0; i < argc; i++)
args.emplace_back(argv[i]);
LocalFree(argv);
}
for (size_t i = 1; i < args.size(); i++) {
const auto arg = std::wstring_view(args[i]);
if (launcherArgs) {
launcherArgs->emplace_back(arg);
} else if (constexpr wchar_t pwszArgPrefix[] = L"--process-handle="; arg.starts_with(pwszArgPrefix)) {
g_hProcess = reinterpret_cast<HANDLE>(std::wcstoull(&arg[ARRAYSIZE(pwszArgPrefix) - 1], nullptr, 0));
} else if (constexpr wchar_t pwszArgPrefix[] = L"--exception-info-pipe-read-handle="; arg.starts_with(pwszArgPrefix)) {
hPipeRead = reinterpret_cast<HANDLE>(std::wcstoull(&arg[ARRAYSIZE(pwszArgPrefix) - 1], nullptr, 0));
} else if (constexpr wchar_t pwszArgPrefix[] = L"--asset-directory="; arg.starts_with(pwszArgPrefix)) {
assetDir = arg.substr(ARRAYSIZE(pwszArgPrefix) - 1);
} else if (constexpr wchar_t pwszArgPrefix[] = L"--log-directory="; arg.starts_with(pwszArgPrefix)) {
logDir = arg.substr(ARRAYSIZE(pwszArgPrefix) - 1);
} else if (arg == L"--") {
launcherArgs.emplace();
} else {
std::wcerr << L"Invalid argument: " << arg << std::endl;
return InvalidParameter;
}
}
if (g_hProcess == nullptr) {
std::wcerr << L"Target process not specified" << std::endl;
return InvalidParameter;
}
if (hPipeRead == nullptr) {
std::wcerr << L"Read pipe handle not specified" << std::endl;
return InvalidParameter;
}
const auto dwProcessId = GetProcessId(g_hProcess);
if (!dwProcessId){
std::wcerr << L"Target process not specified" << std::endl;
return InvalidParameter;
}
while (true) {
std::cout << "Waiting for crash...\n";
exception_info exinfo;
if (DWORD exsize{}; !ReadFile(hPipeRead, &exinfo, static_cast<DWORD>(sizeof exinfo), &exsize, nullptr) || exsize != sizeof exinfo) {
if (WaitForSingleObject(g_hProcess, 0) == WAIT_OBJECT_0) {
auto excode = static_cast<DWORD>(ProcessExitedUnknownExitCode);
if (!GetExitCodeProcess(g_hProcess, &excode))
std::cerr << std::format("Process exited, but failed to read exit code; error: 0x{:x}", GetLastError()) << std::endl;
else
std::cout << std::format("Process exited with exit code {0} (0x{0:x})", excode) << std::endl;
break;
}
const auto err = GetLastError();
std::cerr << std::format("Failed to read exception information; error: 0x{:x}", err) << std::endl;
std::cerr << "Terminating target process." << std::endl;
TerminateProcess(g_hProcess, -1);
break;
}
if (exinfo.ExceptionRecord.ExceptionCode == 0x12345678) {
std::cout << "Restart requested" << std::endl;
TerminateProcess(g_hProcess, 0);
restart_game_using_injector(IdRadioRestartNormal, *launcherArgs);
break;
}
std::cout << "Crash triggered" << std::endl;
if (g_bSymbolsAvailable) {
SymRefreshModuleList(g_hProcess);
} else if (g_bSymbolsAvailable = SymInitialize(g_hProcess, nullptr, true); g_bSymbolsAvailable) {
if (!assetDir.empty()) {
if (!SymSetSearchPathW(g_hProcess, std::format(L".;{}", (assetDir / "UIRes" / "pdb").wstring()).c_str()))
std::wcerr << std::format(L"SymSetSearchPathW error: 0x{:x}", GetLastError()) << std::endl;
}
} else {
std::wcerr << std::format(L"SymInitialize error: 0x{:x}", GetLastError()) << std::endl;
}
std::wstring stackTrace(exinfo.dwStackTraceLength, L'\0');
if (exinfo.dwStackTraceLength) {
if (DWORD read; !ReadFile(hPipeRead, &stackTrace[0], 2 * exinfo.dwStackTraceLength, &read, nullptr)) {
std::cout << std::format("Failed to read supplied stack trace: error 0x{:x}", GetLastError()) << std::endl;
}
}
SYSTEMTIME st;
GetLocalTime(&st);
const auto dumpPath = logDir.empty() ? std::filesystem::path() : logDir / std::format("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("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;
if (dumpPath.empty()) {
std::cout << "Skipping dump path, as log directory has not been specified" << std::endl;
} else {
MINIDUMP_EXCEPTION_INFORMATION mdmp_info{};
mdmp_info.ThreadId = GetThreadId(exinfo.hThreadHandle);
mdmp_info.ExceptionPointers = exinfo.pExceptionPointers;
mdmp_info.ClientPointers = TRUE;
do {
const auto hDumpFile = CreateFileW(dumpPath.c_str(), GENERIC_READ | GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, 0, nullptr);
if (hDumpFile == INVALID_HANDLE_VALUE) {
std::wcerr << (dumpError = std::format(L"CreateFileW({}, GENERIC_READ | GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, 0, nullptr) error: 0x{:x}", dumpPath.wstring(), GetLastError())) << std::endl;
break;
}
std::unique_ptr<std::remove_pointer_t<HANDLE>, decltype(&CloseHandle)> hDumpFilePtr(hDumpFile, &CloseHandle);
if (!MiniDumpWriteDump(g_hProcess, dwProcessId, hDumpFile, static_cast<MINIDUMP_TYPE>(MiniDumpWithDataSegs | MiniDumpWithModuleHeaders), &mdmp_info, nullptr, nullptr)) {
std::wcerr << (dumpError = std::format(L"MiniDumpWriteDump(0x{:x}, {}, 0x{:x}({}), MiniDumpWithFullMemory, ..., nullptr, nullptr) error: 0x{:x}", reinterpret_cast<size_t>(g_hProcess), dwProcessId, reinterpret_cast<size_t>(hDumpFile), dumpPath.wstring(), GetLastError())) << std::endl;
break;
}
std::wcout << "Dump written to path: " << dumpPath << std::endl;
} while (false);
}
std::wostringstream log;
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 (dumpPath.empty())
log << L"Dump skipped" << std::endl;
else if (dumpError.empty())
log << std::format(L"Dump at: {}", dumpPath.wstring()) << std::endl;
else
log << std::format(L"Dump error: {}", dumpError) << std::endl;
log << L"Time: " << std::chrono::zoned_time{ std::chrono::current_zone(), std::chrono::system_clock::now() } << std::endl;
log << L"\n" << stackTrace << std::endl;
SymRefreshModuleList(GetCurrentProcess());
print_exception_info(exinfo.hThreadHandle, exinfo.ExceptionPointers, exinfo.ContextRecord, log);
auto window_log_str = log.str();
print_exception_info_extended(exinfo.ExceptionPointers, exinfo.ContextRecord, log);
std::wofstream(logPath) << log.str();
std::thread submitThread;
if (!getenv("DALAMUD_NO_METRIC")) {
auto url = std::format(L"/Dalamud/Metric/ReportCrash/?lt={}&code={:x}", exinfo.nLifetime, exinfo.ExceptionRecord.ExceptionCode);
submitThread = std::thread([url = std::move(url)] {
const auto hInternet = WinHttpOpen(L"DALAMUDCRASHHANDLER", WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY, nullptr, nullptr, WINHTTP_FLAG_SECURE_DEFAULTS);
const auto hConnect = !hInternet ? nullptr : WinHttpConnect(hInternet, L"kamori.goats.dev", INTERNET_DEFAULT_HTTPS_PORT, 0);
const auto hRequest = !hConnect ? nullptr : WinHttpOpenRequest(hConnect, L"GET", url.c_str(), nullptr, nullptr, nullptr, 0);
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 (hRequest) WinHttpCloseHandle(hRequest);
if (hConnect) WinHttpCloseHandle(hConnect);
if (hInternet) WinHttpCloseHandle(hInternet);
});
}
TASKDIALOGCONFIG config = { 0 };
const TASKDIALOG_BUTTON radios[]{
{IdRadioRestartNormal, L"Restart"},
{IdRadioRestartWithout3pPlugins, L"Restart without 3rd party plugins"},
{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."},
{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.pszMainIcon = MAKEINTRESOURCE(IDI_ICON1);
config.pszMainInstruction = L"An error occurred";
config.pszContent = (L""
R"aa(This may be caused by a faulty plugin, a broken TexTools modification, any other third-party tool, or simply a bug in the game.)aa" "\n"
"\n"
R"aa(Try running integrity check in the XIVLauncher settings, and disabling plugins you don't need.)aa"
);
config.pButtons = buttons;
config.cButtons = ARRAYSIZE(buttons);
config.nDefaultButton = IdButtonRestart;
config.pszExpandedInformation = window_log_str.c_str();
config.pszWindowTitle = L"Dalamud Error";
config.pRadioButtons = radios;
config.cRadioButtons = ARRAYSIZE(radios);
config.nDefaultRadioButton = IdRadioRestartNormal;
config.cxWidth = 300;
config.pszFooter = (L""
R"aa(<a href="help">Help</a> | <a href="logdir">Open log directory</a> | <a href="logfile">Open log file</a> | <a href="resume">Attempt to resume</a>)aa"
);
// Can't do this, xiv stops pumping messages here
//config.hwndParent = FindWindowA("FFXIVGAME", NULL);
auto attemptResume = false;
const auto callback = [&](HWND hwnd, UINT uNotification, WPARAM wParam, LPARAM lParam) -> HRESULT {
switch (uNotification) {
case TDN_CREATED:
{
SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW);
return S_OK;
}
case TDN_HYPERLINK_CLICKED:
{
const auto link = std::wstring_view(reinterpret_cast<const wchar_t*>(lParam));
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);
} else if (link == L"logfile") {
ShellExecuteW(hwnd, nullptr, logPath.c_str(), nullptr, nullptr, SW_SHOW);
} else if (link == L"resume") {
attemptResume = true;
DestroyWindow(hwnd);
}
return S_OK;
}
}
return S_OK;
};
config.pfCallback = [](HWND hwnd, UINT uNotification, WPARAM wParam, LPARAM lParam, LONG_PTR dwRefData) {
return (*reinterpret_cast<decltype(callback)*>(dwRefData))(hwnd, uNotification, wParam, lParam);
};
config.lpCallbackData = reinterpret_cast<LONG_PTR>(&callback);
if (submitThread.joinable()) {
submitThread.join();
submitThread = {};
}
int nButtonPressed = 0, nRadioButton = 0;
if (FAILED(TaskDialogIndirect(&config, &nButtonPressed, &nRadioButton, nullptr))) {
ResumeThread(exinfo.hThreadHandle);
} else {
switch (nButtonPressed) {
case IdButtonRestart:
{
TerminateProcess(g_hProcess, exinfo.ExceptionRecord.ExceptionCode);
restart_game_using_injector(nRadioButton, *launcherArgs);
break;
}
default:
if (attemptResume)
ResumeThread(exinfo.hThreadHandle);
else
TerminateProcess(g_hProcess, exinfo.ExceptionRecord.ExceptionCode);
}
}
}
return 0;
}

View file

@ -0,0 +1,71 @@
// Microsoft Visual C++ generated resource script.
//
#include "resource.h"
#define APSTUDIO_READONLY_SYMBOLS
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 2 resource.
//
#include "winres.h"
/////////////////////////////////////////////////////////////////////////////
#undef APSTUDIO_READONLY_SYMBOLS
/////////////////////////////////////////////////////////////////////////////
// English (United Kingdom) resources
#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENG)
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_UK
#pragma code_page(1252)
#ifdef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// TEXTINCLUDE
//
1 TEXTINCLUDE
BEGIN
"resource.h\0"
END
2 TEXTINCLUDE
BEGIN
"#include ""winres.h""\r\n"
"\0"
END
3 TEXTINCLUDE
BEGIN
"\r\n"
"\0"
END
#endif // APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// Icon
//
// Icon with lowest ID value placed first to ensure application icon
// remains consistent on all systems.
IDI_ICON1 ICON "dalamud.ico"
#endif // English (United Kingdom) resources
/////////////////////////////////////////////////////////////////////////////
#ifndef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 3 resource.
//
/////////////////////////////////////////////////////////////////////////////
#endif // not APSTUDIO_INVOKED

View file

@ -0,0 +1,94 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Label="Globals">
<ProjectGuid>{317a264c-920b-44a1-8a34-f3a6827b0705}</ProjectGuid>
<RootNamespace>DalamudCrashHandler</RootNamespace>
<Configuration Condition=" '$(Configuration)'=='' ">Debug</Configuration>
<Platform>x64</Platform>
</PropertyGroup>
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>16.0</VCProjectVersion>
<Keyword>Win32Proj</Keyword>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<LinkIncremental>false</LinkIncremental>
<CharacterSet>Unicode</CharacterSet>
<OutDir>..\bin\$(Configuration)\</OutDir>
<IntDir>obj\$(Configuration)\</IntDir>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<UseDebugLibraries>true</UseDebugLibraries>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<UseDebugLibraries>false</UseDebugLibraries>
</PropertyGroup>
<ItemDefinitionGroup>
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_CRT_SECURE_NO_WARNINGS;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<LanguageStandard>stdcpplatest</LanguageStandard>
<LanguageStandard_C>stdc17</LanguageStandard_C>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<AdditionalDependencies>Winhttp.lib;Dbghelp.lib;$(CoreLibraryDependencies);%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)'=='Debug'">
<ClCompile>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>false</IntrinsicFunctions>
<PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ClCompile>
<Link>
<EnableCOMDATFolding>false</EnableCOMDATFolding>
<OptimizeReferences>false</OptimizeReferences>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)'=='Release'">
<ClCompile>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ClCompile>
<Link>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="DalamudCrashHandler.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\Dalamud.Boot\crashhandler_shared.h" />
<ClInclude Include="resource.h" />
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="DalamudCrashHandler.rc" />
</ItemGroup>
<ItemGroup>
<Image Include="dalamud.ico" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
</Project>

View file

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="Source Files">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
<Filter Include="Header Files">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
<Extensions>h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd</Extensions>
</Filter>
<Filter Include="Resource Files">
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
</Filter>
</ItemGroup>
<ItemGroup>
<ClCompile Include="DalamudCrashHandler.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\Dalamud.Boot\crashhandler_shared.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="resource.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="DalamudCrashHandler.rc">
<Filter>Resource Files</Filter>
</ResourceCompile>
</ItemGroup>
<ItemGroup>
<Image Include="dalamud.ico">
<Filter>Resource Files</Filter>
</Image>
</ItemGroup>
</Project>

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

View file

@ -0,0 +1,16 @@
//{{NO_DEPENDENCIES}}
// Microsoft Visual C++ generated include file.
// Used by DalamudCrashHandler.rc
//
#define IDI_ICON1 101
// Next default values for new objects
//
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE 102
#define _APS_NEXT_COMMAND_VALUE 40001
#define _APS_NEXT_CONTROL_VALUE 1001
#define _APS_NEXT_SYMED_VALUE 101
#endif
#endif

View file

@ -31,6 +31,9 @@ public class DalamudBuild : NukeBuild
AbsolutePath DalamudBootProjectDir => RootDirectory / "Dalamud.Boot";
AbsolutePath DalamudBootProjectFile => DalamudBootProjectDir / "Dalamud.Boot.vcxproj";
AbsolutePath DalamudCrashHandlerProjectDir => RootDirectory / "DalamudCrashHandler";
AbsolutePath DalamudCrashHandlerProjectFile => DalamudCrashHandlerProjectDir / "DalamudCrashHandler.vcxproj";
AbsolutePath InjectorProjectDir => RootDirectory / "Dalamud.Injector";
AbsolutePath InjectorProjectFile => InjectorProjectDir / "Dalamud.Injector.csproj";
@ -71,6 +74,14 @@ public class DalamudBuild : NukeBuild
.SetTargetPath(DalamudBootProjectFile)
.SetConfiguration(Configuration));
});
Target CompileDalamudCrashHandler => _ => _
.Executes(() =>
{
MSBuildTasks.MSBuild(s => s
.SetTargetPath(DalamudCrashHandlerProjectFile)
.SetConfiguration(Configuration));
});
Target CompileInjector => _ => _
.DependsOn(Restore)
@ -93,6 +104,7 @@ public class DalamudBuild : NukeBuild
Target Compile => _ => _
.DependsOn(CompileDalamud)
.DependsOn(CompileDalamudBoot)
.DependsOn(CompileDalamudCrashHandler)
.DependsOn(CompileInjector)
.DependsOn(CompileInjectorBoot);
@ -117,6 +129,11 @@ public class DalamudBuild : NukeBuild
.SetProjectFile(DalamudBootProjectFile)
.SetConfiguration(Configuration)
.SetTargets("Clean"));
MSBuildTasks.MSBuild(s => s
.SetProjectFile(DalamudCrashHandlerProjectFile)
.SetConfiguration(Configuration)
.SetTargets("Clean"));
DotNetTasks.DotNetClean(s => s
.SetProject(InjectorProjectFile)

7
global.json Normal file
View file

@ -0,0 +1,7 @@
{
"sdk": {
"version": "6.0.0",
"rollForward": "latestMajor",
"allowPrerelease": true
}
}

View file

@ -41,9 +41,10 @@ int InitializeClrAndGetEntryPoint(
int result;
SetEnvironmentVariable(L"DOTNET_MULTILEVEL_LOOKUP", L"0");
//SetEnvironmentVariable(L"COMPlus_legacyCorruptedStateExceptionsPolicy", L"1");
SetEnvironmentVariable(L"COMPlus_legacyCorruptedStateExceptionsPolicy", L"1");
SetEnvironmentVariable(L"DOTNET_legacyCorruptedStateExceptionsPolicy", L"1");
SetEnvironmentVariable(L"COMPLUS_ForceENC", L"1");
SetEnvironmentVariable(L"DOTNET_ForceENC", L"1");
// Enable Dynamic PGO
SetEnvironmentVariable(L"DOTNET_TieredPGO", L"1");

@ -1 +1 @@
Subproject commit 61af16050cffccf587e3f4c74e623b81edbac44d
Subproject commit f8268ce6057b7aae34fc0af4b630defdc5b070f5

@ -1 +1 @@
Subproject commit 1e4c06b36013efb367c8c57c7f724a30f67a3973
Subproject commit b92b51eb4f8f677a11cb076f147977876d45fb6f