Merge pull request #1641 from goatcorp/net8-rollup

[net8] Rollup changes from master
This commit is contained in:
KazWolfe 2024-03-02 08:48:12 -08:00 committed by GitHub
commit 324806341c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
99 changed files with 6308 additions and 1218 deletions

View file

@ -32,6 +32,9 @@
<IntDir>obj\$(Configuration)\</IntDir>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
<Import Project="$(VCTargetsPath)\BuildCustomizations\masm.props" />
</ImportGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<UseDebugLibraries>true</UseDebugLibraries>
<LibraryPath>$(SolutionDir)bin\lib\$(Configuration)\libMinHook\;$(VC_LibraryPath_x64);$(WindowsSDK_LibraryPath_x64)</LibraryPath>
@ -55,7 +58,7 @@
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<EnableUAC>false</EnableUAC>
<AdditionalDependencies>Version.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalDependencies>Version.lib;Shlwapi.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalLibraryDirectories>..\lib\CoreCLR;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
</Link>
</ItemDefinitionGroup>
@ -70,6 +73,7 @@
<Link>
<EnableCOMDATFolding>false</EnableCOMDATFolding>
<OptimizeReferences>false</OptimizeReferences>
<ModuleDefinitionFile Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">module.def</ModuleDefinitionFile>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)'=='Release'">
@ -83,9 +87,13 @@
<Link>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<ModuleDefinitionFile Condition="'$(Configuration)|$(Platform)'=='Release|x64'">module.def</ModuleDefinitionFile>
</Link>
</ItemDefinitionGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
<Import Project="$(VCTargetsPath)\BuildCustomizations\masm.targets" />
</ImportGroup>
<ItemGroup>
<Content Include="..\lib\CoreCLR\nethost\nethost.dll">
<Link>nethost.dll</Link>
@ -129,6 +137,7 @@
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
</ClCompile>
<ClCompile Include="ntdll.cpp" />
<ClCompile Include="unicode.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
@ -168,6 +177,7 @@
<ClInclude Include="DalamudStartInfo.h" />
<ClInclude Include="hooks.h" />
<ClInclude Include="logging.h" />
<ClInclude Include="ntdll.h" />
<ClInclude Include="resource.h" />
<ClInclude Include="unicode.h" />
<ClInclude Include="utils.h" />
@ -181,8 +191,14 @@
<ItemGroup>
<Image Include="dalamud.ico" />
</ItemGroup>
<ItemGroup>
<MASM Include="rewrite_entrypoint_thunks.asm" />
</ItemGroup>
<ItemGroup>
<None Include="module.def" />
</ItemGroup>
<Target Name="RemoveExtraFiles" AfterTargets="PostBuildEvent">
<Delete Files="$(OutDir)$(TargetName).lib" />
<Delete Files="$(OutDir)$(TargetName).exp" />
</Target>
</Project>
</Project>

View file

@ -73,6 +73,9 @@
<ClCompile Include="DalamudStartInfo.cpp">
<Filter>Dalamud.Boot DLL</Filter>
</ClCompile>
<ClCompile Include="ntdll.cpp">
<Filter>Dalamud.Boot DLL</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\lib\CoreCLR\CoreCLR.h">
@ -140,6 +143,9 @@
</ClInclude>
<ClInclude Include="resource.h" />
<ClInclude Include="crashhandler_shared.h" />
<ClInclude Include="ntdll.h">
<Filter>Dalamud.Boot DLL</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="Dalamud.Boot.rc" />
@ -147,4 +153,14 @@
<ItemGroup>
<Image Include="dalamud.ico" />
</ItemGroup>
<ItemGroup>
<MASM Include="rewrite_entrypoint_thunks.asm">
<Filter>Dalamud.Boot DLL</Filter>
</MASM>
</ItemGroup>
<ItemGroup>
<None Include="module.def">
<Filter>Dalamud.Boot DLL</Filter>
</None>
</ItemGroup>
</Project>

View file

@ -89,13 +89,16 @@ void from_json(const nlohmann::json& json, DalamudStartInfo& config) {
config.DalamudLoadMethod = json.value("LoadMethod", config.DalamudLoadMethod);
config.WorkingDirectory = json.value("WorkingDirectory", config.WorkingDirectory);
config.ConfigurationPath = json.value("ConfigurationPath", config.ConfigurationPath);
config.LogPath = json.value("LogPath", config.LogPath);
config.LogName = json.value("LogName", config.LogName);
config.PluginDirectory = json.value("PluginDirectory", config.PluginDirectory);
config.DefaultPluginDirectory = json.value("DefaultPluginDirectory", config.DefaultPluginDirectory);
config.AssetDirectory = json.value("AssetDirectory", config.AssetDirectory);
config.Language = json.value("Language", config.Language);
config.GameVersion = json.value("GameVersion", config.GameVersion);
config.DelayInitializeMs = json.value("DelayInitializeMs", config.DelayInitializeMs);
config.TroubleshootingPackData = json.value("TroubleshootingPackData", std::string{});
config.DelayInitializeMs = json.value("DelayInitializeMs", config.DelayInitializeMs);
config.NoLoadPlugins = json.value("NoLoadPlugins", config.NoLoadPlugins);
config.NoLoadThirdPartyPlugins = json.value("NoLoadThirdPartyPlugins", config.NoLoadThirdPartyPlugins);
config.BootLogPath = json.value("BootLogPath", config.BootLogPath);
config.BootShowConsole = json.value("BootShowConsole", config.BootShowConsole);

View file

@ -35,13 +35,16 @@ struct DalamudStartInfo {
LoadMethod DalamudLoadMethod = LoadMethod::Entrypoint;
std::string WorkingDirectory;
std::string ConfigurationPath;
std::string LogPath;
std::string LogName;
std::string PluginDirectory;
std::string DefaultPluginDirectory;
std::string AssetDirectory;
ClientLanguage Language = ClientLanguage::English;
std::string GameVersion;
int DelayInitializeMs = 0;
std::string TroubleshootingPackData;
int DelayInitializeMs = 0;
bool NoLoadPlugins;
bool NoLoadThirdPartyPlugins;
std::string BootLogPath;
bool BootShowConsole = false;

View file

@ -9,7 +9,7 @@
HMODULE g_hModule;
HINSTANCE g_hGameInstance = GetModuleHandleW(nullptr);
DWORD WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) {
HRESULT WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) {
g_startInfo.from_envvars();
std::string jsonParseError;
@ -114,7 +114,7 @@ DWORD WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) {
logging::I("Calling InitializeClrAndGetEntryPoint");
void* entrypoint_vfn;
int result = InitializeClrAndGetEntryPoint(
const auto result = InitializeClrAndGetEntryPoint(
g_hModule,
g_startInfo.BootEnableEtw,
runtimeconfig_path,
@ -124,7 +124,7 @@ DWORD WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) {
L"Dalamud.EntryPoint+InitDelegate, Dalamud",
&entrypoint_vfn);
if (result != 0)
if (FAILED(result))
return result;
using custom_component_entry_point_fn = void (CORECLR_DELEGATE_CALLTYPE*)(LPVOID, HANDLE);
@ -156,10 +156,10 @@ DWORD WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) {
entrypoint_fn(lpParam, hMainThreadContinue);
logging::I("Done!");
return 0;
return S_OK;
}
DllExport DWORD WINAPI Initialize(LPVOID lpParam) {
extern "C" DWORD WINAPI Initialize(LPVOID lpParam) {
return InitializeImpl(lpParam, CreateEvent(nullptr, TRUE, FALSE, nullptr));
}

View file

@ -2,39 +2,9 @@
#include "hooks.h"
#include "ntdll.h"
#include "logging.h"
enum {
LDR_DLL_NOTIFICATION_REASON_LOADED = 1,
LDR_DLL_NOTIFICATION_REASON_UNLOADED = 2,
};
struct LDR_DLL_UNLOADED_NOTIFICATION_DATA {
ULONG Flags; //Reserved.
const UNICODE_STRING* FullDllName; //The full path name of the DLL module.
const UNICODE_STRING* BaseDllName; //The base file name of the DLL module.
PVOID DllBase; //A pointer to the base address for the DLL in memory.
ULONG SizeOfImage; //The size of the DLL image, in bytes.
};
struct LDR_DLL_LOADED_NOTIFICATION_DATA {
ULONG Flags; //Reserved.
const UNICODE_STRING* FullDllName; //The full path name of the DLL module.
const UNICODE_STRING* BaseDllName; //The base file name of the DLL module.
PVOID DllBase; //A pointer to the base address for the DLL in memory.
ULONG SizeOfImage; //The size of the DLL image, in bytes.
};
union LDR_DLL_NOTIFICATION_DATA {
LDR_DLL_LOADED_NOTIFICATION_DATA Loaded;
LDR_DLL_UNLOADED_NOTIFICATION_DATA Unloaded;
};
using PLDR_DLL_NOTIFICATION_FUNCTION = VOID CALLBACK(_In_ ULONG NotificationReason, _In_ const LDR_DLL_NOTIFICATION_DATA* NotificationData, _In_opt_ PVOID Context);
static const auto LdrRegisterDllNotification = utils::loaded_module(GetModuleHandleW(L"ntdll.dll")).get_exported_function<NTSTATUS(NTAPI)(ULONG Flags, PLDR_DLL_NOTIFICATION_FUNCTION NotificationFunction, PVOID Context, PVOID* Cookie)>("LdrRegisterDllNotification");
static const auto LdrUnregisterDllNotification = utils::loaded_module(GetModuleHandleW(L"ntdll.dll")).get_exported_function<NTSTATUS(NTAPI)(PVOID Cookie)>("LdrUnregisterDllNotification");
hooks::getprocaddress_singleton_import_hook::getprocaddress_singleton_import_hook()
: m_pfnGetProcAddress(GetProcAddress)
, m_thunk("kernel32!GetProcAddress(Singleton Import Hook)",

View file

@ -1,6 +1,5 @@
#pragma once
#include <limits>
#include <map>
#include "utils.h"

5
Dalamud.Boot/module.def Normal file
View file

@ -0,0 +1,5 @@
LIBRARY Dalamud.Boot
EXPORTS
Initialize @1
RewriteRemoteEntryPointW @2
RewrittenEntryPoint @3

15
Dalamud.Boot/ntdll.cpp Normal file
View file

@ -0,0 +1,15 @@
#include "pch.h"
#include "ntdll.h"
#include "utils.h"
NTSTATUS LdrRegisterDllNotification(ULONG Flags, PLDR_DLL_NOTIFICATION_FUNCTION NotificationFunction, PVOID Context, PVOID* Cookie) {
static const auto pfn = utils::loaded_module(GetModuleHandleW(L"ntdll.dll")).get_exported_function<NTSTATUS(NTAPI)(ULONG Flags, PLDR_DLL_NOTIFICATION_FUNCTION NotificationFunction, PVOID Context, PVOID* Cookie)>("LdrRegisterDllNotification");
return pfn(Flags, NotificationFunction, Context, Cookie);
}
NTSTATUS LdrUnregisterDllNotification(PVOID Cookie) {
static const auto pfn = utils::loaded_module(GetModuleHandleW(L"ntdll.dll")).get_exported_function<NTSTATUS(NTAPI)(PVOID Cookie)>("LdrUnregisterDllNotification");
return pfn(Cookie);
}

33
Dalamud.Boot/ntdll.h Normal file
View file

@ -0,0 +1,33 @@
#pragma once
// ntdll exports
enum {
LDR_DLL_NOTIFICATION_REASON_LOADED = 1,
LDR_DLL_NOTIFICATION_REASON_UNLOADED = 2,
};
struct LDR_DLL_UNLOADED_NOTIFICATION_DATA {
ULONG Flags; //Reserved.
const UNICODE_STRING* FullDllName; //The full path name of the DLL module.
const UNICODE_STRING* BaseDllName; //The base file name of the DLL module.
PVOID DllBase; //A pointer to the base address for the DLL in memory.
ULONG SizeOfImage; //The size of the DLL image, in bytes.
};
struct LDR_DLL_LOADED_NOTIFICATION_DATA {
ULONG Flags; //Reserved.
const UNICODE_STRING* FullDllName; //The full path name of the DLL module.
const UNICODE_STRING* BaseDllName; //The base file name of the DLL module.
PVOID DllBase; //A pointer to the base address for the DLL in memory.
ULONG SizeOfImage; //The size of the DLL image, in bytes.
};
union LDR_DLL_NOTIFICATION_DATA {
LDR_DLL_LOADED_NOTIFICATION_DATA Loaded;
LDR_DLL_UNLOADED_NOTIFICATION_DATA Unloaded;
};
using PLDR_DLL_NOTIFICATION_FUNCTION = VOID CALLBACK(_In_ ULONG NotificationReason, _In_ const LDR_DLL_NOTIFICATION_DATA* NotificationData, _In_opt_ PVOID Context);
NTSTATUS LdrRegisterDllNotification(ULONG Flags, PLDR_DLL_NOTIFICATION_FUNCTION NotificationFunction, PVOID Context, PVOID* Cookie);
NTSTATUS LdrUnregisterDllNotification(PVOID Cookie);

View file

@ -15,18 +15,28 @@
#include <Windows.h>
// Windows Header Files (2)
#include <DbgHelp.h>
#include <Dbt.h>
#include <dwmapi.h>
#include <iphlpapi.h>
#include <PathCch.h>
#include <Psapi.h>
#include <ShlObj.h>
#include <Shlwapi.h>
#include <SubAuth.h>
#include <TlHelp32.h>
// Windows Header Files (3)
#include <icmpapi.h> // Must be loaded after iphlpapi.h
// MSVC Compiler Intrinsic
#include <intrin.h>
// COM
#include <comdef.h>
// C++ Standard Libraries
#include <algorithm>
#include <cassert>
#include <chrono>
#include <cstdio>
@ -61,9 +71,6 @@
#include "unicode.h"
// Commonly used macros
#define DllExport extern "C" __declspec(dllexport)
// Global variables
extern HMODULE g_hModule;
extern HINSTANCE g_hGameInstance;

View file

@ -1,115 +1,92 @@
#include "pch.h"
#include "logging.h"
#include "utils.h"
DWORD WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue);
HRESULT WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue);
struct RewrittenEntryPointParameters {
void* pAllocation;
char* pEntrypoint;
char* pEntrypointBytes;
size_t entrypointLength;
char* pLoadInfo;
HANDLE hMainThread;
HANDLE hMainThreadContinue;
};
#pragma pack(push, 1)
struct EntryPointThunkTemplate {
struct DUMMYSTRUCTNAME {
struct {
const uint8_t op_mov_rdi[2]{ 0x48, 0xbf };
void* ptr = nullptr;
} fn;
namespace thunks {
constexpr uint64_t Terminator = 0xCCCCCCCCCCCCCCCCu;
constexpr uint64_t Placeholder = 0x0606060606060606u;
extern "C" void EntryPointReplacement();
extern "C" void RewrittenEntryPoint_Standalone();
const uint8_t op_call_rdi[2]{ 0xff, 0xd7 };
} CallTrampoline;
};
void* resolve_thunk_address(void (*pfn)()) {
const auto ptr = reinterpret_cast<uint8_t*>(pfn);
if (*ptr == 0xe9)
return ptr + 5 + *reinterpret_cast<int32_t*>(ptr + 1);
return ptr;
}
struct TrampolineTemplate {
const struct {
const uint8_t op_sub_rsp_imm[3]{ 0x48, 0x81, 0xec };
const uint32_t length = 0x80;
} stack_alloc;
size_t get_thunk_length(void (*pfn)()) {
size_t length = 0;
for (auto ptr = reinterpret_cast<char*>(resolve_thunk_address(pfn)); *reinterpret_cast<uint64_t*>(ptr) != Terminator; ptr++)
length++;
return length;
}
struct DUMMYSTRUCTNAME {
struct {
const uint8_t op_mov_rcx_imm[2]{ 0x48, 0xb9 };
void* val = nullptr;
} lpLibFileName;
template<typename T>
void* fill_placeholders(void* pfn, const T& value) {
auto ptr = static_cast<char*>(pfn);
struct {
const uint8_t op_mov_rdi_imm[2]{ 0x48, 0xbf };
decltype(&LoadLibraryW) ptr = nullptr;
} fn;
while (*reinterpret_cast<uint64_t*>(ptr) != Placeholder)
ptr++;
const uint8_t op_call_rdi[2]{ 0xff, 0xd7 };
} CallLoadLibrary_nethost;
*reinterpret_cast<uint64_t*>(ptr) = 0;
*reinterpret_cast<T*>(ptr) = value;
return ptr + sizeof(value);
}
struct DUMMYSTRUCTNAME {
struct {
const uint8_t op_mov_rcx_imm[2]{ 0x48, 0xb9 };
void* val = nullptr;
} lpLibFileName;
template<typename T, typename...TArgs>
void* fill_placeholders(void* ptr, const T& value, TArgs&&...more_values) {
return fill_placeholders(fill_placeholders(ptr, value), std::forward<TArgs>(more_values)...);
}
struct {
const uint8_t op_mov_rdi_imm[2]{ 0x48, 0xbf };
decltype(&LoadLibraryW) ptr = nullptr;
} fn;
std::vector<char> create_entrypointreplacement() {
std::vector<char> buf(get_thunk_length(&EntryPointReplacement));
memcpy(buf.data(), resolve_thunk_address(&EntryPointReplacement), buf.size());
return buf;
}
const uint8_t op_call_rdi[2]{ 0xff, 0xd7 };
} CallLoadLibrary_DalamudBoot;
std::vector<char> create_standalone_rewrittenentrypoint(const std::filesystem::path& dalamud_path) {
const auto nethost_path = std::filesystem::path(dalamud_path).replace_filename(L"nethost.dll");
struct {
const uint8_t hModule_op_mov_rcx_rax[3]{ 0x48, 0x89, 0xc1 };
// These are null terminated, since pointers are returned from .c_str()
const auto dalamud_path_wview = std::wstring_view(dalamud_path.c_str());
const auto nethost_path_wview = std::wstring_view(nethost_path.c_str());
struct {
const uint8_t op_mov_rdx_imm[2]{ 0x48, 0xba };
void* val = nullptr;
} lpProcName;
// +2 is for null terminator
const auto dalamud_path_view = std::span(reinterpret_cast<const char*>(dalamud_path_wview.data()), dalamud_path_wview.size() * 2 + 2);
const auto nethost_path_view = std::span(reinterpret_cast<const char*>(nethost_path_wview.data()), nethost_path_wview.size() * 2 + 2);
struct {
const uint8_t op_mov_rdi_imm[2]{ 0x48, 0xbf };
decltype(&GetProcAddress) ptr = nullptr;
} fn;
std::vector<char> buffer;
const auto thunk_template_length = thunks::get_thunk_length(&thunks::RewrittenEntryPoint_Standalone);
buffer.reserve(thunk_template_length + dalamud_path_view.size() + nethost_path_view.size());
buffer.resize(thunk_template_length);
memcpy(buffer.data(), resolve_thunk_address(&thunks::RewrittenEntryPoint_Standalone), thunk_template_length);
const uint8_t op_call_rdi[2]{ 0xff, 0xd7 };
} CallGetProcAddress;
// &::GetProcAddress will return Dalamud.dll's import table entry.
// GetProcAddress(..., "GetProcAddress") returns the address inside kernel32.dll.
const auto kernel32 = GetModuleHandleA("kernel32.dll");
struct {
const uint8_t op_add_rsp_imm[3]{ 0x48, 0x81, 0xc4 };
const uint32_t length = 0x80;
} stack_release;
struct DUMMYSTRUCTNAME2 {
// rdi := returned value from GetProcAddress
const uint8_t op_mov_rdi_rax[3]{ 0x48, 0x89, 0xc7 };
// rax := return address
const uint8_t op_pop_rax[1]{ 0x58 };
// rax := rax - sizeof thunk (last instruction must be call)
struct {
const uint8_t op_sub_rax_imm4[2]{ 0x48, 0x2d };
const uint32_t displacement = static_cast<uint32_t>(sizeof EntryPointThunkTemplate);
} op_sub_rax_to_entry_point;
struct {
const uint8_t op_mov_rcx_imm[2]{ 0x48, 0xb9 };
void* val = nullptr;
} param;
const uint8_t op_push_rax[1]{ 0x50 };
const uint8_t op_jmp_rdi[2]{ 0xff, 0xe7 };
} CallInjectEntryPoint;
const char buf_CallGetProcAddress_lpProcName[20] = "RewrittenEntryPoint";
uint8_t buf_EntryPointBackup[sizeof EntryPointThunkTemplate]{};
#pragma pack(push, 8)
RewrittenEntryPointParameters parameters{};
#pragma pack(pop)
};
#pragma pack(pop)
thunks::fill_placeholders(buffer.data(),
/* pfnLoadLibraryW = */ GetProcAddress(kernel32, "LoadLibraryW"),
/* pfnGetProcAddress = */ GetProcAddress(kernel32, "GetProcAddress"),
/* pRewrittenEntryPointParameters = */ Placeholder,
/* nNethostOffset = */ 0,
/* nDalamudOffset = */ nethost_path_view.size_bytes()
);
buffer.insert(buffer.end(), nethost_path_view.begin(), nethost_path_view.end());
buffer.insert(buffer.end(), dalamud_path_view.begin(), dalamud_path_view.end());
return buffer;
}
}
void read_process_memory_or_throw(HANDLE hProcess, void* pAddress, void* data, size_t len) {
SIZE_T read = 0;
@ -126,6 +103,7 @@ void read_process_memory_or_throw(HANDLE hProcess, void* pAddress, T& data) {
void write_process_memory_or_throw(HANDLE hProcess, void* pAddress, const void* data, size_t len) {
SIZE_T written = 0;
const utils::memory_tenderizer tenderizer(hProcess, pAddress, len, PAGE_EXECUTE_READWRITE);
if (!WriteProcessMemory(hProcess, pAddress, data, len, &written))
throw std::runtime_error("WriteProcessMemory failure");
if (written != len)
@ -170,10 +148,17 @@ void* get_mapped_image_base_address(HANDLE hProcess, const std::filesystem::path
exe.read(reinterpret_cast<char*>(&exe_section_headers[0]), sizeof IMAGE_SECTION_HEADER * exe_section_headers.size());
if (!exe)
throw std::runtime_error("Game executable is corrupt (Truncated section header).");
SYSTEM_INFO sysinfo;
GetSystemInfo(&sysinfo);
for (MEMORY_BASIC_INFORMATION mbi{};
VirtualQueryEx(hProcess, mbi.BaseAddress, &mbi, sizeof mbi);
mbi.BaseAddress = static_cast<char*>(mbi.BaseAddress) + mbi.RegionSize) {
// wine: apparently there exists a RegionSize of 0xFFF
mbi.RegionSize = (mbi.RegionSize + sysinfo.dwPageSize - 1) / sysinfo.dwPageSize * sysinfo.dwPageSize;
if (!(mbi.State & MEM_COMMIT) || mbi.Type != MEM_IMAGE)
continue;
@ -241,31 +226,22 @@ void* get_mapped_image_base_address(HANDLE hProcess, const std::filesystem::path
throw std::runtime_error("corresponding base address not found");
}
std::string from_utf16(const std::wstring& wstr, UINT codePage = CP_UTF8) {
std::string str(WideCharToMultiByte(codePage, 0, &wstr[0], static_cast<int>(wstr.size()), nullptr, 0, nullptr, nullptr), 0);
WideCharToMultiByte(codePage, 0, &wstr[0], static_cast<int>(wstr.size()), &str[0], static_cast<int>(str.size()), nullptr, nullptr);
return str;
}
std::wstring to_utf16(const std::string& str, UINT codePage = CP_UTF8, bool errorOnInvalidChars = false) {
std::wstring wstr(MultiByteToWideChar(codePage, 0, &str[0], static_cast<int>(str.size()), nullptr, 0), 0);
MultiByteToWideChar(codePage, errorOnInvalidChars ? MB_ERR_INVALID_CHARS : 0, &str[0], static_cast<int>(str.size()), &wstr[0], static_cast<int>(wstr.size()));
return wstr;
}
/// @brief Rewrite target process' entry point so that this DLL can be loaded and executed first.
/// @param hProcess Process handle.
/// @param pcwzPath Path to target process.
/// @param pcszLoadInfo JSON string to be passed to Initialize.
/// @return 0 if successful; nonzero if unsuccessful
/// @param pcwzLoadInfo JSON string to be passed to Initialize.
/// @return null if successful; memory containing wide string allocated via GlobalAlloc if unsuccessful
///
/// When the process has just been started up via CreateProcess (CREATE_SUSPENDED), GetModuleFileName and alikes result in an error.
/// Instead, we have to enumerate through all the files mapped into target process' virtual address space and find the base address
/// of memory region corresponding to the path given.
///
DllExport DWORD WINAPI RewriteRemoteEntryPointW(HANDLE hProcess, const wchar_t* pcwzPath, const wchar_t* pcwzLoadInfo) {
extern "C" HRESULT WINAPI RewriteRemoteEntryPointW(HANDLE hProcess, const wchar_t* pcwzPath, const wchar_t* pcwzLoadInfo) {
std::wstring last_operation;
SetLastError(ERROR_SUCCESS);
try {
const auto base_address = reinterpret_cast<char*>(get_mapped_image_base_address(hProcess, pcwzPath));
last_operation = L"get_mapped_image_base_address";
const auto base_address = static_cast<char*>(get_mapped_image_base_address(hProcess, pcwzPath));
IMAGE_DOS_HEADER dos_header{};
union {
@ -273,112 +249,150 @@ DllExport DWORD WINAPI RewriteRemoteEntryPointW(HANDLE hProcess, const wchar_t*
IMAGE_NT_HEADERS64 nt_header64{};
};
last_operation = L"read_process_memory_or_throw(base_address)";
read_process_memory_or_throw(hProcess, base_address, dos_header);
last_operation = L"read_process_memory_or_throw(base_address + dos_header.e_lfanew)";
read_process_memory_or_throw(hProcess, base_address + dos_header.e_lfanew, nt_header64);
const auto entrypoint = base_address + (nt_header32.OptionalHeader.Magic == IMAGE_NT_OPTIONAL_HDR32_MAGIC
? nt_header32.OptionalHeader.AddressOfEntryPoint
: nt_header64.OptionalHeader.AddressOfEntryPoint);
auto path = get_path_from_local_module(g_hModule).wstring();
path.resize(path.size() + 1); // ensure null termination
auto path_bytes = std::span(reinterpret_cast<const char*>(&path[0]), std::span(path).size_bytes());
last_operation = L"get_path_from_local_module(g_hModule)";
auto local_module_path = get_path_from_local_module(g_hModule);
last_operation = L"thunks::create_standalone_rewrittenentrypoint(local_module_path)";
auto standalone_rewrittenentrypoint = thunks::create_standalone_rewrittenentrypoint(local_module_path);
auto nethost_path = (get_path_from_local_module(g_hModule).parent_path() / L"nethost.dll").wstring();
nethost_path.resize(nethost_path.size() + 1); // ensure null termination
auto nethost_path_bytes = std::span(reinterpret_cast<const char*>(&nethost_path[0]), std::span(nethost_path).size_bytes());
last_operation = L"thunks::create_entrypointreplacement()";
auto entrypoint_replacement = thunks::create_entrypointreplacement();
auto load_info = from_utf16(pcwzLoadInfo);
last_operation = L"unicode::convert<std::string>(pcwzLoadInfo)";
auto load_info = unicode::convert<std::string>(pcwzLoadInfo);
load_info.resize(load_info.size() + 1); //ensure null termination
// Allocate full buffer in advance to keep reference to trampoline valid.
std::vector<uint8_t> buffer(sizeof TrampolineTemplate + load_info.size() + nethost_path_bytes.size() + path_bytes.size());
auto& trampoline = *reinterpret_cast<TrampolineTemplate*>(&buffer[0]);
const auto load_info_buffer = std::span(buffer).subspan(sizeof trampoline, load_info.size());
const auto nethost_path_buffer = std::span(buffer).subspan(sizeof trampoline + load_info.size(), nethost_path_bytes.size());
const auto dalamud_path_buffer = std::span(buffer).subspan(sizeof trampoline + load_info.size() + nethost_path_bytes.size(), path_bytes.size());
new(&trampoline)TrampolineTemplate(); // this line initializes given buffer instead of allocating memory
memcpy(&load_info_buffer[0], &load_info[0], load_info_buffer.size());
memcpy(&nethost_path_buffer[0], &nethost_path_bytes[0], nethost_path_buffer.size());
memcpy(&dalamud_path_buffer[0], &path_bytes[0], dalamud_path_buffer.size());
// Backup remote process' original entry point.
read_process_memory_or_throw(hProcess, entrypoint, trampoline.buf_EntryPointBackup);
const auto bufferSize = sizeof(RewrittenEntryPointParameters) + entrypoint_replacement.size() + load_info.size() + standalone_rewrittenentrypoint.size();
last_operation = std::format(L"std::vector alloc({}b)", bufferSize);
std::vector<uint8_t> buffer(bufferSize);
// Allocate buffer in remote process, which will be used to fill addresses in the local buffer.
const auto remote_buffer = reinterpret_cast<char*>(VirtualAllocEx(hProcess, nullptr, buffer.size(), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE));
// Fill the values to be used in RewrittenEntryPoint
trampoline.parameters = {
.pAllocation = remote_buffer,
.pEntrypoint = entrypoint,
.pEntrypointBytes = remote_buffer + offsetof(TrampolineTemplate, buf_EntryPointBackup),
.entrypointLength = sizeof trampoline.buf_EntryPointBackup,
.pLoadInfo = remote_buffer + (&load_info_buffer[0] - &buffer[0]),
};
last_operation = std::format(L"VirtualAllocEx({}b)", bufferSize);
const auto remote_buffer = static_cast<char*>(VirtualAllocEx(hProcess, nullptr, buffer.size(), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE));
auto& params = *reinterpret_cast<RewrittenEntryPointParameters*>(buffer.data());
params.entrypointLength = entrypoint_replacement.size();
params.pEntrypoint = entrypoint;
// Fill the addresses referred in machine code.
trampoline.CallLoadLibrary_nethost.lpLibFileName.val = remote_buffer + (&nethost_path_buffer[0] - &buffer[0]);
trampoline.CallLoadLibrary_nethost.fn.ptr = LoadLibraryW;
trampoline.CallLoadLibrary_DalamudBoot.lpLibFileName.val = remote_buffer + (&dalamud_path_buffer[0] - &buffer[0]);
trampoline.CallLoadLibrary_DalamudBoot.fn.ptr = LoadLibraryW;
trampoline.CallGetProcAddress.lpProcName.val = remote_buffer + offsetof(TrampolineTemplate, buf_CallGetProcAddress_lpProcName);
trampoline.CallGetProcAddress.fn.ptr = GetProcAddress;
trampoline.CallInjectEntryPoint.param.val = remote_buffer + offsetof(TrampolineTemplate, parameters);
// Backup original entry point.
last_operation = std::format(L"read_process_memory_or_throw(entrypoint, {}b)", entrypoint_replacement.size());
read_process_memory_or_throw(hProcess, entrypoint, &buffer[sizeof params], entrypoint_replacement.size());
memcpy(&buffer[sizeof params + entrypoint_replacement.size()], load_info.data(), load_info.size());
last_operation = L"thunks::fill_placeholders(EntryPointReplacement)";
thunks::fill_placeholders(standalone_rewrittenentrypoint.data(), remote_buffer);
memcpy(&buffer[sizeof params + entrypoint_replacement.size() + load_info.size()], standalone_rewrittenentrypoint.data(), standalone_rewrittenentrypoint.size());
// Write the local buffer into the buffer in remote process.
last_operation = std::format(L"write_process_memory_or_throw(remote_buffer, {}b)", buffer.size());
write_process_memory_or_throw(hProcess, remote_buffer, buffer.data(), buffer.size());
// Overwrite remote process' entry point with a thunk that immediately calls our trampoline function.
EntryPointThunkTemplate thunk{};
thunk.CallTrampoline.fn.ptr = remote_buffer;
write_process_memory_or_throw(hProcess, entrypoint, thunk);
last_operation = L"thunks::fill_placeholders(RewrittenEntryPoint_Standalone::pRewrittenEntryPointParameters)";
thunks::fill_placeholders(entrypoint_replacement.data(), remote_buffer + sizeof params + entrypoint_replacement.size() + load_info.size());
return 0;
// Overwrite remote process' entry point with a thunk that will load our DLLs and call our trampoline function.
last_operation = std::format(L"write_process_memory_or_throw(entrypoint={:X}, {}b)", reinterpret_cast<uintptr_t>(entrypoint), buffer.size());
write_process_memory_or_throw(hProcess, entrypoint, entrypoint_replacement.data(), entrypoint_replacement.size());
FlushInstructionCache(hProcess, entrypoint, entrypoint_replacement.size());
return S_OK;
} catch (const std::exception& e) {
OutputDebugStringA(std::format("RewriteRemoteEntryPoint failure: {} (GetLastError: {})\n", e.what(), GetLastError()).c_str());
return 1;
}
}
const auto err = GetLastError();
const auto hr = err == ERROR_SUCCESS ? E_FAIL : HRESULT_FROM_WIN32(err);
auto formatted = std::format(
L"{}: {} ({})",
last_operation,
unicode::convert<std::wstring>(e.what()),
utils::format_win32_error(err));
OutputDebugStringW((formatted + L"\r\n").c_str());
/// @deprecated
DllExport DWORD WINAPI RewriteRemoteEntryPoint(HANDLE hProcess, const wchar_t* pcwzPath, const char* pcszLoadInfo) {
return RewriteRemoteEntryPointW(hProcess, pcwzPath, to_utf16(pcszLoadInfo).c_str());
ICreateErrorInfoPtr cei;
if (FAILED(CreateErrorInfo(&cei)))
return hr;
if (FAILED(cei->SetSource(const_cast<LPOLESTR>(L"Dalamud.Boot"))))
return hr;
if (FAILED(cei->SetDescription(const_cast<LPOLESTR>(formatted.c_str()))))
return hr;
IErrorInfoPtr ei;
if (FAILED(cei.QueryInterface(IID_PPV_ARGS(&ei))))
return hr;
(void)SetErrorInfo(0, ei);
return hr;
}
}
/// @brief Entry point function "called" instead of game's original main entry point.
/// @param params Parameters set up from RewriteRemoteEntryPoint.
DllExport void WINAPI RewrittenEntryPoint(RewrittenEntryPointParameters& params) {
params.hMainThreadContinue = CreateEventW(nullptr, true, false, nullptr);
if (!params.hMainThreadContinue)
ExitProcess(-1);
extern "C" void WINAPI RewrittenEntryPoint_AdjustedStack(RewrittenEntryPointParameters & params) {
HANDLE hMainThreadContinue = nullptr;
auto hr = S_OK;
std::wstring last_operation;
std::wstring exc_msg;
SetLastError(ERROR_SUCCESS);
// Do whatever the work in a separate thread to minimize the stack usage at this context,
// as this function really should have been a naked procedure but __declspec(naked) isn't supported in x64 version of msvc.
params.hMainThread = CreateThread(nullptr, 0, [](void* p) -> DWORD {
try {
std::string loadInfo;
auto& params = *reinterpret_cast<RewrittenEntryPointParameters*>(p);
{
// Restore original entry point.
// Use WriteProcessMemory instead of memcpy to avoid having to fiddle with VirtualProtect.
write_process_memory_or_throw(GetCurrentProcess(), params.pEntrypoint, params.pEntrypointBytes, params.entrypointLength);
try {
const auto pOriginalEntryPointBytes = reinterpret_cast<char*>(&params) + sizeof(params);
const auto pLoadInfo = pOriginalEntryPointBytes + params.entrypointLength;
// Make a copy of load info, as the whole params will be freed after this code block.
loadInfo = params.pLoadInfo;
}
// Restore original entry point.
// Use WriteProcessMemory instead of memcpy to avoid having to fiddle with VirtualProtect.
last_operation = L"restore original entry point";
write_process_memory_or_throw(GetCurrentProcess(), params.pEntrypoint, pOriginalEntryPointBytes, params.entrypointLength);
FlushInstructionCache(GetCurrentProcess(), params.pEntrypoint, params.entrypointLength);
InitializeImpl(&loadInfo[0], params.hMainThreadContinue);
return 0;
} catch (const std::exception& e) {
MessageBoxA(nullptr, std::format("Failed to load Dalamud.\n\nError: {}", e.what()).c_str(), "Dalamud.Boot", MB_OK | MB_ICONERROR);
ExitProcess(-1);
hMainThreadContinue = CreateEventW(nullptr, true, false, nullptr);
last_operation = L"hMainThreadContinue = CreateEventW";
if (!hMainThreadContinue)
throw std::runtime_error("CreateEventW");
last_operation = L"InitializeImpl";
hr = InitializeImpl(pLoadInfo, hMainThreadContinue);
} catch (const std::exception& e) {
if (hr == S_OK) {
const auto err = GetLastError();
hr = err == ERROR_SUCCESS ? E_FAIL : HRESULT_FROM_WIN32(err);
}
}, &params, 0, nullptr);
if (!params.hMainThread)
ExitProcess(-1);
CloseHandle(params.hMainThread);
WaitForSingleObject(params.hMainThreadContinue, INFINITE);
VirtualFree(params.pAllocation, 0, MEM_RELEASE);
ICreateErrorInfoPtr cei;
IErrorInfoPtr ei;
if (SUCCEEDED(CreateErrorInfo(&cei))
&& SUCCEEDED(cei->SetDescription(const_cast<wchar_t*>(unicode::convert<std::wstring>(e.what()).c_str())))
&& SUCCEEDED(cei.QueryInterface(IID_PPV_ARGS(&ei)))) {
(void)SetErrorInfo(0, ei);
}
}
if (FAILED(hr)) {
const _com_error err(hr);
auto desc = err.Description();
if (desc.length() == 0)
desc = err.ErrorMessage();
if (MessageBoxW(nullptr, std::format(
L"Failed to load Dalamud. Load game without Dalamud(yes) or abort(no)?\n\n{}\n{}",
last_operation,
desc.GetBSTR()).c_str(),
L"Dalamud.Boot", MB_OK | MB_YESNO) == IDNO)
ExitProcess(-1);
if (hMainThreadContinue) {
CloseHandle(hMainThreadContinue);
hMainThreadContinue = nullptr;
}
}
if (hMainThreadContinue)
WaitForSingleObject(hMainThreadContinue, INFINITE);
VirtualFree(&params, 0, MEM_RELEASE);
}

View file

@ -0,0 +1,82 @@
PUBLIC EntryPointReplacement
PUBLIC RewrittenEntryPoint_Standalone
PUBLIC RewrittenEntryPoint
; 06 and 07 are invalid opcodes
; CC is int3 = bp
; using 0CCCCCCCCCCCCCCCCh as function terminator
; using 00606060606060606h as placeholders
TERMINATOR = 0CCCCCCCCCCCCCCCCh
PLACEHOLDER = 00606060606060606h
.code
EntryPointReplacement PROC
start:
; rsp % 0x10 = 0x08
lea rax, [start]
push rax
; rsp % 0x10 = 0x00
mov rax, PLACEHOLDER
; this calls RewrittenEntryPoint_Standalone
jmp rax
dq TERMINATOR
EntryPointReplacement ENDP
RewrittenEntryPoint_Standalone PROC
start:
; stack is aligned to 0x10; see above
sub rsp, 20h
lea rcx, [embeddedData]
add rcx, qword ptr [nNethostOffset]
call qword ptr [pfnLoadLibraryW]
lea rcx, [embeddedData]
add rcx, qword ptr [nDalamudOffset]
call qword ptr [pfnLoadLibraryW]
mov rcx, rax
lea rdx, [pcszEntryPointName]
call qword ptr [pfnGetProcAddress]
mov rcx, qword ptr [pRewrittenEntryPointParameters]
; this calls RewrittenEntryPoint
jmp rax
pfnLoadLibraryW:
dq PLACEHOLDER
pfnGetProcAddress:
dq PLACEHOLDER
pRewrittenEntryPointParameters:
dq PLACEHOLDER
nNethostOffset:
dq PLACEHOLDER
nDalamudOffset:
dq PLACEHOLDER
pcszEntryPointName:
db "RewrittenEntryPoint", 0
embeddedData:
dq TERMINATOR
RewrittenEntryPoint_Standalone ENDP
EXTERN RewrittenEntryPoint_AdjustedStack :PROC
RewrittenEntryPoint PROC
; stack is aligned to 0x10; see above
call RewrittenEntryPoint_AdjustedStack
add rsp, 20h
ret
RewrittenEntryPoint ENDP
END

View file

@ -408,14 +408,20 @@ utils::signature_finder::result utils::signature_finder::find_one() const {
return find(1, 1, false).front();
}
utils::memory_tenderizer::memory_tenderizer(const void* pAddress, size_t length, DWORD dwNewProtect) : m_data(reinterpret_cast<char*>(const_cast<void*>(pAddress)), length) {
utils::memory_tenderizer::memory_tenderizer(const void* pAddress, size_t length, DWORD dwNewProtect)
: memory_tenderizer(GetCurrentProcess(), pAddress, length, dwNewProtect) {
}
utils::memory_tenderizer::memory_tenderizer(HANDLE hProcess, const void* pAddress, size_t length, DWORD dwNewProtect)
: m_process(hProcess)
, m_data(static_cast<char*>(const_cast<void*>(pAddress)), length) {
try {
for (auto pCoveredAddress = &m_data[0];
pCoveredAddress < &m_data[0] + m_data.size();
pCoveredAddress = reinterpret_cast<char*>(m_regions.back().BaseAddress) + m_regions.back().RegionSize) {
for (auto pCoveredAddress = m_data.data();
pCoveredAddress < m_data.data() + m_data.size();
pCoveredAddress = static_cast<char*>(m_regions.back().BaseAddress) + m_regions.back().RegionSize) {
MEMORY_BASIC_INFORMATION region{};
if (!VirtualQuery(pCoveredAddress, &region, sizeof region)) {
if (!VirtualQueryEx(hProcess, pCoveredAddress, &region, sizeof region)) {
throw std::runtime_error(std::format(
"VirtualQuery(addr=0x{:X}, ..., cb={}) failed with Win32 code 0x{:X}",
reinterpret_cast<size_t>(pCoveredAddress),
@ -423,7 +429,7 @@ utils::memory_tenderizer::memory_tenderizer(const void* pAddress, size_t length,
GetLastError()));
}
if (!VirtualProtect(region.BaseAddress, region.RegionSize, dwNewProtect, &region.Protect)) {
if (!VirtualProtectEx(hProcess, region.BaseAddress, region.RegionSize, dwNewProtect, &region.Protect)) {
throw std::runtime_error(std::format(
"(Change)VirtualProtect(addr=0x{:X}, size=0x{:X}, ..., ...) failed with Win32 code 0x{:X}",
reinterpret_cast<size_t>(region.BaseAddress),
@ -436,7 +442,7 @@ utils::memory_tenderizer::memory_tenderizer(const void* pAddress, size_t length,
} catch (...) {
for (auto& region : std::ranges::reverse_view(m_regions)) {
if (!VirtualProtect(region.BaseAddress, region.RegionSize, region.Protect, &region.Protect)) {
if (!VirtualProtectEx(hProcess, region.BaseAddress, region.RegionSize, region.Protect, &region.Protect)) {
// Could not restore; fast fail
__fastfail(GetLastError());
}
@ -448,7 +454,7 @@ utils::memory_tenderizer::memory_tenderizer(const void* pAddress, size_t length,
utils::memory_tenderizer::~memory_tenderizer() {
for (auto& region : std::ranges::reverse_view(m_regions)) {
if (!VirtualProtect(region.BaseAddress, region.RegionSize, region.Protect, &region.Protect)) {
if (!VirtualProtectEx(m_process, region.BaseAddress, region.RegionSize, region.Protect, &region.Protect)) {
// Could not restore; fast fail
__fastfail(GetLastError());
}
@ -578,16 +584,6 @@ std::vector<std::string> utils::get_env_list(const wchar_t* pcszName) {
return res;
}
std::wstring utils::to_wstring(const std::string& str) {
if (str.empty()) return std::wstring();
size_t convertedChars = 0;
size_t newStrSize = str.size() + 1;
std::wstring wstr(newStrSize, L'\0');
mbstowcs_s(&convertedChars, &wstr[0], newStrSize, str.c_str(), _TRUNCATE);
wstr.resize(convertedChars - 1);
return wstr;
}
std::filesystem::path utils::get_module_path(HMODULE hModule) {
std::wstring buf(MAX_PATH, L'\0');
while (true) {
@ -654,3 +650,25 @@ std::wstring utils::escape_shell_arg(const std::wstring& arg) {
}
return res;
}
std::wstring utils::format_win32_error(DWORD err) {
wchar_t* pwszMsg = nullptr;
FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER |
FORMAT_MESSAGE_FROM_SYSTEM |
FORMAT_MESSAGE_IGNORE_INSERTS,
nullptr,
err,
MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US),
reinterpret_cast<LPWSTR>(&pwszMsg),
0,
nullptr);
if (pwszMsg) {
std::wstring result = std::format(L"Win32 error ({}=0x{:X}): {}", err, err, pwszMsg);
while (!result.empty() && std::isspace(result.back()))
result.pop_back();
LocalFree(pwszMsg);
return result;
}
return std::format(L"Win32 error ({}=0x{:X})", err, err);
}

View file

@ -111,10 +111,13 @@ namespace utils {
};
class memory_tenderizer {
HANDLE m_process;
std::span<char> m_data;
std::vector<MEMORY_BASIC_INFORMATION> m_regions;
public:
memory_tenderizer(HANDLE hProcess, const void* pAddress, size_t length, DWORD dwNewProtect);
memory_tenderizer(const void* pAddress, size_t length, DWORD dwNewProtect);
template<typename T, typename = std::enable_if_t<std::is_trivial_v<T>&& std::is_standard_layout_v<T>>>
@ -264,8 +267,6 @@ namespace utils {
return get_env_list<T>(unicode::convert<std::wstring>(pcszName).c_str());
}
std::wstring to_wstring(const std::string& str);
std::filesystem::path get_module_path(HMODULE hModule);
/// @brief Find the game main window.
@ -275,4 +276,6 @@ namespace utils {
void wait_for_game_window();
std::wstring escape_shell_arg(const std::wstring& arg);
std::wstring format_win32_error(DWORD err);
}

View file

@ -6,6 +6,7 @@
#include "logging.h"
#include "utils.h"
#include "hooks.h"
#include "crashhandler_shared.h"
#include "DalamudStartInfo.h"
@ -24,6 +25,7 @@
PVOID g_veh_handle = nullptr;
bool g_veh_do_full_dump = false;
std::optional<hooks::import_hook<decltype(SetUnhandledExceptionFilter)>> g_HookSetUnhandledExceptionFilter;
HANDLE g_crashhandler_process = nullptr;
HANDLE g_crashhandler_event = nullptr;
@ -110,13 +112,16 @@ static void append_injector_launch_args(std::vector<std::wstring>& args)
case DalamudStartInfo::LoadMethod::DllInject:
args.emplace_back(L"--mode=inject");
}
args.emplace_back(L"--logpath=\"" + utils::to_wstring(g_startInfo.BootLogPath) + L"\"");
args.emplace_back(L"--dalamud-working-directory=\"" + utils::to_wstring(g_startInfo.WorkingDirectory) + L"\"");
args.emplace_back(L"--dalamud-configuration-path=\"" + utils::to_wstring(g_startInfo.ConfigurationPath) + L"\"");
args.emplace_back(L"--dalamud-plugin-directory=\"" + utils::to_wstring(g_startInfo.PluginDirectory) + L"\"");
args.emplace_back(L"--dalamud-asset-directory=\"" + utils::to_wstring(g_startInfo.AssetDirectory) + L"\"");
args.emplace_back(L"--dalamud-client-language=" + std::to_wstring(static_cast<int>(g_startInfo.Language)));
args.emplace_back(L"--dalamud-delay-initialize=" + std::to_wstring(g_startInfo.DelayInitializeMs));
args.emplace_back(L"--dalamud-working-directory=\"" + unicode::convert<std::wstring>(g_startInfo.WorkingDirectory) + L"\"");
args.emplace_back(L"--dalamud-configuration-path=\"" + unicode::convert<std::wstring>(g_startInfo.ConfigurationPath) + L"\"");
args.emplace_back(L"--logpath=\"" + unicode::convert<std::wstring>(g_startInfo.LogPath) + L"\"");
args.emplace_back(L"--logname=\"" + unicode::convert<std::wstring>(g_startInfo.LogName) + L"\"");
args.emplace_back(L"--dalamud-plugin-directory=\"" + unicode::convert<std::wstring>(g_startInfo.PluginDirectory) + L"\"");
args.emplace_back(L"--dalamud-asset-directory=\"" + unicode::convert<std::wstring>(g_startInfo.AssetDirectory) + L"\"");
args.emplace_back(std::format(L"--dalamud-client-language={}", static_cast<int>(g_startInfo.Language)));
args.emplace_back(std::format(L"--dalamud-delay-initialize={}", g_startInfo.DelayInitializeMs));
// NoLoadPlugins/NoLoadThirdPartyPlugins: supplied from DalamudCrashHandler
if (g_startInfo.BootShowConsole)
args.emplace_back(L"--console");
if (g_startInfo.BootEnableEtw)
@ -143,21 +148,7 @@ static void append_injector_launch_args(std::vector<std::wstring>& args)
LONG exception_handler(EXCEPTION_POINTERS* ex)
{
if (ex->ExceptionRecord->ExceptionCode == 0x12345678)
{
// pass
}
else
{
if (!is_whitelist_exception(ex->ExceptionRecord->ExceptionCode))
return EXCEPTION_CONTINUE_SEARCH;
if (!is_ffxiv_address(L"ffxiv_dx11.exe", ex->ContextRecord->Rip) &&
!is_ffxiv_address(L"cimgui.dll", ex->ContextRecord->Rip))
return EXCEPTION_CONTINUE_SEARCH;
}
// block any other exceptions hitting the veh while the messagebox is open
// block any other exceptions hitting the handler while the messagebox is open
const auto lock = std::lock_guard(g_exception_handler_mutex);
exception_info exinfo{};
@ -167,7 +158,7 @@ LONG exception_handler(EXCEPTION_POINTERS* ex)
exinfo.ExceptionRecord = ex->ExceptionRecord ? *ex->ExceptionRecord : EXCEPTION_RECORD{};
const auto time_now = std::chrono::system_clock::now();
auto lifetime = std::chrono::duration_cast<std::chrono::seconds>(
time_now.time_since_epoch()).count()
time_now.time_since_epoch()).count()
- std::chrono::duration_cast<std::chrono::seconds>(
g_time_start.time_since_epoch()).count();
exinfo.nLifetime = lifetime;
@ -175,10 +166,14 @@ LONG exception_handler(EXCEPTION_POINTERS* ex)
DuplicateHandle(GetCurrentProcess(), g_crashhandler_event, g_crashhandler_process, &exinfo.hEventHandle, 0, TRUE, DUPLICATE_SAME_ACCESS);
std::wstring stackTrace;
if (void* fn; const auto err = static_cast<DWORD>(g_clr->get_function_pointer(
if (!g_clr)
{
stackTrace = L"(no CLR stack trace available)";
}
else if (void* fn; const auto err = static_cast<DWORD>(g_clr->get_function_pointer(
L"Dalamud.EntryPoint, Dalamud",
L"VehCallback",
L"Dalamud.EntryPoint+VehDelegate, Dalamud",
L"Dalamud.EntryPoint+VehDelegate, Dalamud",
nullptr, nullptr, &fn)))
{
stackTrace = std::format(L"Failed to read stack trace: 0x{:08x}", err);
@ -188,17 +183,25 @@ LONG exception_handler(EXCEPTION_POINTERS* ex)
stackTrace = static_cast<wchar_t*(*)()>(fn)();
// Don't free it, as the program's going to be quit anyway
}
exinfo.dwStackTraceLength = static_cast<DWORD>(stackTrace.size());
exinfo.dwTroubleshootingPackDataLength = static_cast<DWORD>(g_startInfo.TroubleshootingPackData.size());
if (DWORD written; !WriteFile(g_crashhandler_pipe_write, &exinfo, static_cast<DWORD>(sizeof exinfo), &written, nullptr) || sizeof exinfo != written)
return EXCEPTION_CONTINUE_SEARCH;
if (DWORD written; !WriteFile(g_crashhandler_pipe_write, &stackTrace[0], static_cast<DWORD>(std::span(stackTrace).size_bytes()), &written, nullptr) || std::span(stackTrace).size_bytes() != written)
return EXCEPTION_CONTINUE_SEARCH;
if (const auto nb = static_cast<DWORD>(std::span(stackTrace).size_bytes()))
{
if (DWORD written; !WriteFile(g_crashhandler_pipe_write, stackTrace.data(), nb, &written, nullptr) || nb != written)
return EXCEPTION_CONTINUE_SEARCH;
}
if (DWORD written; !WriteFile(g_crashhandler_pipe_write, &g_startInfo.TroubleshootingPackData[0], static_cast<DWORD>(std::span(g_startInfo.TroubleshootingPackData).size_bytes()), &written, nullptr) || std::span(g_startInfo.TroubleshootingPackData).size_bytes() != written)
return EXCEPTION_CONTINUE_SEARCH;
if (const auto nb = static_cast<DWORD>(std::span(g_startInfo.TroubleshootingPackData).size_bytes()))
{
if (DWORD written; !WriteFile(g_crashhandler_pipe_write, g_startInfo.TroubleshootingPackData.data(), nb, &written, nullptr) || nb != written)
return EXCEPTION_CONTINUE_SEARCH;
}
AllowSetForegroundWindow(GetProcessId(g_crashhandler_process));
HANDLE waitHandles[] = { g_crashhandler_process, g_crashhandler_event };
DWORD waitResult = WaitForMultipleObjects(2, waitHandles, FALSE, INFINITE);
@ -217,13 +220,44 @@ LONG exception_handler(EXCEPTION_POINTERS* ex)
return EXCEPTION_CONTINUE_SEARCH;
}
LONG WINAPI structured_exception_handler(EXCEPTION_POINTERS* ex)
{
return exception_handler(ex);
}
LONG WINAPI vectored_exception_handler(EXCEPTION_POINTERS* ex)
{
if (ex->ExceptionRecord->ExceptionCode == 0x12345678)
{
// pass
}
else
{
if (!is_whitelist_exception(ex->ExceptionRecord->ExceptionCode))
return EXCEPTION_CONTINUE_SEARCH;
if (!is_ffxiv_address(L"ffxiv_dx11.exe", ex->ContextRecord->Rip) &&
!is_ffxiv_address(L"cimgui.dll", ex->ContextRecord->Rip))
return EXCEPTION_CONTINUE_SEARCH;
}
return exception_handler(ex);
}
bool veh::add_handler(bool doFullDump, const std::string& workingDirectory)
{
if (g_veh_handle)
return false;
g_veh_handle = AddVectoredExceptionHandler(1, exception_handler);
SetUnhandledExceptionFilter(nullptr);
g_veh_handle = AddVectoredExceptionHandler(TRUE, vectored_exception_handler);
g_HookSetUnhandledExceptionFilter.emplace("kernel32.dll!SetUnhandledExceptionFilter (lpTopLevelExceptionFilter)", "kernel32.dll", "SetUnhandledExceptionFilter", 0);
g_HookSetUnhandledExceptionFilter->set_detour([](LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter) -> LPTOP_LEVEL_EXCEPTION_FILTER
{
logging::I("Overwriting UnhandledExceptionFilter from {} to {}", reinterpret_cast<ULONG_PTR>(lpTopLevelExceptionFilter), reinterpret_cast<ULONG_PTR>(structured_exception_handler));
return g_HookSetUnhandledExceptionFilter->call_original(structured_exception_handler);
});
SetUnhandledExceptionFilter(structured_exception_handler);
g_veh_do_full_dump = doFullDump;
g_time_start = std::chrono::system_clock::now();
@ -355,6 +389,8 @@ bool veh::remove_handler()
if (g_veh_handle && RemoveVectoredExceptionHandler(g_veh_handle) != 0)
{
g_veh_handle = nullptr;
g_HookSetUnhandledExceptionFilter.reset();
SetUnhandledExceptionFilter(nullptr);
return true;
}
return false;

View file

@ -5,9 +5,8 @@
#include "DalamudStartInfo.h"
#include "hooks.h"
#include "logging.h"
#include "ntdll.h"
#include "utils.h"
#include <iphlpapi.h>
#include <icmpapi.h>
template<typename T>
static std::span<T> assume_nonempty_span(std::span<T> t, const char* descr) {
@ -513,50 +512,6 @@ void xivfixes::backup_userdata_save(bool bApply) {
}
}
void xivfixes::clr_failfast_hijack(bool bApply)
{
static const char* LogTag = "[xivfixes:clr_failfast_hijack]";
static std::optional<hooks::import_hook<decltype(RaiseFailFastException)>> s_HookClrFatalError;
static std::optional<hooks::import_hook<decltype(SetUnhandledExceptionFilter)>> s_HookSetUnhandledExceptionFilter;
if (bApply)
{
if (!g_startInfo.BootEnabledGameFixes.contains("clr_failfast_hijack")) {
logging::I("{} Turned off via environment variable.", LogTag);
return;
}
s_HookClrFatalError.emplace("kernel32.dll!RaiseFailFastException (import, backup_userdata_save)", "kernel32.dll", "RaiseFailFastException", 0);
s_HookSetUnhandledExceptionFilter.emplace("kernel32.dll!SetUnhandledExceptionFilter (lpTopLevelExceptionFilter)", "kernel32.dll", "SetUnhandledExceptionFilter", 0);
s_HookClrFatalError->set_detour([](PEXCEPTION_RECORD pExceptionRecord,
_In_opt_ PCONTEXT pContextRecord,
_In_ DWORD dwFlags)
{
MessageBoxW(nullptr, L"An error in a Dalamud plugin was detected and the game cannot continue.\n\nPlease take a screenshot of this error message and let us know about it.", L"Dalamud", MB_OK | MB_ICONERROR);
return s_HookClrFatalError->call_original(pExceptionRecord, pContextRecord, dwFlags);
});
s_HookSetUnhandledExceptionFilter->set_detour([](LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter) -> LPTOP_LEVEL_EXCEPTION_FILTER
{
logging::I("{} SetUnhandledExceptionFilter", LogTag);
return nullptr;
});
logging::I("{} Enable", LogTag);
}
else
{
if (s_HookClrFatalError) {
logging::I("{} Disable ClrFatalError", LogTag);
s_HookClrFatalError.reset();
s_HookSetUnhandledExceptionFilter.reset();
}
}
}
void xivfixes::prevent_icmphandle_crashes(bool bApply) {
static const char* LogTag = "[xivfixes:prevent_icmphandle_crashes]";
@ -590,6 +545,109 @@ void xivfixes::prevent_icmphandle_crashes(bool bApply) {
}
}
void xivfixes::symbol_load_patches(bool bApply) {
static const char* LogTag = "[xivfixes:symbol_load_patches]";
static std::optional<hooks::import_hook<decltype(SymInitialize)>> s_hookSymInitialize;
static PVOID s_dllNotificationCookie = nullptr;
static const auto RemoveFullPathPdbInfo = [](const utils::loaded_module& mod) {
const auto ddva = mod.data_directory(IMAGE_DIRECTORY_ENTRY_DEBUG).VirtualAddress;
if (!ddva)
return;
const auto& ddir = mod.ref_as<IMAGE_DEBUG_DIRECTORY>(ddva);
if (ddir.Type == IMAGE_DEBUG_TYPE_CODEVIEW) {
// The Visual C++ debug information.
// Ghidra calls it "DotNetPdbInfo".
static constexpr DWORD DotNetPdbInfoSignatureValue = 0x53445352;
struct DotNetPdbInfo {
DWORD Signature; // RSDS
GUID Guid;
DWORD Age;
char PdbPath[1];
};
const auto& pdbref = mod.ref_as<DotNetPdbInfo>(ddir.AddressOfRawData);
if (pdbref.Signature == DotNetPdbInfoSignatureValue) {
const auto pathSpan = std::string_view(pdbref.PdbPath, strlen(pdbref.PdbPath));
const auto pathWide = unicode::convert<std::wstring>(pathSpan);
std::wstring windowsDirectory(GetWindowsDirectoryW(nullptr, 0) + 1, L'\0');
windowsDirectory.resize(
GetWindowsDirectoryW(windowsDirectory.data(), static_cast<UINT>(windowsDirectory.size())));
if (!PathIsRelativeW(pathWide.c_str()) && !PathIsSameRootW(windowsDirectory.c_str(), pathWide.c_str())) {
utils::memory_tenderizer pathOverwrite(&pdbref.PdbPath, pathSpan.size(), PAGE_READWRITE);
auto sep = std::find(pathSpan.rbegin(), pathSpan.rend(), '/');
if (sep == pathSpan.rend())
sep = std::find(pathSpan.rbegin(), pathSpan.rend(), '\\');
if (sep != pathSpan.rend()) {
logging::I(
"{} Stripping pdb path folder: {} to {}",
LogTag,
pathSpan,
&*sep + 1);
memmove(const_cast<char*>(pathSpan.data()), &*sep + 1, sep - pathSpan.rbegin() + 1);
} else {
logging::I("{} Leaving pdb path unchanged: {}", LogTag, pathSpan);
}
} else {
logging::I("{} Leaving pdb path unchanged: {}", LogTag, pathSpan);
}
} else {
logging::I("{} CODEVIEW struct signature mismatch: got {:08X} instead.", LogTag, pdbref.Signature);
}
} else {
logging::I("{} Debug directory: type {} is unsupported.", LogTag, ddir.Type);
}
};
if (bApply) {
if (!g_startInfo.BootEnabledGameFixes.contains("symbol_load_patches")) {
logging::I("{} Turned off via environment variable.", LogTag);
return;
}
for (const auto& mod : utils::loaded_module::all_modules())
RemoveFullPathPdbInfo(mod);
if (!s_dllNotificationCookie) {
const auto res = LdrRegisterDllNotification(
0,
[](ULONG notiReason, const LDR_DLL_NOTIFICATION_DATA* pData, void* /* context */) {
if (notiReason == LDR_DLL_NOTIFICATION_REASON_LOADED)
RemoveFullPathPdbInfo(pData->Loaded.DllBase);
},
nullptr,
&s_dllNotificationCookie);
if (res != STATUS_SUCCESS) {
logging::E("{} LdrRegisterDllNotification failure: 0x{:08X}", LogTag, res);
s_dllNotificationCookie = nullptr;
}
}
s_hookSymInitialize.emplace("dbghelp.dll!SymInitialize (import, symbol_load_patches)", "dbghelp.dll", "SymInitialize", 0);
s_hookSymInitialize->set_detour([](HANDLE hProcess, PCSTR UserSearchPath, BOOL fInvadeProcess) noexcept {
logging::I("{} Suppressed SymInitialize.", LogTag);
SetLastError(ERROR_NOT_SUPPORTED);
return FALSE;
});
logging::I("{} Enable", LogTag);
}
else {
if (s_hookSymInitialize) {
logging::I("{} Disable", LogTag);
s_hookSymInitialize.reset();
}
if (s_dllNotificationCookie) {
(void)LdrUnregisterDllNotification(s_dllNotificationCookie);
s_dllNotificationCookie = nullptr;
}
}
}
void xivfixes::apply_all(bool bApply) {
for (const auto& [taskName, taskFunction] : std::initializer_list<std::pair<const char*, void(*)(bool)>>
{
@ -598,8 +656,8 @@ void xivfixes::apply_all(bool bApply) {
{ "disable_game_openprocess_access_check", &disable_game_openprocess_access_check },
{ "redirect_openprocess", &redirect_openprocess },
{ "backup_userdata_save", &backup_userdata_save },
{ "clr_failfast_hijack", &clr_failfast_hijack },
{ "prevent_icmphandle_crashes", &prevent_icmphandle_crashes }
{ "prevent_icmphandle_crashes", &prevent_icmphandle_crashes },
{ "symbol_load_patches", &symbol_load_patches },
}
) {
try {

View file

@ -6,8 +6,8 @@ namespace xivfixes {
void disable_game_openprocess_access_check(bool bApply);
void redirect_openprocess(bool bApply);
void backup_userdata_save(bool bApply);
void clr_failfast_hijack(bool bApply);
void prevent_icmphandle_crashes(bool bApply);
void symbol_load_patches(bool bApply);
void apply_all(bool bApply);
}

View file

@ -5,6 +5,7 @@ namespace Dalamud.Common;
/// <summary>
/// Struct containing information needed to initialize Dalamud.
/// Modify DalamudStartInfo.h and DalamudStartInfo.cpp along with this record.
/// </summary>
[Serializable]
public record DalamudStartInfo

View file

@ -27,7 +27,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Lumina" Version="3.15.2" />
<PackageReference Include="Lumina" Version="3.16.0" />
<PackageReference Include="Lumina.Excel" Version="6.5.2" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.333">

View file

@ -23,7 +23,7 @@ int wmain(int argc, wchar_t** argv)
// =========================================================================== //
void* entrypoint_vfn;
int result = InitializeClrAndGetEntryPoint(
const auto result = InitializeClrAndGetEntryPoint(
GetModuleHandleW(nullptr),
false,
runtimeconfig_path,
@ -33,15 +33,15 @@ int wmain(int argc, wchar_t** argv)
L"Dalamud.Injector.EntryPoint+MainDelegate, Dalamud.Injector",
&entrypoint_vfn);
if (result != 0)
if (FAILED(result))
return result;
typedef void (CORECLR_DELEGATE_CALLTYPE* custom_component_entry_point_fn)(int, wchar_t**);
typedef int (CORECLR_DELEGATE_CALLTYPE* custom_component_entry_point_fn)(int, wchar_t**);
custom_component_entry_point_fn entrypoint_fn = reinterpret_cast<custom_component_entry_point_fn>(entrypoint_vfn);
logging::I("Running Dalamud Injector...");
entrypoint_fn(argc, argv);
const auto ret = entrypoint_fn(argc, argv);
logging::I("Done!");
return 0;
return ret;
}

View file

@ -31,89 +31,100 @@ namespace Dalamud.Injector
/// </summary>
/// <param name="argc">Count of arguments.</param>
/// <param name="argvPtr">char** string arguments.</param>
public delegate void MainDelegate(int argc, IntPtr argvPtr);
/// <returns>Return value (HRESULT).</returns>
public delegate int MainDelegate(int argc, IntPtr argvPtr);
/// <summary>
/// Start the Dalamud injector.
/// </summary>
/// <param name="argc">Count of arguments.</param>
/// <param name="argvPtr">byte** string arguments.</param>
public static void Main(int argc, IntPtr argvPtr)
/// <returns>Return value (HRESULT).</returns>
public static int Main(int argc, IntPtr argvPtr)
{
List<string> args = new(argc);
unsafe
try
{
var argv = (IntPtr*)argvPtr;
for (var i = 0; i < argc; i++)
args.Add(Marshal.PtrToStringUni(argv[i]));
}
List<string> args = new(argc);
Init(args);
args.Remove("-v"); // Remove "verbose" flag
if (args.Count >= 2 && args[1].ToLowerInvariant() == "launch-test")
{
Environment.Exit(ProcessLaunchTestCommand(args));
return;
}
DalamudStartInfo startInfo = null;
if (args.Count == 1)
{
// No command defaults to inject
args.Add("inject");
args.Add("--all");
#if !DEBUG
args.Add("--warn");
#endif
}
else if (int.TryParse(args[1], out var _))
{
// Assume that PID has been passed.
args.Insert(1, "inject");
// If originally second parameter exists, then assume that it's a base64 encoded start info.
// Dalamud.Injector.exe inject [pid] [base64]
if (args.Count == 4)
unsafe
{
startInfo = JsonConvert.DeserializeObject<DalamudStartInfo>(Encoding.UTF8.GetString(Convert.FromBase64String(args[3])));
args.RemoveAt(3);
var argv = (IntPtr*)argvPtr;
for (var i = 0; i < argc; i++)
args.Add(Marshal.PtrToStringUni(argv[i]));
}
Init(args);
args.Remove("-v"); // Remove "verbose" flag
if (args.Count >= 2 && args[1].ToLowerInvariant() == "launch-test")
{
return ProcessLaunchTestCommand(args);
}
DalamudStartInfo startInfo = null;
if (args.Count == 1)
{
// No command defaults to inject
args.Add("inject");
args.Add("--all");
#if !DEBUG
args.Add("--warn");
#endif
}
else if (int.TryParse(args[1], out var _))
{
// Assume that PID has been passed.
args.Insert(1, "inject");
// If originally second parameter exists, then assume that it's a base64 encoded start info.
// Dalamud.Injector.exe inject [pid] [base64]
if (args.Count == 4)
{
startInfo = JsonConvert.DeserializeObject<DalamudStartInfo>(Encoding.UTF8.GetString(Convert.FromBase64String(args[3])));
args.RemoveAt(3);
}
}
startInfo = ExtractAndInitializeStartInfoFromArguments(startInfo, args);
// Remove already handled arguments
args.Remove("--console");
args.Remove("--msgbox1");
args.Remove("--msgbox2");
args.Remove("--msgbox3");
args.Remove("--etw");
args.Remove("--veh");
args.Remove("--veh-full");
args.Remove("--no-plugin");
args.Remove("--no-3rd-plugin");
args.Remove("--crash-handler-console");
args.Remove("--no-exception-handlers");
var mainCommand = args[1].ToLowerInvariant();
if (mainCommand.Length > 0 && mainCommand.Length <= 6 && "inject"[..mainCommand.Length] == mainCommand)
{
return ProcessInjectCommand(args, startInfo);
}
else if (mainCommand.Length > 0 && mainCommand.Length <= 6 &&
"launch"[..mainCommand.Length] == mainCommand)
{
return ProcessLaunchCommand(args, startInfo);
}
else if (mainCommand.Length > 0 && mainCommand.Length <= 4 &&
"help"[..mainCommand.Length] == mainCommand)
{
return ProcessHelpCommand(args, args.Count >= 3 ? args[2] : null);
}
else
{
throw new CommandLineException($"\"{mainCommand}\" is not a valid command.");
}
}
startInfo = ExtractAndInitializeStartInfoFromArguments(startInfo, args);
// Remove already handled arguments
args.Remove("--console");
args.Remove("--msgbox1");
args.Remove("--msgbox2");
args.Remove("--msgbox3");
args.Remove("--etw");
args.Remove("--veh");
args.Remove("--veh-full");
args.Remove("--no-plugin");
args.Remove("--no-3rd-plugin");
args.Remove("--crash-handler-console");
args.Remove("--no-exception-handlers");
var mainCommand = args[1].ToLowerInvariant();
if (mainCommand.Length > 0 && mainCommand.Length <= 6 && "inject"[..mainCommand.Length] == mainCommand)
catch (Exception e)
{
Environment.Exit(ProcessInjectCommand(args, startInfo));
}
else if (mainCommand.Length > 0 && mainCommand.Length <= 6 && "launch"[..mainCommand.Length] == mainCommand)
{
Environment.Exit(ProcessLaunchCommand(args, startInfo));
}
else if (mainCommand.Length > 0 && mainCommand.Length <= 4 && "help"[..mainCommand.Length] == mainCommand)
{
Environment.Exit(ProcessHelpCommand(args, args.Count >= 3 ? args[2] : null));
}
else
{
throw new CommandLineException($"\"{mainCommand}\" is not a valid command.");
Log.Error(e, "Operation failed.");
return e.HResult;
}
}
@ -189,6 +200,7 @@ namespace Dalamud.Injector
CullLogFile(logPath, 1 * 1024 * 1024);
Log.Logger = new LoggerConfiguration()
.WriteTo.Console(standardErrorFromLevel: LogEventLevel.Debug)
.WriteTo.File(logPath, fileSizeLimitBytes: null)
.MinimumLevel.ControlledBy(levelSwitch)
.CreateLogger();
@ -377,12 +389,22 @@ namespace Dalamud.Injector
#else
startInfo.LogPath ??= xivlauncherDir;
#endif
startInfo.LogName ??= string.Empty;
// Set boot defaults
startInfo.BootShowConsole = args.Contains("--console");
startInfo.BootEnableEtw = args.Contains("--etw");
startInfo.BootLogPath = GetLogPath(startInfo.LogPath, "dalamud.boot", startInfo.LogName);
startInfo.BootEnabledGameFixes = new List<string> { "prevent_devicechange_crashes", "disable_game_openprocess_access_check", "redirect_openprocess", "backup_userdata_save", "prevent_icmphandle_crashes" };
startInfo.BootEnabledGameFixes = new()
{
// See: xivfixes.h, xivfixes.cpp
"prevent_devicechange_crashes",
"disable_game_openprocess_access_check",
"redirect_openprocess",
"backup_userdata_save",
"prevent_icmphandle_crashes",
"symbol_load_patches",
};
startInfo.BootDotnetOpenProcessHookMode = 0;
startInfo.BootWaitMessageBox |= args.Contains("--msgbox1") ? 1 : 0;
startInfo.BootWaitMessageBox |= args.Contains("--msgbox2") ? 2 : 0;
@ -800,12 +822,8 @@ namespace Dalamud.Injector
{
var startInfo = AdjustStartInfo(dalamudStartInfo, gamePath);
Log.Information("Using start info: {0}", JsonConvert.SerializeObject(startInfo));
if (RewriteRemoteEntryPointW(p.Handle, gamePath, JsonConvert.SerializeObject(startInfo)) != 0)
{
Log.Error("[HOOKS] RewriteRemoteEntryPointW failed");
throw new Exception("RewriteRemoteEntryPointW failed");
}
Marshal.ThrowExceptionForHR(
RewriteRemoteEntryPointW(p.Handle, gamePath, JsonConvert.SerializeObject(startInfo)));
Log.Verbose("RewriteRemoteEntryPointW called!");
}
},

View file

@ -6,8 +6,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
.gitignore = .gitignore
targets\Dalamud.Plugin.targets = targets\Dalamud.Plugin.targets
targets\Dalamud.Plugin.Bootstrap.targets = targets\Dalamud.Plugin.Bootstrap.targets
targets\Dalamud.Plugin.targets = targets\Dalamud.Plugin.targets
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "build", "build\build.csproj", "{94E5B016-02B1-459B-97D9-E783F28764B2}"
@ -38,184 +38,70 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FFXIVClientStructs.InteropS
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "DalamudCrashHandler", "DalamudCrashHandler\DalamudCrashHandler.vcxproj", "{317A264C-920B-44A1-8A34-F3A6827B0705}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dalamud.Common", "Dalamud.Common\Dalamud.Common.csproj", "{F21B13D2-D7D0-4456-B70F-3F8D695064E2}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dalamud.Common", "Dalamud.Common\Dalamud.Common.csproj", "{F21B13D2-D7D0-4456-B70F-3F8D695064E2}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{94E5B016-02B1-459B-97D9-E783F28764B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{94E5B016-02B1-459B-97D9-E783F28764B2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{94E5B016-02B1-459B-97D9-E783F28764B2}.Debug|x64.ActiveCfg = Debug|Any CPU
{94E5B016-02B1-459B-97D9-E783F28764B2}.Debug|x64.Build.0 = Debug|Any CPU
{94E5B016-02B1-459B-97D9-E783F28764B2}.Debug|x86.ActiveCfg = Debug|Any CPU
{94E5B016-02B1-459B-97D9-E783F28764B2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{94E5B016-02B1-459B-97D9-E783F28764B2}.Release|Any CPU.Build.0 = Release|Any CPU
{94E5B016-02B1-459B-97D9-E783F28764B2}.Release|x64.ActiveCfg = Release|Any CPU
{94E5B016-02B1-459B-97D9-E783F28764B2}.Release|x64.Build.0 = Release|Any CPU
{94E5B016-02B1-459B-97D9-E783F28764B2}.Release|x86.ActiveCfg = Release|Any CPU
{B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Debug|Any CPU.ActiveCfg = Debug|x64
{B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Debug|Any CPU.Build.0 = Debug|x64
{B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Debug|x64.ActiveCfg = Debug|x64
{B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Debug|x64.Build.0 = Debug|x64
{B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Debug|x86.ActiveCfg = Debug|Any CPU
{B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Debug|x86.Build.0 = Debug|Any CPU
{B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Release|Any CPU.ActiveCfg = Release|x64
{B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Release|Any CPU.Build.0 = Release|x64
{B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Release|x64.ActiveCfg = Release|x64
{B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Release|x64.Build.0 = Release|x64
{B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Release|x86.ActiveCfg = Release|Any CPU
{B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Release|x86.Build.0 = Release|Any CPU
{55198DC3-A03D-408E-A8EB-2077780C8576}.Debug|Any CPU.ActiveCfg = Debug|x64
{55198DC3-A03D-408E-A8EB-2077780C8576}.Debug|Any CPU.Build.0 = Debug|x64
{55198DC3-A03D-408E-A8EB-2077780C8576}.Debug|x64.ActiveCfg = Debug|x64
{55198DC3-A03D-408E-A8EB-2077780C8576}.Debug|x64.Build.0 = Debug|x64
{55198DC3-A03D-408E-A8EB-2077780C8576}.Debug|x86.ActiveCfg = Debug|x64
{55198DC3-A03D-408E-A8EB-2077780C8576}.Debug|x86.Build.0 = Debug|x64
{55198DC3-A03D-408E-A8EB-2077780C8576}.Release|Any CPU.ActiveCfg = Release|x64
{55198DC3-A03D-408E-A8EB-2077780C8576}.Release|Any CPU.Build.0 = Release|x64
{55198DC3-A03D-408E-A8EB-2077780C8576}.Release|x64.ActiveCfg = Release|x64
{55198DC3-A03D-408E-A8EB-2077780C8576}.Release|x64.Build.0 = Release|x64
{55198DC3-A03D-408E-A8EB-2077780C8576}.Release|x86.ActiveCfg = Release|x64
{55198DC3-A03D-408E-A8EB-2077780C8576}.Release|x86.Build.0 = Release|x64
{5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Debug|Any CPU.ActiveCfg = Debug|x64
{5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Debug|Any CPU.Build.0 = Debug|x64
{5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Debug|x64.ActiveCfg = Debug|x64
{5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Debug|x64.Build.0 = Debug|x64
{5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Debug|x86.ActiveCfg = Debug|Any CPU
{5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Debug|x86.Build.0 = Debug|Any CPU
{5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|Any CPU.ActiveCfg = Release|x64
{5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|Any CPU.Build.0 = Release|x64
{5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|x64.ActiveCfg = Release|x64
{5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|x64.Build.0 = Release|x64
{5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|x86.ActiveCfg = Release|Any CPU
{5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|x86.Build.0 = Release|Any CPU
{8874326B-E755-4D13-90B4-59AB263A3E6B}.Debug|Any CPU.ActiveCfg = Debug|x64
{8874326B-E755-4D13-90B4-59AB263A3E6B}.Debug|Any CPU.Build.0 = Debug|x64
{8874326B-E755-4D13-90B4-59AB263A3E6B}.Debug|x64.ActiveCfg = Debug|x64
{8874326B-E755-4D13-90B4-59AB263A3E6B}.Debug|x64.Build.0 = Debug|x64
{8874326B-E755-4D13-90B4-59AB263A3E6B}.Debug|x86.ActiveCfg = Debug|x64
{8874326B-E755-4D13-90B4-59AB263A3E6B}.Debug|x86.Build.0 = Debug|x64
{8874326B-E755-4D13-90B4-59AB263A3E6B}.Release|Any CPU.ActiveCfg = Release|x64
{8874326B-E755-4D13-90B4-59AB263A3E6B}.Release|Any CPU.Build.0 = Release|x64
{8874326B-E755-4D13-90B4-59AB263A3E6B}.Release|x64.ActiveCfg = Release|x64
{8874326B-E755-4D13-90B4-59AB263A3E6B}.Release|x64.Build.0 = Release|x64
{8874326B-E755-4D13-90B4-59AB263A3E6B}.Release|x86.ActiveCfg = Release|x64
{8874326B-E755-4D13-90B4-59AB263A3E6B}.Release|x86.Build.0 = Release|x64
{C8004563-1806-4329-844F-0EF6274291FC}.Debug|Any CPU.ActiveCfg = Debug|x64
{C8004563-1806-4329-844F-0EF6274291FC}.Debug|Any CPU.Build.0 = Debug|x64
{C8004563-1806-4329-844F-0EF6274291FC}.Debug|x64.ActiveCfg = Debug|x64
{C8004563-1806-4329-844F-0EF6274291FC}.Debug|x64.Build.0 = Debug|x64
{C8004563-1806-4329-844F-0EF6274291FC}.Debug|x86.ActiveCfg = Debug|Any CPU
{C8004563-1806-4329-844F-0EF6274291FC}.Debug|x86.Build.0 = Debug|Any CPU
{C8004563-1806-4329-844F-0EF6274291FC}.Release|Any CPU.ActiveCfg = Release|x64
{C8004563-1806-4329-844F-0EF6274291FC}.Release|Any CPU.Build.0 = Release|x64
{C8004563-1806-4329-844F-0EF6274291FC}.Release|x64.ActiveCfg = Release|x64
{C8004563-1806-4329-844F-0EF6274291FC}.Release|x64.Build.0 = Release|x64
{C8004563-1806-4329-844F-0EF6274291FC}.Release|x86.ActiveCfg = Release|Any CPU
{C8004563-1806-4329-844F-0EF6274291FC}.Release|x86.Build.0 = Release|Any CPU
{0483026E-C6CE-4B1A-AA68-46544C08140B}.Debug|Any CPU.ActiveCfg = Debug|x64
{0483026E-C6CE-4B1A-AA68-46544C08140B}.Debug|Any CPU.Build.0 = Debug|x64
{0483026E-C6CE-4B1A-AA68-46544C08140B}.Debug|x64.ActiveCfg = Debug|x64
{0483026E-C6CE-4B1A-AA68-46544C08140B}.Debug|x64.Build.0 = Debug|x64
{0483026E-C6CE-4B1A-AA68-46544C08140B}.Debug|x86.ActiveCfg = Debug|x64
{0483026E-C6CE-4B1A-AA68-46544C08140B}.Debug|x86.Build.0 = Debug|x64
{0483026E-C6CE-4B1A-AA68-46544C08140B}.Release|Any CPU.ActiveCfg = Release|x64
{0483026E-C6CE-4B1A-AA68-46544C08140B}.Release|Any CPU.Build.0 = Release|x64
{0483026E-C6CE-4B1A-AA68-46544C08140B}.Release|x64.ActiveCfg = Release|x64
{0483026E-C6CE-4B1A-AA68-46544C08140B}.Release|x64.Build.0 = Release|x64
{0483026E-C6CE-4B1A-AA68-46544C08140B}.Release|x86.ActiveCfg = Release|x64
{0483026E-C6CE-4B1A-AA68-46544C08140B}.Release|x86.Build.0 = Release|x64
{C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Debug|Any CPU.ActiveCfg = Debug|x64
{C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Debug|Any CPU.Build.0 = Debug|x64
{C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Debug|x64.ActiveCfg = Debug|x64
{C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Debug|x64.Build.0 = Debug|x64
{C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Debug|x86.ActiveCfg = Debug|x64
{C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Debug|x86.Build.0 = Debug|x64
{C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Release|Any CPU.ActiveCfg = Release|x64
{C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Release|Any CPU.Build.0 = Release|x64
{C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Release|x64.ActiveCfg = Release|x64
{C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Release|x64.Build.0 = Release|x64
{C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Release|x86.ActiveCfg = Release|x64
{C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Release|x86.Build.0 = Release|x64
{2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Debug|Any CPU.ActiveCfg = Debug|x64
{2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Debug|Any CPU.Build.0 = Debug|x64
{2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Debug|x64.ActiveCfg = Debug|x64
{2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Debug|x64.Build.0 = Debug|x64
{2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Debug|x86.ActiveCfg = Debug|x64
{2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Debug|x86.Build.0 = Debug|x64
{2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Release|Any CPU.ActiveCfg = Release|x64
{2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Release|Any CPU.Build.0 = Release|x64
{2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Release|x64.ActiveCfg = Release|x64
{2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Release|x64.Build.0 = Release|x64
{2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Release|x86.ActiveCfg = Release|x64
{2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Release|x86.Build.0 = Release|x64
{4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Debug|Any CPU.ActiveCfg = Debug|x64
{4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Debug|Any CPU.Build.0 = Debug|x64
{4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Debug|x64.ActiveCfg = Debug|x64
{4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Debug|x64.Build.0 = Debug|x64
{4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Debug|x86.ActiveCfg = Debug|x64
{4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Debug|x86.Build.0 = Debug|x64
{4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Release|Any CPU.ActiveCfg = Release|x64
{4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Release|Any CPU.Build.0 = Release|x64
{4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Release|x64.ActiveCfg = Release|x64
{4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Release|x64.Build.0 = Release|x64
{4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Release|x86.ActiveCfg = Release|x64
{4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Release|x86.Build.0 = Release|x64
{C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Debug|x64.ActiveCfg = Debug|Any CPU
{C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Debug|x64.Build.0 = Debug|Any CPU
{C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Debug|x86.ActiveCfg = Debug|Any CPU
{C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Debug|x86.Build.0 = Debug|Any CPU
{C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Release|Any CPU.Build.0 = Release|Any CPU
{C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Release|x64.ActiveCfg = Release|Any CPU
{C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Release|x64.Build.0 = Release|Any CPU
{C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Release|x86.ActiveCfg = Release|Any CPU
{C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Release|x86.Build.0 = Release|Any CPU
{05AB2F46-268B-4915-806F-DDF813E2D59D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{05AB2F46-268B-4915-806F-DDF813E2D59D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{05AB2F46-268B-4915-806F-DDF813E2D59D}.Debug|x64.ActiveCfg = Debug|Any CPU
{05AB2F46-268B-4915-806F-DDF813E2D59D}.Debug|x64.Build.0 = Debug|Any CPU
{05AB2F46-268B-4915-806F-DDF813E2D59D}.Debug|x86.ActiveCfg = Debug|Any CPU
{05AB2F46-268B-4915-806F-DDF813E2D59D}.Debug|x86.Build.0 = Debug|Any CPU
{05AB2F46-268B-4915-806F-DDF813E2D59D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{05AB2F46-268B-4915-806F-DDF813E2D59D}.Release|Any CPU.Build.0 = Release|Any CPU
{05AB2F46-268B-4915-806F-DDF813E2D59D}.Release|x64.ActiveCfg = Release|Any CPU
{05AB2F46-268B-4915-806F-DDF813E2D59D}.Release|x64.Build.0 = Release|Any CPU
{05AB2F46-268B-4915-806F-DDF813E2D59D}.Release|x86.ActiveCfg = Release|Any CPU
{05AB2F46-268B-4915-806F-DDF813E2D59D}.Release|x86.Build.0 = Release|Any CPU
{317A264C-920B-44A1-8A34-F3A6827B0705}.Debug|Any CPU.ActiveCfg = Debug|x64
{317A264C-920B-44A1-8A34-F3A6827B0705}.Debug|Any CPU.Build.0 = Debug|x64
{317A264C-920B-44A1-8A34-F3A6827B0705}.Debug|x64.ActiveCfg = Debug|x64
{317A264C-920B-44A1-8A34-F3A6827B0705}.Debug|x64.Build.0 = Debug|x64
{317A264C-920B-44A1-8A34-F3A6827B0705}.Debug|x86.ActiveCfg = Debug|x64
{317A264C-920B-44A1-8A34-F3A6827B0705}.Debug|x86.Build.0 = Debug|x64
{317A264C-920B-44A1-8A34-F3A6827B0705}.Release|Any CPU.ActiveCfg = Release|x64
{317A264C-920B-44A1-8A34-F3A6827B0705}.Release|Any CPU.Build.0 = Release|x64
{317A264C-920B-44A1-8A34-F3A6827B0705}.Release|x64.ActiveCfg = Release|x64
{317A264C-920B-44A1-8A34-F3A6827B0705}.Release|x64.Build.0 = Release|x64
{317A264C-920B-44A1-8A34-F3A6827B0705}.Release|x86.ActiveCfg = Release|x64
{317A264C-920B-44A1-8A34-F3A6827B0705}.Release|x86.Build.0 = Release|x64
{F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Debug|x64.ActiveCfg = Debug|Any CPU
{F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Debug|x64.Build.0 = Debug|Any CPU
{F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Debug|x86.ActiveCfg = Debug|Any CPU
{F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Debug|x86.Build.0 = Debug|Any CPU
{F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Release|Any CPU.Build.0 = Release|Any CPU
{F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Release|x64.ActiveCfg = Release|Any CPU
{F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Release|x64.Build.0 = Release|Any CPU
{F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Release|x86.ActiveCfg = Release|Any CPU
{F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View file

@ -5,6 +5,7 @@ using System.IO;
using System.Linq;
using Dalamud.Game.Text;
using Dalamud.Interface.FontIdentifier;
using Dalamud.Interface.Internal.Windows.PluginInstaller;
using Dalamud.Interface.Style;
using Dalamud.IoC.Internal;
@ -145,7 +146,13 @@ internal sealed class DalamudConfiguration : IServiceType, IDisposable
/// <summary>
/// Gets or sets a value indicating whether to use AXIS fonts from the game.
/// </summary>
public bool UseAxisFontsFromGame { get; set; } = false;
[Obsolete($"See {nameof(DefaultFontSpec)}")]
public bool UseAxisFontsFromGame { get; set; } = true;
/// <summary>
/// Gets or sets the default font spec.
/// </summary>
public IFontSpec? DefaultFontSpec { get; set; }
/// <summary>
/// Gets or sets the gamma value to apply for Dalamud fonts. Do not use.
@ -208,6 +215,11 @@ internal sealed class DalamudConfiguration : IServiceType, IDisposable
/// </summary>
public bool LogOpenAtStartup { get; set; }
/// <summary>
/// Gets or sets the number of lines to keep for the Dalamud Console window.
/// </summary>
public int LogLinesLimit { get; set; } = 10000;
/// <summary>
/// Gets or sets a value indicating whether or not the dev bar should open at startup.
/// </summary>

View file

@ -8,7 +8,7 @@
</PropertyGroup>
<PropertyGroup Label="Feature">
<DalamudVersion>9.0.0.17</DalamudVersion>
<DalamudVersion>9.0.0.21</DalamudVersion>
<Description>XIV Launcher addon framework</Description>
<AssemblyVersion>$(DalamudVersion)</AssemblyVersion>
<Version>$(DalamudVersion)</Version>
@ -68,7 +68,7 @@
<PackageReference Include="goaaats.Reloaded.Hooks" Version="4.2.0-goat.4" />
<PackageReference Include="goaaats.Reloaded.Assembler" Version="1.0.14-goat.2" />
<PackageReference Include="JetBrains.Annotations" Version="2021.2.0" />
<PackageReference Include="Lumina" Version="3.15.2" />
<PackageReference Include="Lumina" Version="3.16.0" />
<PackageReference Include="Lumina.Excel" Version="6.5.2" />
<PackageReference Include="Microsoft.Extensions.ObjectPool" Version="8.0.1" />
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.46-beta">

View file

@ -1,4 +1,6 @@
using Dalamud.Hooking;
using System.Threading.Tasks;
using Dalamud.Hooking;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services;
@ -15,6 +17,11 @@ namespace Dalamud.Game.Config;
[ServiceManager.BlockingEarlyLoadedService]
internal sealed class GameConfig : IServiceType, IGameConfig, IDisposable
{
private readonly TaskCompletionSource tcsInitialization = new();
private readonly TaskCompletionSource<GameConfigSection> tcsSystem = new();
private readonly TaskCompletionSource<GameConfigSection> tcsUiConfig = new();
private readonly TaskCompletionSource<GameConfigSection> tcsUiControl = new();
private readonly GameConfigAddressResolver address = new();
private Hook<ConfigChangeDelegate>? configChangeHook;
@ -23,16 +30,32 @@ internal sealed class GameConfig : IServiceType, IGameConfig, IDisposable
{
framework.RunOnTick(() =>
{
Log.Verbose("[GameConfig] Initializing");
var csFramework = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework.Instance();
var commonConfig = &csFramework->SystemConfig.CommonSystemConfig;
this.System = new GameConfigSection("System", framework, &commonConfig->ConfigBase);
this.UiConfig = new GameConfigSection("UiConfig", framework, &commonConfig->UiConfig);
this.UiControl = new GameConfigSection("UiControl", framework, () => this.UiConfig.TryGetBool("PadMode", out var padMode) && padMode ? &commonConfig->UiControlGamepadConfig : &commonConfig->UiControlConfig);
this.address.Setup(sigScanner);
this.configChangeHook = Hook<ConfigChangeDelegate>.FromAddress(this.address.ConfigChangeAddress, this.OnConfigChanged);
this.configChangeHook.Enable();
try
{
Log.Verbose("[GameConfig] Initializing");
var csFramework = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework.Instance();
var commonConfig = &csFramework->SystemConfig.CommonSystemConfig;
this.tcsSystem.SetResult(new("System", framework, &commonConfig->ConfigBase));
this.tcsUiConfig.SetResult(new("UiConfig", framework, &commonConfig->UiConfig));
this.tcsUiControl.SetResult(
new(
"UiControl",
framework,
() => this.UiConfig.TryGetBool("PadMode", out var padMode) && padMode
? &commonConfig->UiControlGamepadConfig
: &commonConfig->UiControlConfig));
this.address.Setup(sigScanner);
this.configChangeHook = Hook<ConfigChangeDelegate>.FromAddress(
this.address.ConfigChangeAddress,
this.OnConfigChanged);
this.configChangeHook.Enable();
this.tcsInitialization.SetResult();
}
catch (Exception ex)
{
this.tcsInitialization.SetExceptionIfIncomplete(ex);
}
});
}
@ -58,14 +81,19 @@ internal sealed class GameConfig : IServiceType, IGameConfig, IDisposable
public event EventHandler<ConfigChangeEvent>? UiControlChanged;
#pragma warning restore 67
/// <inheritdoc/>
public GameConfigSection System { get; private set; }
/// <summary>
/// Gets a task representing the initialization state of this class.
/// </summary>
public Task InitializationTask => this.tcsInitialization.Task;
/// <inheritdoc/>
public GameConfigSection UiConfig { get; private set; }
public GameConfigSection System => this.tcsSystem.Task.Result;
/// <inheritdoc/>
public GameConfigSection UiControl { get; private set; }
public GameConfigSection UiConfig => this.tcsUiConfig.Task.Result;
/// <inheritdoc/>
public GameConfigSection UiControl => this.tcsUiControl.Task.Result;
/// <inheritdoc/>
public bool TryGet(SystemConfigOption option, out bool value) => this.System.TryGet(option.GetName(), out value);
@ -169,6 +197,11 @@ internal sealed class GameConfig : IServiceType, IGameConfig, IDisposable
/// <inheritdoc/>
void IDisposable.Dispose()
{
var ode = new ObjectDisposedException(nameof(GameConfig));
this.tcsInitialization.SetExceptionIfIncomplete(ode);
this.tcsSystem.SetExceptionIfIncomplete(ode);
this.tcsUiConfig.SetExceptionIfIncomplete(ode);
this.tcsUiControl.SetExceptionIfIncomplete(ode);
this.configChangeHook?.Disable();
this.configChangeHook?.Dispose();
}
@ -220,15 +253,24 @@ internal class GameConfigPluginScoped : IDisposable, IServiceType, IGameConfig
[ServiceManager.ServiceDependency]
private readonly GameConfig gameConfigService = Service<GameConfig>.Get();
private readonly Task initializationTask;
/// <summary>
/// Initializes a new instance of the <see cref="GameConfigPluginScoped"/> class.
/// </summary>
internal GameConfigPluginScoped()
{
this.gameConfigService.Changed += this.ConfigChangedForward;
this.gameConfigService.System.Changed += this.SystemConfigChangedForward;
this.gameConfigService.UiConfig.Changed += this.UiConfigConfigChangedForward;
this.gameConfigService.UiControl.Changed += this.UiControlConfigChangedForward;
this.initializationTask = this.gameConfigService.InitializationTask.ContinueWith(
r =>
{
if (!r.IsCompletedSuccessfully)
return r;
this.gameConfigService.System.Changed += this.SystemConfigChangedForward;
this.gameConfigService.UiConfig.Changed += this.UiConfigConfigChangedForward;
this.gameConfigService.UiControl.Changed += this.UiControlConfigChangedForward;
return Task.CompletedTask;
}).Unwrap();
}
/// <inheritdoc/>
@ -256,9 +298,15 @@ internal class GameConfigPluginScoped : IDisposable, IServiceType, IGameConfig
public void Dispose()
{
this.gameConfigService.Changed -= this.ConfigChangedForward;
this.gameConfigService.System.Changed -= this.SystemConfigChangedForward;
this.gameConfigService.UiConfig.Changed -= this.UiConfigConfigChangedForward;
this.gameConfigService.UiControl.Changed -= this.UiControlConfigChangedForward;
this.initializationTask.ContinueWith(
r =>
{
if (!r.IsCompletedSuccessfully)
return;
this.gameConfigService.System.Changed -= this.SystemConfigChangedForward;
this.gameConfigService.UiConfig.Changed -= this.UiConfigConfigChangedForward;
this.gameConfigService.UiControl.Changed -= this.UiControlConfigChangedForward;
});
this.Changed = null;
this.SystemChanged = null;

View file

@ -0,0 +1,560 @@
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Hooking;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Logging.Internal;
using Dalamud.Memory;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.System.Memory;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Component.GUI;
using FFXIVClientStructs.Interop;
using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType;
namespace Dalamud.Game.Gui.ContextMenu;
/// <summary>
/// This class handles interacting with the game's (right-click) context menu.
/// </summary>
[InterfaceVersion("1.0")]
[ServiceManager.EarlyLoadedService]
internal sealed unsafe class ContextMenu : IDisposable, IServiceType, IContextMenu
{
private static readonly ModuleLog Log = new("ContextMenu");
private readonly Hook<RaptureAtkModuleOpenAddonByAgentDelegate> raptureAtkModuleOpenAddonByAgentHook;
private readonly Hook<AddonContextMenuOnMenuSelectedDelegate> addonContextMenuOnMenuSelectedHook;
private readonly RaptureAtkModuleOpenAddonDelegate raptureAtkModuleOpenAddon;
[ServiceManager.ServiceConstructor]
private ContextMenu()
{
this.raptureAtkModuleOpenAddonByAgentHook = Hook<RaptureAtkModuleOpenAddonByAgentDelegate>.FromAddress((nint)RaptureAtkModule.Addresses.OpenAddonByAgent.Value, this.RaptureAtkModuleOpenAddonByAgentDetour);
this.addonContextMenuOnMenuSelectedHook = Hook<AddonContextMenuOnMenuSelectedDelegate>.FromAddress((nint)AddonContextMenu.StaticVTable.OnMenuSelected, this.AddonContextMenuOnMenuSelectedDetour);
this.raptureAtkModuleOpenAddon = Marshal.GetDelegateForFunctionPointer<RaptureAtkModuleOpenAddonDelegate>((nint)RaptureAtkModule.Addresses.OpenAddon.Value);
this.raptureAtkModuleOpenAddonByAgentHook.Enable();
this.addonContextMenuOnMenuSelectedHook.Enable();
}
private unsafe delegate ushort RaptureAtkModuleOpenAddonByAgentDelegate(RaptureAtkModule* module, byte* addonName, AtkUnitBase* addon, int valueCount, AtkValue* values, AgentInterface* agent, nint a7, ushort parentAddonId);
private unsafe delegate bool AddonContextMenuOnMenuSelectedDelegate(AddonContextMenu* addon, int selectedIdx, byte a3);
private unsafe delegate ushort RaptureAtkModuleOpenAddonDelegate(RaptureAtkModule* a1, uint addonNameId, uint valueCount, AtkValue* values, AgentInterface* parentAgent, ulong unk, ushort parentAddonId, int unk2);
/// <inheritdoc/>
public event IContextMenu.OnMenuOpenedDelegate OnMenuOpened;
private Dictionary<ContextMenuType, List<MenuItem>> MenuItems { get; } = new();
private object MenuItemsLock { get; } = new();
private AgentInterface* SelectedAgent { get; set; }
private ContextMenuType? SelectedMenuType { get; set; }
private List<MenuItem>? SelectedItems { get; set; }
private HashSet<nint> SelectedEventInterfaces { get; } = new();
private AtkUnitBase* SelectedParentAddon { get; set; }
// -1 -> -inf: native items
// 0 -> inf: selected items
private List<int> MenuCallbackIds { get; } = new();
private IReadOnlyList<MenuItem>? SubmenuItems { get; set; }
/// <inheritdoc/>
public void Dispose()
{
var manager = RaptureAtkUnitManager.Instance();
var menu = manager->GetAddonByName("ContextMenu");
var submenu = manager->GetAddonByName("AddonContextSub");
if (menu->IsVisible)
menu->FireCallbackInt(-1);
if (submenu->IsVisible)
submenu->FireCallbackInt(-1);
this.raptureAtkModuleOpenAddonByAgentHook.Dispose();
this.addonContextMenuOnMenuSelectedHook.Dispose();
}
/// <inheritdoc/>
public void AddMenuItem(ContextMenuType menuType, MenuItem item)
{
lock (this.MenuItemsLock)
{
if (!this.MenuItems.TryGetValue(menuType, out var items))
this.MenuItems[menuType] = items = new();
items.Add(item);
}
}
/// <inheritdoc/>
public bool RemoveMenuItem(ContextMenuType menuType, MenuItem item)
{
lock (this.MenuItemsLock)
{
if (!this.MenuItems.TryGetValue(menuType, out var items))
return false;
return items.Remove(item);
}
}
private AtkValue* ExpandContextMenuArray(Span<AtkValue> oldValues, int newSize)
{
// if the array has enough room, don't reallocate
if (oldValues.Length >= newSize)
return (AtkValue*)Unsafe.AsPointer(ref oldValues[0]);
var size = (sizeof(AtkValue) * newSize) + 8;
var newArray = (nint)IMemorySpace.GetUISpace()->Malloc((ulong)size, 0);
if (newArray == nint.Zero)
throw new OutOfMemoryException();
NativeMemory.Fill((void*)newArray, (nuint)size, 0);
*(ulong*)newArray = (ulong)newSize;
// copy old memory if existing
if (!oldValues.IsEmpty)
oldValues.CopyTo(new((void*)(newArray + 8), oldValues.Length));
return (AtkValue*)(newArray + 8);
}
private void FreeExpandedContextMenuArray(AtkValue* newValues, int newSize) =>
IMemorySpace.Free((void*)((nint)newValues - 8), (ulong)((newSize * sizeof(AtkValue)) + 8));
private AtkValue* CreateEmptySubmenuContextMenuArray(SeString name, int x, int y, out int valueCount)
{
// 0: UInt = ContextItemCount
// 1: String = Name
// 2: Int = PositionX
// 3: Int = PositionY
// 4: Bool = false
// 5: UInt = ContextItemSubmenuMask
// 6: UInt = ReturnArrowMask (_gap_0x6BC ? 1 << (ContextItemCount - 1) : 0)
// 7: UInt = 1
valueCount = 8;
var values = this.ExpandContextMenuArray(Span<AtkValue>.Empty, valueCount);
values[0].ChangeType(ValueType.UInt);
values[0].UInt = 0;
values[1].ChangeType(ValueType.String);
values[1].SetString(name.Encode().NullTerminate());
values[2].ChangeType(ValueType.Int);
values[2].Int = x;
values[3].ChangeType(ValueType.Int);
values[3].Int = y;
values[4].ChangeType(ValueType.Bool);
values[4].Byte = 0;
values[5].ChangeType(ValueType.UInt);
values[5].UInt = 0;
values[6].ChangeType(ValueType.UInt);
values[6].UInt = 0;
values[7].ChangeType(ValueType.UInt);
values[7].UInt = 1;
return values;
}
private void SetupGenericMenu(int headerCount, int sizeHeaderIdx, int returnHeaderIdx, int submenuHeaderIdx, IReadOnlyList<MenuItem> items, ref int valueCount, ref AtkValue* values)
{
var itemsWithIdx = items.Select((item, idx) => (item, idx)).OrderBy(i => i.item.Priority);
var prefixItems = itemsWithIdx.Where(i => i.item.Priority < 0).ToArray();
var suffixItems = itemsWithIdx.Where(i => i.item.Priority >= 0).ToArray();
var nativeMenuSize = (int)values[sizeHeaderIdx].UInt;
var prefixMenuSize = prefixItems.Length;
var suffixMenuSize = suffixItems.Length;
var hasGameDisabled = valueCount - headerCount - nativeMenuSize > 0;
var hasCustomDisabled = items.Any(item => !item.IsEnabled);
var hasAnyDisabled = hasGameDisabled || hasCustomDisabled;
values = this.ExpandContextMenuArray(
new(values, valueCount),
valueCount = (nativeMenuSize + items.Count) * (hasAnyDisabled ? 2 : 1) + headerCount);
var offsetData = new Span<AtkValue>(values, headerCount);
var nameData = new Span<AtkValue>(values + headerCount, nativeMenuSize + items.Count);
var disabledData = hasAnyDisabled ? new Span<AtkValue>(values + headerCount + nativeMenuSize + items.Count, nativeMenuSize + items.Count) : Span<AtkValue>.Empty;
var returnMask = offsetData[returnHeaderIdx].UInt;
var submenuMask = offsetData[submenuHeaderIdx].UInt;
nameData[..nativeMenuSize].CopyTo(nameData.Slice(prefixMenuSize, nativeMenuSize));
if (hasAnyDisabled)
{
if (hasGameDisabled)
{
// copy old disabled data
var oldDisabledData = new Span<AtkValue>(values + headerCount + nativeMenuSize, nativeMenuSize);
oldDisabledData.CopyTo(disabledData.Slice(prefixMenuSize, nativeMenuSize));
}
else
{
// enable all
for (var i = prefixMenuSize; i < prefixMenuSize + nativeMenuSize; ++i)
{
disabledData[i].ChangeType(ValueType.Int);
disabledData[i].Int = 0;
}
}
}
returnMask <<= prefixMenuSize;
submenuMask <<= prefixMenuSize;
void FillData(Span<AtkValue> disabledData, Span<AtkValue> nameData, int i, MenuItem item, int idx)
{
this.MenuCallbackIds.Add(idx);
if (hasAnyDisabled)
{
disabledData[i].ChangeType(ValueType.Int);
disabledData[i].Int = item.IsEnabled ? 0 : 1;
}
if (item.IsReturn)
returnMask |= 1u << i;
if (item.IsSubmenu)
submenuMask |= 1u << i;
nameData[i].ChangeType(ValueType.String);
nameData[i].SetString(item.PrefixedName.Encode().NullTerminate());
}
for (var i = 0; i < prefixMenuSize; ++i)
{
var (item, idx) = prefixItems[i];
FillData(disabledData, nameData, i, item, idx);
}
this.MenuCallbackIds.AddRange(Enumerable.Range(0, nativeMenuSize).Select(i => -i - 1));
for (var i = prefixMenuSize + nativeMenuSize; i < prefixMenuSize + nativeMenuSize + suffixMenuSize; ++i)
{
var (item, idx) = suffixItems[i - prefixMenuSize - nativeMenuSize];
FillData(disabledData, nameData, i, item, idx);
}
offsetData[returnHeaderIdx].UInt = returnMask;
offsetData[submenuHeaderIdx].UInt = submenuMask;
offsetData[sizeHeaderIdx].UInt += (uint)items.Count;
}
private void SetupContextMenu(IReadOnlyList<MenuItem> items, ref int valueCount, ref AtkValue* values)
{
// 0: UInt = Item Count
// 1: UInt = 0 (probably window name, just unused)
// 2: UInt = Return Mask (?)
// 3: UInt = Submenu Mask
// 4: UInt = OpenAtCursorPosition ? 2 : 1
// 5: UInt = 0
// 6: UInt = 0
foreach (var item in items)
{
if (!item.Prefix.HasValue)
{
item.PrefixChar = 'D';
item.PrefixColor = 539;
Log.Warning($"Menu item \"{item.Name}\" has no prefix, defaulting to Dalamud's. Menu items outside of a submenu must have a prefix.");
}
}
this.SetupGenericMenu(7, 0, 2, 3, items, ref valueCount, ref values);
}
private void SetupContextSubMenu(IReadOnlyList<MenuItem> items, ref int valueCount, ref AtkValue* values)
{
// 0: UInt = ContextItemCount
// 1: skipped?
// 2: Int = PositionX
// 3: Int = PositionY
// 4: Bool = false
// 5: UInt = ContextItemSubmenuMask
// 6: UInt = _gap_0x6BC ? 1 << (ContextItemCount - 1) : 0
// 7: UInt = 1
this.SetupGenericMenu(8, 0, 6, 5, items, ref valueCount, ref values);
}
private ushort RaptureAtkModuleOpenAddonByAgentDetour(RaptureAtkModule* module, byte* addonName, AtkUnitBase* addon, int valueCount, AtkValue* values, AgentInterface* agent, nint a7, ushort parentAddonId)
{
var oldValues = values;
if (MemoryHelper.EqualsZeroTerminatedString("ContextMenu", (nint)addonName))
{
this.MenuCallbackIds.Clear();
this.SelectedAgent = agent;
this.SelectedParentAddon = module->RaptureAtkUnitManager.GetAddonById(parentAddonId);
this.SelectedEventInterfaces.Clear();
if (this.SelectedAgent == AgentInventoryContext.Instance())
{
this.SelectedMenuType = ContextMenuType.Inventory;
}
else if (this.SelectedAgent == AgentContext.Instance())
{
this.SelectedMenuType = ContextMenuType.Default;
var menu = AgentContext.Instance()->CurrentContextMenu;
var handlers = new Span<Pointer<AtkEventInterface>>(menu->EventHandlerArray, 32);
var ids = new Span<byte>(menu->EventIdArray, 32);
var count = (int)values[0].UInt;
handlers = handlers.Slice(7, count);
ids = ids.Slice(7, count);
for (var i = 0; i < count; ++i)
{
if (ids[i] <= 106)
continue;
this.SelectedEventInterfaces.Add((nint)handlers[i].Value);
}
}
else
{
this.SelectedMenuType = null;
}
this.SubmenuItems = null;
if (this.SelectedMenuType is { } menuType)
{
lock (this.MenuItemsLock)
{
if (this.MenuItems.TryGetValue(menuType, out var items))
this.SelectedItems = new(items);
else
this.SelectedItems = new();
}
var args = new MenuOpenedArgs(this.SelectedItems.Add, this.SelectedParentAddon, this.SelectedAgent, this.SelectedMenuType.Value, this.SelectedEventInterfaces);
this.OnMenuOpened?.InvokeSafely(args);
this.SelectedItems = this.FixupMenuList(this.SelectedItems, (int)values[0].UInt);
this.SetupContextMenu(this.SelectedItems, ref valueCount, ref values);
Log.Verbose($"Opening {this.SelectedMenuType} context menu with {this.SelectedItems.Count} custom items.");
}
else
{
this.SelectedItems = null;
}
}
else if (MemoryHelper.EqualsZeroTerminatedString("AddonContextSub", (nint)addonName))
{
this.MenuCallbackIds.Clear();
if (this.SubmenuItems != null)
{
this.SubmenuItems = this.FixupMenuList(this.SubmenuItems.ToList(), (int)values[0].UInt);
this.SetupContextSubMenu(this.SubmenuItems, ref valueCount, ref values);
Log.Verbose($"Opening {this.SelectedMenuType} submenu with {this.SubmenuItems.Count} custom items.");
}
}
var ret = this.raptureAtkModuleOpenAddonByAgentHook.Original(module, addonName, addon, valueCount, values, agent, a7, parentAddonId);
if (values != oldValues)
this.FreeExpandedContextMenuArray(values, valueCount);
return ret;
}
private List<MenuItem> FixupMenuList(List<MenuItem> items, int nativeMenuSize)
{
// The in game menu actually supports 32 items, but the last item can't have a visible submenu arrow.
// As such, we'll only work with 31 items.
const int MaxMenuItems = 31;
if (items.Count + nativeMenuSize > MaxMenuItems)
{
Log.Warning($"Menu size exceeds {MaxMenuItems} items, truncating.");
var orderedItems = items.OrderBy(i => i.Priority).ToArray();
var newItems = orderedItems[..(MaxMenuItems - nativeMenuSize - 1)];
var submenuItems = orderedItems[(MaxMenuItems - nativeMenuSize - 1)..];
return newItems.Append(new MenuItem
{
Prefix = SeIconChar.BoxedLetterD,
PrefixColor = 539,
IsSubmenu = true,
Priority = int.MaxValue,
Name = $"See More ({submenuItems.Length})",
OnClicked = a => a.OpenSubmenu(submenuItems),
}).ToList();
}
return items;
}
private void OpenSubmenu(SeString name, IReadOnlyList<MenuItem> submenuItems, int posX, int posY)
{
if (submenuItems.Count == 0)
throw new ArgumentException("Submenu must not be empty", nameof(submenuItems));
this.SubmenuItems = submenuItems;
var module = RaptureAtkModule.Instance();
var values = this.CreateEmptySubmenuContextMenuArray(name, posX, posY, out var valueCount);
switch (this.SelectedMenuType)
{
case ContextMenuType.Default:
{
var ownerAddonId = ((AgentContext*)this.SelectedAgent)->OwnerAddon;
this.raptureAtkModuleOpenAddon(module, 445, (uint)valueCount, values, this.SelectedAgent, 71, checked((ushort)ownerAddonId), 4);
break;
}
case ContextMenuType.Inventory:
{
var ownerAddonId = ((AgentInventoryContext*)this.SelectedAgent)->OwnerAddonId;
this.raptureAtkModuleOpenAddon(module, 445, (uint)valueCount, values, this.SelectedAgent, 0, checked((ushort)ownerAddonId), 4);
break;
}
default:
Log.Warning($"Unknown context menu type (agent: {(nint)this.SelectedAgent}, cannot open submenu");
break;
}
this.FreeExpandedContextMenuArray(values, valueCount);
}
private bool AddonContextMenuOnMenuSelectedDetour(AddonContextMenu* addon, int selectedIdx, byte a3)
{
var items = this.SubmenuItems ?? this.SelectedItems;
if (items == null)
goto original;
if (this.MenuCallbackIds.Count == 0)
goto original;
if (selectedIdx < 0)
goto original;
if (selectedIdx >= this.MenuCallbackIds.Count)
goto original;
var callbackId = this.MenuCallbackIds[selectedIdx];
if (callbackId < 0)
{
selectedIdx = -callbackId - 1;
goto original;
}
else
{
var item = items[callbackId];
var openedSubmenu = false;
try
{
if (item.OnClicked == null)
throw new InvalidOperationException("Item has no OnClicked handler");
item.OnClicked.InvokeSafely(new(
(name, items) =>
{
short x, y;
addon->AtkUnitBase.GetPosition(&x, &y);
this.OpenSubmenu(name ?? item.Name, items, x, y);
openedSubmenu = true;
},
this.SelectedParentAddon,
this.SelectedAgent,
this.SelectedMenuType.Value,
this.SelectedEventInterfaces));
}
catch (Exception e)
{
Log.Error(e, "Error while handling context menu click");
}
// Close with clicky sound
if (!openedSubmenu)
addon->AtkUnitBase.FireCallbackInt(-2);
return false;
}
original:
// Eventually handled by inventorycontext here: 14022BBD0 (6.51)
return this.addonContextMenuOnMenuSelectedHook.Original(addon, selectedIdx, a3);
}
}
/// <summary>
/// Plugin-scoped version of a <see cref="ContextMenu"/> service.
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.ScopedService]
#pragma warning disable SA1015
[ResolveVia<IContextMenu>]
#pragma warning restore SA1015
internal class ContextMenuPluginScoped : IDisposable, IServiceType, IContextMenu
{
[ServiceManager.ServiceDependency]
private readonly ContextMenu parentService = Service<ContextMenu>.Get();
private ContextMenuPluginScoped()
{
this.parentService.OnMenuOpened += this.OnMenuOpenedForward;
}
/// <inheritdoc/>
public event IContextMenu.OnMenuOpenedDelegate OnMenuOpened;
private Dictionary<ContextMenuType, List<MenuItem>> MenuItems { get; } = new();
private object MenuItemsLock { get; } = new();
/// <inheritdoc/>
public void Dispose()
{
this.parentService.OnMenuOpened -= this.OnMenuOpenedForward;
this.OnMenuOpened = null;
lock (this.MenuItemsLock)
{
foreach (var (menuType, items) in this.MenuItems)
{
foreach (var item in items)
this.parentService.RemoveMenuItem(menuType, item);
}
}
}
/// <inheritdoc/>
public void AddMenuItem(ContextMenuType menuType, MenuItem item)
{
lock (this.MenuItemsLock)
{
if (!this.MenuItems.TryGetValue(menuType, out var items))
this.MenuItems[menuType] = items = new();
items.Add(item);
}
this.parentService.AddMenuItem(menuType, item);
}
/// <inheritdoc/>
public bool RemoveMenuItem(ContextMenuType menuType, MenuItem item)
{
lock (this.MenuItemsLock)
{
if (this.MenuItems.TryGetValue(menuType, out var items))
items.Remove(item);
}
return this.parentService.RemoveMenuItem(menuType, item);
}
private void OnMenuOpenedForward(MenuOpenedArgs args) =>
this.OnMenuOpened?.Invoke(args);
}

View file

@ -0,0 +1,18 @@
namespace Dalamud.Game.Gui.ContextMenu;
/// <summary>
/// The type of context menu.
/// Each one has a different associated <see cref="MenuTarget"/>.
/// </summary>
public enum ContextMenuType
{
/// <summary>
/// The default context menu.
/// </summary>
Default,
/// <summary>
/// The inventory context menu. Used when right-clicked on an item.
/// </summary>
Inventory,
}

View file

@ -0,0 +1,77 @@
using System.Collections.Generic;
using Dalamud.Memory;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace Dalamud.Game.Gui.ContextMenu;
/// <summary>
/// Base class for <see cref="IContextMenu"/> menu args.
/// </summary>
public abstract unsafe class MenuArgs
{
private IReadOnlySet<nint>? eventInterfaces;
/// <summary>
/// Initializes a new instance of the <see cref="MenuArgs"/> class.
/// </summary>
/// <param name="addon">Addon associated with the context menu.</param>
/// <param name="agent">Agent associated with the context menu.</param>
/// <param name="type">The type of context menu.</param>
/// <param name="eventInterfaces">List of AtkEventInterfaces associated with the context menu.</param>
protected internal MenuArgs(AtkUnitBase* addon, AgentInterface* agent, ContextMenuType type, IReadOnlySet<nint>? eventInterfaces)
{
this.AddonName = addon != null ? MemoryHelper.ReadString((nint)addon->Name, 32) : null;
this.AddonPtr = (nint)addon;
this.AgentPtr = (nint)agent;
this.MenuType = type;
this.eventInterfaces = eventInterfaces;
this.Target = type switch
{
ContextMenuType.Default => new MenuTargetDefault((AgentContext*)agent),
ContextMenuType.Inventory => new MenuTargetInventory((AgentInventoryContext*)agent),
_ => throw new ArgumentException("Invalid context menu type", nameof(type)),
};
}
/// <summary>
/// Gets the name of the addon that opened the context menu.
/// </summary>
public string? AddonName { get; }
/// <summary>
/// Gets the memory pointer of the addon that opened the context menu.
/// </summary>
public nint AddonPtr { get; }
/// <summary>
/// Gets the memory pointer of the agent that opened the context menu.
/// </summary>
public nint AgentPtr { get; }
/// <summary>
/// Gets the type of the context menu.
/// </summary>
public ContextMenuType MenuType { get; }
/// <summary>
/// Gets the target info of the context menu. The actual type depends on <see cref="MenuType"/>.
/// <see cref="ContextMenuType.Default"/> signifies a <see cref="MenuTargetDefault"/>.
/// <see cref="ContextMenuType.Inventory"/> signifies a <see cref="MenuTargetInventory"/>.
/// </summary>
public MenuTarget Target { get; }
/// <summary>
/// Gets a list of AtkEventInterface pointers associated with the context menu.
/// Only available with <see cref="ContextMenuType.Default"/>.
/// Almost always an agent pointer. You can use this to find out what type of context menu it is.
/// </summary>
/// <exception cref="InvalidOperationException">Thrown when the context menu is not a <see cref="ContextMenuType.Default"/>.</exception>
public IReadOnlySet<nint> EventInterfaces =>
this.MenuType != ContextMenuType.Default ?
this.eventInterfaces :
throw new InvalidOperationException("Not a default context menu");
}

View file

@ -0,0 +1,91 @@
using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling;
using Lumina.Excel.GeneratedSheets;
namespace Dalamud.Game.Gui.ContextMenu;
/// <summary>
/// A menu item that can be added to a context menu.
/// </summary>
public sealed record MenuItem
{
/// <summary>
/// Gets or sets the display name of the menu item.
/// </summary>
public SeString Name { get; set; } = SeString.Empty;
/// <summary>
/// Gets or sets the prefix attached to the beginning of <see cref="Name"/>.
/// </summary>
public SeIconChar? Prefix { get; set; }
/// <summary>
/// Sets the character to prefix the <see cref="Name"/> with. Will be converted into a fancy boxed letter icon. Must be an uppercase letter.
/// </summary>
/// <exception cref="ArgumentException"><paramref name="value"/> must be an uppercase letter.</exception>
public char? PrefixChar
{
set
{
if (value is { } prefix)
{
if (!char.IsAsciiLetterUpper(prefix))
throw new ArgumentException("Prefix must be an uppercase letter", nameof(value));
this.Prefix = SeIconChar.BoxedLetterA + prefix - 'A';
}
else
{
this.Prefix = null;
}
}
}
/// <summary>
/// Gets or sets the color of the <see cref="Prefix"/>. Specifies a <see cref="UIColor"/> row id.
/// </summary>
public ushort PrefixColor { get; set; }
/// <summary>
/// Gets or sets the callback to be invoked when the menu item is clicked.
/// </summary>
public Action<MenuItemClickedArgs>? OnClicked { get; set; }
/// <summary>
/// Gets or sets the priority (or order) with which the menu item should be displayed in descending order.
/// Priorities below 0 will be displayed above the native menu items.
/// Other priorities will be displayed below the native menu items.
/// </summary>
public int Priority { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the menu item is enabled.
/// Disabled items will be faded and cannot be clicked on.
/// </summary>
public bool IsEnabled { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether the menu item is a submenu.
/// This value is purely visual. Submenu items will have an arrow to its right.
/// </summary>
public bool IsSubmenu { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the menu item is a return item.
/// This value is purely visual. Return items will have a back arrow to its left.
/// If both <see cref="IsSubmenu"/> and <see cref="IsReturn"/> are true, the return arrow will take precedence.
/// </summary>
public bool IsReturn { get; set; }
/// <summary>
/// Gets the name with the given prefix.
/// </summary>
internal SeString PrefixedName =>
this.Prefix is { } prefix
? new SeStringBuilder()
.AddUiForeground($"{prefix.ToIconString()} ", this.PrefixColor)
.Append(this.Name)
.Build()
: this.Name;
}

View file

@ -0,0 +1,44 @@
using System.Collections.Generic;
using Dalamud.Game.Text.SeStringHandling;
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace Dalamud.Game.Gui.ContextMenu;
/// <summary>
/// Callback args used when a menu item is clicked.
/// </summary>
public sealed unsafe class MenuItemClickedArgs : MenuArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="MenuItemClickedArgs"/> class.
/// </summary>
/// <param name="openSubmenu">Callback for opening a submenu.</param>
/// <param name="addon">Addon associated with the context menu.</param>
/// <param name="agent">Agent associated with the context menu.</param>
/// <param name="type">The type of context menu.</param>
/// <param name="eventInterfaces">List of AtkEventInterfaces associated with the context menu.</param>
internal MenuItemClickedArgs(Action<SeString?, IReadOnlyList<MenuItem>> openSubmenu, AtkUnitBase* addon, AgentInterface* agent, ContextMenuType type, IReadOnlySet<nint> eventInterfaces)
: base(addon, agent, type, eventInterfaces)
{
this.OnOpenSubmenu = openSubmenu;
}
private Action<SeString?, IReadOnlyList<MenuItem>> OnOpenSubmenu { get; }
/// <summary>
/// Opens a submenu with the given name and items.
/// </summary>
/// <param name="name">The name of the submenu, displayed at the top.</param>
/// <param name="items">The items to display in the submenu.</param>
public void OpenSubmenu(SeString name, IReadOnlyList<MenuItem> items) =>
this.OnOpenSubmenu(name, items);
/// <summary>
/// Opens a submenu with the given items.
/// </summary>
/// <param name="items">The items to display in the submenu.</param>
public void OpenSubmenu(IReadOnlyList<MenuItem> items) =>
this.OnOpenSubmenu(null, items);
}

View file

@ -0,0 +1,34 @@
using System.Collections.Generic;
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace Dalamud.Game.Gui.ContextMenu;
/// <summary>
/// Callback args used when a menu item is opened.
/// </summary>
public sealed unsafe class MenuOpenedArgs : MenuArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="MenuOpenedArgs"/> class.
/// </summary>
/// <param name="addMenuItem">Callback for adding a custom menu item.</param>
/// <param name="addon">Addon associated with the context menu.</param>
/// <param name="agent">Agent associated with the context menu.</param>
/// <param name="type">The type of context menu.</param>
/// <param name="eventInterfaces">List of AtkEventInterfaces associated with the context menu.</param>
internal MenuOpenedArgs(Action<MenuItem> addMenuItem, AtkUnitBase* addon, AgentInterface* agent, ContextMenuType type, IReadOnlySet<nint> eventInterfaces)
: base(addon, agent, type, eventInterfaces)
{
this.OnAddMenuItem = addMenuItem;
}
private Action<MenuItem> OnAddMenuItem { get; }
/// <summary>
/// Adds a custom menu item to the context menu.
/// </summary>
/// <param name="item">The menu item to add.</param>
public void AddMenuItem(MenuItem item) =>
this.OnAddMenuItem(item);
}

View file

@ -0,0 +1,9 @@
namespace Dalamud.Game.Gui.ContextMenu;
/// <summary>
/// Base class for <see cref="MenuArgs"/> contexts.
/// Discriminated based on <see cref="ContextMenuType"/>.
/// </summary>
public abstract class MenuTarget
{
}

View file

@ -0,0 +1,67 @@
using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Game.ClientState.Resolvers;
using Dalamud.Game.Network.Structures.InfoProxy;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using Lumina.Excel.GeneratedSheets;
namespace Dalamud.Game.Gui.ContextMenu;
/// <summary>
/// Target information on a default context menu.
/// </summary>
public sealed unsafe class MenuTargetDefault : MenuTarget
{
/// <summary>
/// Initializes a new instance of the <see cref="MenuTargetDefault"/> class.
/// </summary>
/// <param name="context">The agent associated with the context menu.</param>
internal MenuTargetDefault(AgentContext* context)
{
this.Context = context;
}
/// <summary>
/// Gets the name of the target.
/// </summary>
public string TargetName => this.Context->TargetName.ToString();
/// <summary>
/// Gets the object id of the target.
/// </summary>
public ulong TargetObjectId => this.Context->TargetObjectId;
/// <summary>
/// Gets the target object.
/// </summary>
public GameObject? TargetObject => Service<ObjectTable>.Get().SearchById(this.TargetObjectId);
/// <summary>
/// Gets the content id of the target.
/// </summary>
public ulong TargetContentId => this.Context->TargetContentId;
/// <summary>
/// Gets the home world id of the target.
/// </summary>
public ExcelResolver<World> TargetHomeWorld => new((uint)this.Context->TargetHomeWorldId);
/// <summary>
/// Gets the currently targeted character. Only shows up for specific targets, like friends, party finder listings, or party members.
/// Just because this is <see langword="null"/> doesn't mean the target isn't a character.
/// </summary>
public CharacterData? TargetCharacter
{
get
{
var target = this.Context->CurrentContextMenuTarget;
if (target != null)
return new(target);
return null;
}
}
private AgentContext* Context { get; }
}

View file

@ -0,0 +1,36 @@
using Dalamud.Game.Inventory;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
namespace Dalamud.Game.Gui.ContextMenu;
/// <summary>
/// Target information on an inventory context menu.
/// </summary>
public sealed unsafe class MenuTargetInventory : MenuTarget
{
/// <summary>
/// Initializes a new instance of the <see cref="MenuTargetInventory"/> class.
/// </summary>
/// <param name="context">The agent associated with the context menu.</param>
internal MenuTargetInventory(AgentInventoryContext* context)
{
this.Context = context;
}
/// <summary>
/// Gets the target item.
/// </summary>
public GameInventoryItem? TargetItem
{
get
{
var target = this.Context->TargetInventorySlot;
if (target != null)
return new(*target);
return null;
}
}
private AgentInventoryContext* Context { get; }
}

View file

@ -107,7 +107,7 @@ internal sealed unsafe partial class DalamudAtkTweaks : IServiceType
private IntPtr AtkUnitBaseReceiveGlobalEventDetour(AtkUnitBase* thisPtr, ushort cmd, uint a3, IntPtr a4, uint* arg)
{
// Log.Information("{0}: cmd#{1} a3#{2} - HasAnyFocus:{3}", Marshal.PtrToStringAnsi(new IntPtr(thisPtr->Name)), cmd, a3, WindowSystem.HasAnyWindowSystemFocus);
// Log.Information("{0}: cmd#{1} a3#{2} - HasAnyFocus:{3}", MemoryHelper.ReadSeStringAsString(out _, new IntPtr(thisPtr->Name)), cmd, a3, WindowSystem.HasAnyWindowSystemFocus);
// "SendHotkey"
// 3 == Close

View file

@ -1,7 +1,10 @@
using System.Diagnostics;
using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.Game;
namespace Dalamud.Game.Inventory;
@ -103,8 +106,10 @@ public unsafe struct GameInventoryItem : IEquatable<GameInventoryItem>
/// <summary>
/// Gets the array of materia grades.
/// </summary>
// TODO: Replace with MateriaGradeBytes
[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)]
public ReadOnlySpan<ushort> MateriaGrade =>
new(Unsafe.AsPointer(ref Unsafe.AsRef(in this.InternalItem.MateriaGrade[0])), 5);
this.MateriaGradeBytes.ToArray().Select(g => (ushort)g).ToArray().AsSpan();
/// <summary>
/// Gets the address of native inventory item in the game.<br />
@ -146,6 +151,9 @@ public unsafe struct GameInventoryItem : IEquatable<GameInventoryItem>
/// </summary>
internal ulong CrafterContentId => this.InternalItem.CrafterContentID;
private ReadOnlySpan<byte> MateriaGradeBytes =>
new(Unsafe.AsPointer(ref Unsafe.AsRef(in this.InternalItem.MateriaGrade[0])), 5);
public static bool operator ==(in GameInventoryItem l, in GameInventoryItem r) => l.Equals(r);
public static bool operator !=(in GameInventoryItem l, in GameInventoryItem r) => !l.Equals(r);

View file

@ -0,0 +1,197 @@
using System.Collections.Generic;
using Dalamud.Game.ClientState.Resolvers;
using Dalamud.Memory;
using FFXIVClientStructs.FFXIV.Client.UI.Info;
using Lumina.Excel.GeneratedSheets;
namespace Dalamud.Game.Network.Structures.InfoProxy;
/// <summary>
/// Dalamud wrapper around a client structs <see cref="InfoProxyCommonList.CharacterData"/>.
/// </summary>
public unsafe class CharacterData
{
/// <summary>
/// Initializes a new instance of the <see cref="CharacterData"/> class.
/// </summary>
/// <param name="data">Character data to wrap.</param>
internal CharacterData(InfoProxyCommonList.CharacterData* data)
{
this.Address = (nint)data;
}
/// <summary>
/// Gets the address of the <see cref="InfoProxyCommonList.CharacterData"/> in memory.
/// </summary>
public nint Address { get; }
/// <summary>
/// Gets the content id of the character.
/// </summary>
public ulong ContentId => this.Struct->ContentId;
/// <summary>
/// Gets the status mask of the character.
/// </summary>
public ulong StatusMask => (ulong)this.Struct->State;
/// <summary>
/// Gets the applicable statues of the character.
/// </summary>
public IReadOnlyList<ExcelResolver<OnlineStatus>> Statuses
{
get
{
var statuses = new List<ExcelResolver<OnlineStatus>>();
for (var i = 0; i < 64; i++)
{
if ((this.StatusMask & (1UL << i)) != 0)
statuses.Add(new((uint)i));
}
return statuses;
}
}
/// <summary>
/// Gets the display group of the character.
/// </summary>
public DisplayGroup DisplayGroup => (DisplayGroup)this.Struct->Group;
/// <summary>
/// Gets a value indicating whether the character's home world is different from the current world.
/// </summary>
public bool IsFromOtherServer => this.Struct->IsOtherServer;
/// <summary>
/// Gets the sort order of the character.
/// </summary>
public byte Sort => this.Struct->Sort;
/// <summary>
/// Gets the current world of the character.
/// </summary>
public ExcelResolver<World> CurrentWorld => new(this.Struct->CurrentWorld);
/// <summary>
/// Gets the home world of the character.
/// </summary>
public ExcelResolver<World> HomeWorld => new(this.Struct->HomeWorld);
/// <summary>
/// Gets the location of the character.
/// </summary>
public ExcelResolver<TerritoryType> Location => new(this.Struct->Location);
/// <summary>
/// Gets the grand company of the character.
/// </summary>
public ExcelResolver<GrandCompany> GrandCompany => new((uint)this.Struct->GrandCompany);
/// <summary>
/// Gets the primary client language of the character.
/// </summary>
public ClientLanguage ClientLanguage => (ClientLanguage)this.Struct->ClientLanguage;
/// <summary>
/// Gets the supported language mask of the character.
/// </summary>
public byte LanguageMask => (byte)this.Struct->Languages;
/// <summary>
/// Gets the supported languages the character supports.
/// </summary>
public IReadOnlyList<ClientLanguage> Languages
{
get
{
var languages = new List<ClientLanguage>();
for (var i = 0; i < 4; i++)
{
if ((this.LanguageMask & (1 << i)) != 0)
languages.Add((ClientLanguage)i);
}
return languages;
}
}
/// <summary>
/// Gets the gender of the character.
/// </summary>
public byte Gender => this.Struct->Sex;
/// <summary>
/// Gets the job of the character.
/// </summary>
public ExcelResolver<ClassJob> ClassJob => new(this.Struct->Job);
/// <summary>
/// Gets the name of the character.
/// </summary>
public string Name => MemoryHelper.ReadString((nint)this.Struct->Name, 32);
/// <summary>
/// Gets the free company tag of the character.
/// </summary>
public string FCTag => MemoryHelper.ReadString((nint)this.Struct->Name, 6);
/// <summary>
/// Gets the underlying <see cref="InfoProxyCommonList.CharacterData"/> struct.
/// </summary>
internal InfoProxyCommonList.CharacterData* Struct => (InfoProxyCommonList.CharacterData*)this.Address;
}
/// <summary>
/// Display group of a character. Used for friends.
/// </summary>
public enum DisplayGroup : sbyte
{
/// <summary>
/// All display groups.
/// </summary>
All = -1,
/// <summary>
/// No display group.
/// </summary>
None,
/// <summary>
/// Star display group.
/// </summary>
Star,
/// <summary>
/// Circle display group.
/// </summary>
Circle,
/// <summary>
/// Triangle display group.
/// </summary>
Triangle,
/// <summary>
/// Diamond display group.
/// </summary>
Diamond,
/// <summary>
/// Heart display group.
/// </summary>
Heart,
/// <summary>
/// Spade display group.
/// </summary>
Spade,
/// <summary>
/// Club display group.
/// </summary>
Club,
}

View file

@ -0,0 +1,87 @@
using System.Collections.Generic;
using Dalamud.Interface.ManagedFontAtlas;
using Dalamud.Storage.Assets;
using ImGuiNET;
using Newtonsoft.Json;
using TerraFX.Interop.DirectX;
namespace Dalamud.Interface.FontIdentifier;
/// <summary>
/// Represents a font from Dalamud assets.
/// </summary>
public sealed class DalamudAssetFontAndFamilyId : IFontFamilyId, IFontId
{
/// <summary>
/// Initializes a new instance of the <see cref="DalamudAssetFontAndFamilyId"/> class.
/// </summary>
/// <param name="asset">The font asset.</param>
public DalamudAssetFontAndFamilyId(DalamudAsset asset)
{
if (asset.GetPurpose() != DalamudAssetPurpose.Font)
throw new ArgumentOutOfRangeException(nameof(asset), asset, "The specified asset is not a font asset.");
this.Asset = asset;
}
/// <summary>
/// Gets the font asset.
/// </summary>
[JsonProperty]
public DalamudAsset Asset { get; init; }
/// <inheritdoc/>
[JsonIgnore]
public string EnglishName => $"Dalamud: {this.Asset}";
/// <inheritdoc/>
[JsonIgnore]
public IReadOnlyDictionary<string, string>? LocaleNames => null;
/// <inheritdoc/>
[JsonIgnore]
public IReadOnlyList<IFontId> Fonts => new List<IFontId> { this }.AsReadOnly();
/// <inheritdoc/>
[JsonIgnore]
public IFontFamilyId Family => this;
/// <inheritdoc/>
[JsonIgnore]
public int Weight => (int)DWRITE_FONT_WEIGHT.DWRITE_FONT_WEIGHT_NORMAL;
/// <inheritdoc/>
[JsonIgnore]
public int Stretch => (int)DWRITE_FONT_STRETCH.DWRITE_FONT_STRETCH_NORMAL;
/// <inheritdoc/>
[JsonIgnore]
public int Style => (int)DWRITE_FONT_STYLE.DWRITE_FONT_STYLE_NORMAL;
public static bool operator ==(DalamudAssetFontAndFamilyId? left, DalamudAssetFontAndFamilyId? right) =>
Equals(left, right);
public static bool operator !=(DalamudAssetFontAndFamilyId? left, DalamudAssetFontAndFamilyId? right) =>
!Equals(left, right);
/// <inheritdoc/>
public override bool Equals(object? obj) => obj is DalamudAssetFontAndFamilyId other && this.Equals(other);
/// <inheritdoc/>
public override int GetHashCode() => (int)this.Asset;
/// <inheritdoc/>
public override string ToString() => $"{nameof(DalamudAssetFontAndFamilyId)}:{this.Asset}";
/// <inheritdoc/>
public int FindBestMatch(int weight, int stretch, int style) => 0;
/// <inheritdoc/>
public ImFontPtr AddToBuildToolkit(IFontAtlasBuildToolkitPreBuild tk, in SafeFontConfig config) =>
tk.AddDalamudAssetFont(this.Asset, config);
private bool Equals(DalamudAssetFontAndFamilyId other) => this.Asset == other.Asset;
}

View file

@ -0,0 +1,77 @@
using System.Collections.Generic;
using Dalamud.Interface.ManagedFontAtlas;
using ImGuiNET;
using Newtonsoft.Json;
using TerraFX.Interop.DirectX;
namespace Dalamud.Interface.FontIdentifier;
/// <summary>
/// Represents the default Dalamud font.
/// </summary>
public sealed class DalamudDefaultFontAndFamilyId : IFontId, IFontFamilyId
{
/// <summary>
/// The shared instance of <see cref="DalamudDefaultFontAndFamilyId"/>.
/// </summary>
public static readonly DalamudDefaultFontAndFamilyId Instance = new();
private DalamudDefaultFontAndFamilyId()
{
}
/// <inheritdoc/>
[JsonIgnore]
public string EnglishName => "(Default)";
/// <inheritdoc/>
[JsonIgnore]
public IReadOnlyDictionary<string, string>? LocaleNames => null;
/// <inheritdoc/>
[JsonIgnore]
public IFontFamilyId Family => this;
/// <inheritdoc/>
[JsonIgnore]
public int Weight => (int)DWRITE_FONT_WEIGHT.DWRITE_FONT_WEIGHT_NORMAL;
/// <inheritdoc/>
[JsonIgnore]
public int Stretch => (int)DWRITE_FONT_STRETCH.DWRITE_FONT_STRETCH_NORMAL;
/// <inheritdoc/>
[JsonIgnore]
public int Style => (int)DWRITE_FONT_STYLE.DWRITE_FONT_STYLE_NORMAL;
/// <inheritdoc/>
[JsonIgnore]
public IReadOnlyList<IFontId> Fonts => new List<IFontId> { this }.AsReadOnly();
public static bool operator ==(DalamudDefaultFontAndFamilyId? left, DalamudDefaultFontAndFamilyId? right) =>
left is null == right is null;
public static bool operator !=(DalamudDefaultFontAndFamilyId? left, DalamudDefaultFontAndFamilyId? right) =>
left is null != right is null;
/// <inheritdoc/>
public override bool Equals(object? obj) => obj is DalamudDefaultFontAndFamilyId;
/// <inheritdoc/>
public override int GetHashCode() => 12345678;
/// <inheritdoc/>
public override string ToString() => nameof(DalamudDefaultFontAndFamilyId);
/// <inheritdoc/>
public ImFontPtr AddToBuildToolkit(IFontAtlasBuildToolkitPreBuild tk, in SafeFontConfig config)
=> tk.AddDalamudDefaultFont(config.SizePx, config.GlyphRanges);
// TODO: mergeFont
/// <inheritdoc/>
public int FindBestMatch(int weight, int stretch, int style) => 0;
}

View file

@ -0,0 +1,81 @@
using System.Collections.Generic;
using Dalamud.Interface.GameFonts;
using Dalamud.Interface.ManagedFontAtlas;
using ImGuiNET;
using Newtonsoft.Json;
using TerraFX.Interop.DirectX;
namespace Dalamud.Interface.FontIdentifier;
/// <summary>
/// Represents a font from the game.
/// </summary>
public sealed class GameFontAndFamilyId : IFontId, IFontFamilyId
{
/// <summary>
/// Initializes a new instance of the <see cref="GameFontAndFamilyId"/> class.
/// </summary>
/// <param name="family">The game font family.</param>
public GameFontAndFamilyId(GameFontFamily family) => this.GameFontFamily = family;
/// <summary>
/// Gets the game font family.
/// </summary>
[JsonProperty]
public GameFontFamily GameFontFamily { get; init; }
/// <inheritdoc/>
[JsonIgnore]
public string EnglishName => $"Game: {Enum.GetName(this.GameFontFamily) ?? throw new NotSupportedException()}";
/// <inheritdoc/>
[JsonIgnore]
public IReadOnlyDictionary<string, string>? LocaleNames => null;
/// <inheritdoc/>
[JsonIgnore]
public IFontFamilyId Family => this;
/// <inheritdoc/>
[JsonIgnore]
public int Weight => (int)DWRITE_FONT_WEIGHT.DWRITE_FONT_WEIGHT_NORMAL;
/// <inheritdoc/>
[JsonIgnore]
public int Stretch => (int)DWRITE_FONT_STRETCH.DWRITE_FONT_STRETCH_NORMAL;
/// <inheritdoc/>
[JsonIgnore]
public int Style => (int)DWRITE_FONT_STYLE.DWRITE_FONT_STYLE_NORMAL;
/// <inheritdoc/>
[JsonIgnore]
public IReadOnlyList<IFontId> Fonts => new List<IFontId> { this }.AsReadOnly();
public static bool operator ==(GameFontAndFamilyId? left, GameFontAndFamilyId? right) => Equals(left, right);
public static bool operator !=(GameFontAndFamilyId? left, GameFontAndFamilyId? right) => !Equals(left, right);
/// <inheritdoc/>
public override bool Equals(object? obj) =>
ReferenceEquals(this, obj) || (obj is GameFontAndFamilyId other && this.Equals(other));
/// <inheritdoc/>
public override int GetHashCode() => (int)this.GameFontFamily;
/// <inheritdoc/>
public int FindBestMatch(int weight, int stretch, int style) => 0;
/// <inheritdoc/>
public override string ToString() => $"{nameof(GameFontAndFamilyId)}:{this.GameFontFamily}";
/// <inheritdoc/>
public ImFontPtr AddToBuildToolkit(IFontAtlasBuildToolkitPreBuild tk, in SafeFontConfig config) =>
tk.AddGameGlyphs(new(this.GameFontFamily, config.SizePx), config.GlyphRanges, config.MergeFont);
private bool Equals(GameFontAndFamilyId other) => this.GameFontFamily == other.GameFontFamily;
}

View file

@ -0,0 +1,102 @@
using System.Collections.Generic;
using Dalamud.Interface.GameFonts;
using Dalamud.Utility;
using Newtonsoft.Json;
using TerraFX.Interop.DirectX;
using TerraFX.Interop.Windows;
namespace Dalamud.Interface.FontIdentifier;
/// <summary>
/// Represents a font family identifier.<br />
/// Not intended for plugins to implement.
/// </summary>
public interface IFontFamilyId : IObjectWithLocalizableName
{
/// <summary>
/// Gets the list of fonts under this family.
/// </summary>
[JsonIgnore]
IReadOnlyList<IFontId> Fonts { get; }
/// <summary>
/// Finds the index of the font inside <see cref="Fonts"/> that best matches the given parameters.
/// </summary>
/// <param name="weight">The weight of the font.</param>
/// <param name="stretch">The stretch of the font.</param>
/// <param name="style">The style of the font.</param>
/// <returns>The index of the font. Guaranteed to be a valid index.</returns>
int FindBestMatch(int weight, int stretch, int style);
/// <summary>
/// Gets the list of Dalamud-provided fonts.
/// </summary>
/// <returns>The list of fonts.</returns>
public static List<IFontFamilyId> ListDalamudFonts() =>
new()
{
new DalamudAssetFontAndFamilyId(DalamudAsset.NotoSansJpMedium),
new DalamudAssetFontAndFamilyId(DalamudAsset.InconsolataRegular),
new DalamudAssetFontAndFamilyId(DalamudAsset.FontAwesomeFreeSolid),
};
/// <summary>
/// Gets the list of Game-provided fonts.
/// </summary>
/// <returns>The list of fonts.</returns>
public static List<IFontFamilyId> ListGameFonts() => new()
{
new GameFontAndFamilyId(GameFontFamily.Axis),
new GameFontAndFamilyId(GameFontFamily.Jupiter),
new GameFontAndFamilyId(GameFontFamily.JupiterNumeric),
new GameFontAndFamilyId(GameFontFamily.Meidinger),
new GameFontAndFamilyId(GameFontFamily.MiedingerMid),
new GameFontAndFamilyId(GameFontFamily.TrumpGothic),
};
/// <summary>
/// Gets the list of System-provided fonts.
/// </summary>
/// <param name="refresh">If <c>true</c>, try to refresh the list.</param>
/// <returns>The list of fonts.</returns>
public static unsafe List<IFontFamilyId> ListSystemFonts(bool refresh)
{
using var dwf = default(ComPtr<IDWriteFactory>);
fixed (Guid* piid = &IID.IID_IDWriteFactory)
{
DirectX.DWriteCreateFactory(
DWRITE_FACTORY_TYPE.DWRITE_FACTORY_TYPE_SHARED,
piid,
(IUnknown**)dwf.GetAddressOf()).ThrowOnError();
}
using var sfc = default(ComPtr<IDWriteFontCollection>);
dwf.Get()->GetSystemFontCollection(sfc.GetAddressOf(), refresh).ThrowOnError();
var count = (int)sfc.Get()->GetFontFamilyCount();
var result = new List<IFontFamilyId>(count);
for (var i = 0; i < count; i++)
{
using var ff = default(ComPtr<IDWriteFontFamily>);
if (sfc.Get()->GetFontFamily((uint)i, ff.GetAddressOf()).FAILED)
{
// Ignore errors, if any
continue;
}
try
{
result.Add(SystemFontFamilyId.FromDWriteFamily(ff));
}
catch
{
// ignore
}
}
return result;
}
}

View file

@ -0,0 +1,40 @@
using Dalamud.Interface.ManagedFontAtlas;
using ImGuiNET;
namespace Dalamud.Interface.FontIdentifier;
/// <summary>
/// Represents a font identifier.<br />
/// Not intended for plugins to implement.
/// </summary>
public interface IFontId : IObjectWithLocalizableName
{
/// <summary>
/// Gets the associated font family.
/// </summary>
IFontFamilyId Family { get; }
/// <summary>
/// Gets the font weight, ranging from 1 to 999.
/// </summary>
int Weight { get; }
/// <summary>
/// Gets the font stretch, ranging from 1 to 9.
/// </summary>
int Stretch { get; }
/// <summary>
/// Gets the font style. Treat as an opaque value.
/// </summary>
int Style { get; }
/// <summary>
/// Adds this font to the given font build toolkit.
/// </summary>
/// <param name="tk">The font build toolkit.</param>
/// <param name="config">The font configuration. Some parameters may be ignored.</param>
/// <returns>The added font.</returns>
ImFontPtr AddToBuildToolkit(IFontAtlasBuildToolkitPreBuild tk, in SafeFontConfig config);
}

View file

@ -0,0 +1,52 @@
using Dalamud.Interface.ManagedFontAtlas;
using ImGuiNET;
namespace Dalamud.Interface.FontIdentifier;
/// <summary>
/// Represents a user's choice of font(s).<br />
/// Not intended for plugins to implement.
/// </summary>
public interface IFontSpec
{
/// <summary>
/// Gets the font size in pixels.
/// </summary>
float SizePx { get; }
/// <summary>
/// Gets the font size in points.
/// </summary>
float SizePt { get; }
/// <summary>
/// Gets the line height in pixels.
/// </summary>
float LineHeightPx { get; }
/// <summary>
/// Creates a font handle corresponding to this font specification.
/// </summary>
/// <param name="atlas">The atlas to bind this font handle to.</param>
/// <param name="callback">Optional callback to be called after creating the font handle.</param>
/// <returns>The new font handle.</returns>
/// <remarks><see cref="IFontAtlasBuildToolkit.Font"/> will be set when <paramref name="callback"/> is invoked.
/// </remarks>
IFontHandle CreateFontHandle(IFontAtlas atlas, FontAtlasBuildStepDelegate? callback = null);
/// <summary>
/// Adds this font to the given font build toolkit.
/// </summary>
/// <param name="tk">The font build toolkit.</param>
/// <param name="mergeFont">The font to merge to.</param>
/// <returns>The added font.</returns>
ImFontPtr AddToBuildToolkit(IFontAtlasBuildToolkitPreBuild tk, ImFontPtr mergeFont = default);
/// <summary>
/// Represents this font specification, preferrably in the requested locale.
/// </summary>
/// <param name="localeCode">The locale code. Must be in lowercase(invariant).</param>
/// <returns>The value.</returns>
string ToLocalizedString(string localeCode);
}

View file

@ -0,0 +1,76 @@
using System.Collections.Generic;
using Dalamud.Utility;
using TerraFX.Interop.DirectX;
namespace Dalamud.Interface.FontIdentifier;
/// <summary>
/// Represents an object with localizable names.
/// </summary>
public interface IObjectWithLocalizableName
{
/// <summary>
/// Gets the name, preferrably in English.
/// </summary>
string EnglishName { get; }
/// <summary>
/// Gets the names per locales.
/// </summary>
IReadOnlyDictionary<string, string>? LocaleNames { get; }
/// <summary>
/// Gets the name in the requested locale if available; otherwise, <see cref="EnglishName"/>.
/// </summary>
/// <param name="localeCode">The locale code. Must be in lowercase(invariant).</param>
/// <returns>The value.</returns>
string GetLocalizedName(string localeCode)
{
if (this.LocaleNames is null)
return this.EnglishName;
if (this.LocaleNames.TryGetValue(localeCode, out var v))
return v;
foreach (var (a, b) in this.LocaleNames)
{
if (a.StartsWith(localeCode))
return b;
}
return this.EnglishName;
}
/// <summary>
/// Resolves all names per locales.
/// </summary>
/// <param name="fn">The names.</param>
/// <returns>A new dictionary mapping from locale code to localized names.</returns>
internal static unsafe IReadOnlyDictionary<string, string> GetLocaleNames(IDWriteLocalizedStrings* fn)
{
var count = fn->GetCount();
var maxStrLen = 0u;
for (var i = 0u; i < count; i++)
{
var length = 0u;
fn->GetStringLength(i, &length).ThrowOnError();
maxStrLen = Math.Max(maxStrLen, length);
fn->GetLocaleNameLength(i, &length).ThrowOnError();
maxStrLen = Math.Max(maxStrLen, length);
}
maxStrLen++;
var buf = stackalloc char[(int)maxStrLen];
var result = new Dictionary<string, string>((int)count);
for (var i = 0u; i < count; i++)
{
fn->GetLocaleName(i, (ushort*)buf, maxStrLen).ThrowOnError();
var key = new string(buf);
fn->GetString(i, (ushort*)buf, maxStrLen).ThrowOnError();
var value = new string(buf);
result[key.ToLowerInvariant()] = value;
}
return result;
}
}

View file

@ -0,0 +1,154 @@
using System.Collections;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using System.Text;
using Dalamud.Interface.ManagedFontAtlas;
using Dalamud.Interface.Utility;
using ImGuiNET;
using Newtonsoft.Json;
namespace Dalamud.Interface.FontIdentifier;
/// <summary>
/// Represents a user's choice of a single font.
/// </summary>
[SuppressMessage(
"StyleCop.CSharp.OrderingRules",
"SA1206:Declaration keywords should follow order",
Justification = "public required")]
public record SingleFontSpec : IFontSpec
{
/// <summary>
/// Gets the font id.
/// </summary>
[JsonProperty]
public required IFontId FontId { get; init; }
/// <inheritdoc/>
[JsonProperty]
public float SizePx { get; init; } = 16;
/// <inheritdoc/>
[JsonIgnore]
public float SizePt
{
get => (this.SizePx * 3) / 4;
init => this.SizePx = (value * 4) / 3;
}
/// <inheritdoc/>
[JsonIgnore]
public float LineHeightPx => MathF.Round(this.SizePx * this.LineHeight);
/// <summary>
/// Gets the line height ratio to the font size.
/// </summary>
[JsonProperty]
public float LineHeight { get; init; } = 1f;
/// <summary>
/// Gets the glyph offset in pixels.
/// </summary>
[JsonProperty]
public Vector2 GlyphOffset { get; init; }
/// <summary>
/// Gets the letter spacing in pixels.
/// </summary>
[JsonProperty]
public float LetterSpacing { get; init; }
/// <summary>
/// Gets the glyph ranges.
/// </summary>
[JsonProperty]
public ushort[]? GlyphRanges { get; init; }
/// <inheritdoc/>
public string ToLocalizedString(string localeCode)
{
var sb = new StringBuilder();
sb.Append(this.FontId.Family.GetLocalizedName(localeCode));
sb.Append($"({this.FontId.GetLocalizedName(localeCode)}, {this.SizePt}pt");
if (Math.Abs(this.LineHeight - 1f) > 0.000001f)
sb.Append($", LH={this.LineHeight:0.##}");
if (this.GlyphOffset != default)
sb.Append($", O={this.GlyphOffset.X:0.##},{this.GlyphOffset.Y:0.##}");
if (this.LetterSpacing != 0f)
sb.Append($", LS={this.LetterSpacing:0.##}");
sb.Append(')');
return sb.ToString();
}
/// <inheritdoc/>
public override string ToString() => this.ToLocalizedString("en");
/// <inheritdoc/>
public IFontHandle CreateFontHandle(IFontAtlas atlas, FontAtlasBuildStepDelegate? callback = null) =>
atlas.NewDelegateFontHandle(tk =>
{
tk.OnPreBuild(e => e.Font = this.AddToBuildToolkit(e));
callback?.Invoke(tk);
});
/// <inheritdoc/>
public ImFontPtr AddToBuildToolkit(IFontAtlasBuildToolkitPreBuild tk, ImFontPtr mergeFont = default)
{
var font = this.FontId.AddToBuildToolkit(
tk,
new()
{
SizePx = this.SizePx,
GlyphRanges = this.GlyphRanges,
MergeFont = mergeFont,
});
tk.RegisterPostBuild(
() =>
{
// Multiplication by scale will be done with global scale, outside of this handling.
var scale = tk.GetFontScaleMode(font) == FontScaleMode.UndoGlobalScale ? 1 / tk.Scale : 1;
var roundUnit = tk.GetFontScaleMode(font) == FontScaleMode.SkipHandling ? 1 : 1 / tk.Scale;
var newAscent = MathF.Round((font.Ascent * this.LineHeight) / roundUnit) * roundUnit;
var newFontSize = MathF.Round((font.FontSize * this.LineHeight) / roundUnit) * roundUnit;
var shiftDown = MathF.Round((newFontSize - font.FontSize) / 2f / roundUnit) * roundUnit;
font.Ascent = newAscent;
font.FontSize = newFontSize;
font.Descent = newFontSize - font.Ascent;
var lookup = new BitArray(ushort.MaxValue + 1, this.GlyphRanges is null);
if (this.GlyphRanges is not null)
{
for (var i = 0; i < this.GlyphRanges.Length && this.GlyphRanges[i] != 0; i += 2)
{
var to = (int)this.GlyphRanges[i + 1];
for (var j = this.GlyphRanges[i]; j <= to; j++)
lookup[j] = true;
}
}
var dax = MathF.Round((this.LetterSpacing * scale) / roundUnit) * roundUnit;
var dxy0 = this.GlyphOffset * scale;
dxy0 /= roundUnit;
dxy0 = new(MathF.Round(dxy0.X), MathF.Round(dxy0.Y));
dxy0 *= roundUnit;
dxy0.Y += shiftDown;
var dxy = new Vector4(dxy0, dxy0.X, dxy0.Y);
foreach (ref var glyphReal in font.GlyphsWrapped().DataSpan)
{
if (!lookup[glyphReal.Codepoint])
continue;
glyphReal.XY += dxy;
glyphReal.AdvanceX += dax;
}
});
return font;
}
}

View file

@ -0,0 +1,181 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Dalamud.Utility;
using Newtonsoft.Json;
using TerraFX.Interop.DirectX;
using TerraFX.Interop.Windows;
namespace Dalamud.Interface.FontIdentifier;
/// <summary>
/// Represents a font from system.
/// </summary>
public sealed class SystemFontFamilyId : IFontFamilyId
{
[JsonIgnore]
private IReadOnlyList<IFontId>? fontsLazy;
/// <summary>
/// Initializes a new instance of the <see cref="SystemFontFamilyId"/> class.
/// </summary>
/// <param name="englishName">The font name in English.</param>
/// <param name="localeNames">The localized font name for display purposes.</param>
[JsonConstructor]
internal SystemFontFamilyId(string englishName, IReadOnlyDictionary<string, string> localeNames)
{
this.EnglishName = englishName;
this.LocaleNames = localeNames;
}
/// <summary>
/// Initializes a new instance of the <see cref="SystemFontFamilyId"/> class.
/// </summary>
/// <param name="localeNames">The localized font name for display purposes.</param>
internal SystemFontFamilyId(IReadOnlyDictionary<string, string> localeNames)
{
if (localeNames.TryGetValue("en-us", out var name))
this.EnglishName = name;
else if (localeNames.TryGetValue("en", out name))
this.EnglishName = name;
else
this.EnglishName = localeNames.Values.First();
this.LocaleNames = localeNames;
}
/// <inheritdoc/>
[JsonProperty]
public string EnglishName { get; init; }
/// <inheritdoc/>
[JsonProperty]
public IReadOnlyDictionary<string, string>? LocaleNames { get; }
/// <inheritdoc/>
[JsonIgnore]
public IReadOnlyList<IFontId> Fonts => this.fontsLazy ??= this.GetFonts();
public static bool operator ==(SystemFontFamilyId? left, SystemFontFamilyId? right) => Equals(left, right);
public static bool operator !=(SystemFontFamilyId? left, SystemFontFamilyId? right) => !Equals(left, right);
/// <inheritdoc/>
public int FindBestMatch(int weight, int stretch, int style)
{
using var matchingFont = default(ComPtr<IDWriteFont>);
var candidates = this.Fonts.ToList();
var minGap = int.MaxValue;
foreach (var c in candidates)
minGap = Math.Min(minGap, Math.Abs(c.Weight - weight));
candidates.RemoveAll(c => Math.Abs(c.Weight - weight) != minGap);
minGap = int.MaxValue;
foreach (var c in candidates)
minGap = Math.Min(minGap, Math.Abs(c.Stretch - stretch));
candidates.RemoveAll(c => Math.Abs(c.Stretch - stretch) != minGap);
if (candidates.Any(x => x.Style == style))
candidates.RemoveAll(x => x.Style != style);
else if (candidates.Any(x => x.Style == (int)DWRITE_FONT_STYLE.DWRITE_FONT_STYLE_NORMAL))
candidates.RemoveAll(x => x.Style != (int)DWRITE_FONT_STYLE.DWRITE_FONT_STYLE_NORMAL);
if (!candidates.Any())
return 0;
for (var i = 0; i < this.Fonts.Count; i++)
{
if (Equals(this.Fonts[i], candidates[0]))
return i;
}
return 0;
}
/// <inheritdoc/>
public override string ToString() => $"{nameof(SystemFontFamilyId)}:{this.EnglishName}";
/// <inheritdoc/>
public override bool Equals(object? obj) =>
ReferenceEquals(this, obj) || (obj is SystemFontFamilyId other && this.Equals(other));
/// <inheritdoc/>
public override int GetHashCode() => this.EnglishName.GetHashCode();
/// <summary>
/// Create a new instance of <see cref="SystemFontFamilyId"/> from an <see cref="IDWriteFontFamily"/>.
/// </summary>
/// <param name="family">The family.</param>
/// <returns>The new instance.</returns>
internal static unsafe SystemFontFamilyId FromDWriteFamily(ComPtr<IDWriteFontFamily> family)
{
using var fn = default(ComPtr<IDWriteLocalizedStrings>);
family.Get()->GetFamilyNames(fn.GetAddressOf()).ThrowOnError();
return new(IObjectWithLocalizableName.GetLocaleNames(fn));
}
private unsafe IReadOnlyList<IFontId> GetFonts()
{
using var dwf = default(ComPtr<IDWriteFactory>);
fixed (Guid* piid = &IID.IID_IDWriteFactory)
{
DirectX.DWriteCreateFactory(
DWRITE_FACTORY_TYPE.DWRITE_FACTORY_TYPE_SHARED,
piid,
(IUnknown**)dwf.GetAddressOf()).ThrowOnError();
}
using var sfc = default(ComPtr<IDWriteFontCollection>);
dwf.Get()->GetSystemFontCollection(sfc.GetAddressOf(), false).ThrowOnError();
var familyIndex = 0u;
BOOL exists = false;
fixed (void* pName = this.EnglishName)
sfc.Get()->FindFamilyName((ushort*)pName, &familyIndex, &exists).ThrowOnError();
if (!exists)
throw new FileNotFoundException($"Font \"{this.EnglishName}\" not found.");
using var family = default(ComPtr<IDWriteFontFamily>);
sfc.Get()->GetFontFamily(familyIndex, family.GetAddressOf()).ThrowOnError();
var fontCount = (int)family.Get()->GetFontCount();
var fonts = new List<IFontId>(fontCount);
for (var i = 0; i < fontCount; i++)
{
using var font = default(ComPtr<IDWriteFont>);
if (family.Get()->GetFont((uint)i, font.GetAddressOf()).FAILED)
{
// Ignore errors, if any
continue;
}
if (font.Get()->GetSimulations() != DWRITE_FONT_SIMULATIONS.DWRITE_FONT_SIMULATIONS_NONE)
{
// No simulation support
continue;
}
fonts.Add(new SystemFontId(this, font));
}
fonts.Sort(
(a, b) =>
{
var comp = a.Weight.CompareTo(b.Weight);
if (comp != 0)
return comp;
comp = a.Stretch.CompareTo(b.Stretch);
if (comp != 0)
return comp;
return a.Style.CompareTo(b.Style);
});
return fonts;
}
private bool Equals(SystemFontFamilyId other) => this.EnglishName == other.EnglishName;
}

View file

@ -0,0 +1,163 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Dalamud.Interface.ManagedFontAtlas;
using Dalamud.Utility;
using ImGuiNET;
using Newtonsoft.Json;
using TerraFX.Interop.DirectX;
using TerraFX.Interop.Windows;
namespace Dalamud.Interface.FontIdentifier;
/// <summary>
/// Represents a font installed in the system.
/// </summary>
public sealed class SystemFontId : IFontId
{
/// <summary>
/// Initializes a new instance of the <see cref="SystemFontId"/> class.
/// </summary>
/// <param name="family">The parent font family.</param>
/// <param name="font">The font.</param>
internal unsafe SystemFontId(SystemFontFamilyId family, ComPtr<IDWriteFont> font)
{
this.Family = family;
this.Weight = (int)font.Get()->GetWeight();
this.Stretch = (int)font.Get()->GetStretch();
this.Style = (int)font.Get()->GetStyle();
using var fn = default(ComPtr<IDWriteLocalizedStrings>);
font.Get()->GetFaceNames(fn.GetAddressOf()).ThrowOnError();
this.LocaleNames = IObjectWithLocalizableName.GetLocaleNames(fn);
if (this.LocaleNames.TryGetValue("en-us", out var name))
this.EnglishName = name;
else if (this.LocaleNames.TryGetValue("en", out name))
this.EnglishName = name;
else
this.EnglishName = this.LocaleNames.Values.First();
}
[JsonConstructor]
private SystemFontId(string englishName, IReadOnlyDictionary<string, string> localeNames, IFontFamilyId family)
{
this.EnglishName = englishName;
this.LocaleNames = localeNames;
this.Family = family;
}
/// <inheritdoc/>
[JsonProperty]
public string EnglishName { get; init; }
/// <inheritdoc/>
[JsonProperty]
public IReadOnlyDictionary<string, string>? LocaleNames { get; }
/// <inheritdoc/>
[JsonProperty]
public IFontFamilyId Family { get; init; }
/// <inheritdoc/>
[JsonProperty]
public int Weight { get; init; } = (int)DWRITE_FONT_WEIGHT.DWRITE_FONT_WEIGHT_NORMAL;
/// <inheritdoc/>
[JsonProperty]
public int Stretch { get; init; } = (int)DWRITE_FONT_STRETCH.DWRITE_FONT_STRETCH_NORMAL;
/// <inheritdoc/>
[JsonProperty]
public int Style { get; init; } = (int)DWRITE_FONT_STYLE.DWRITE_FONT_STYLE_NORMAL;
public static bool operator ==(SystemFontId? left, SystemFontId? right) => Equals(left, right);
public static bool operator !=(SystemFontId? left, SystemFontId? right) => !Equals(left, right);
/// <inheritdoc/>
public override bool Equals(object? obj) =>
ReferenceEquals(this, obj) || (obj is SystemFontId other && this.Equals(other));
/// <inheritdoc/>
public override int GetHashCode() => HashCode.Combine(this.Family, this.Weight, this.Stretch, this.Style);
/// <inheritdoc/>
public override string ToString() =>
$"{nameof(SystemFontId)}:{this.Weight}:{this.Stretch}:{this.Style}:{this.Family}";
/// <inheritdoc/>
public ImFontPtr AddToBuildToolkit(IFontAtlasBuildToolkitPreBuild tk, in SafeFontConfig config)
{
var (path, index) = this.GetFileAndIndex();
return tk.AddFontFromFile(path, config with { FontNo = index });
}
/// <summary>
/// Gets the file containing this font, and the font index within.
/// </summary>
/// <returns>The path and index.</returns>
public unsafe (string Path, int Index) GetFileAndIndex()
{
using var dwf = default(ComPtr<IDWriteFactory>);
fixed (Guid* piid = &IID.IID_IDWriteFactory)
{
DirectX.DWriteCreateFactory(
DWRITE_FACTORY_TYPE.DWRITE_FACTORY_TYPE_SHARED,
piid,
(IUnknown**)dwf.GetAddressOf()).ThrowOnError();
}
using var sfc = default(ComPtr<IDWriteFontCollection>);
dwf.Get()->GetSystemFontCollection(sfc.GetAddressOf(), false).ThrowOnError();
var familyIndex = 0u;
BOOL exists = false;
fixed (void* name = this.Family.EnglishName)
sfc.Get()->FindFamilyName((ushort*)name, &familyIndex, &exists).ThrowOnError();
if (!exists)
throw new FileNotFoundException($"Font \"{this.Family.EnglishName}\" not found.");
using var family = default(ComPtr<IDWriteFontFamily>);
sfc.Get()->GetFontFamily(familyIndex, family.GetAddressOf()).ThrowOnError();
using var font = default(ComPtr<IDWriteFont>);
family.Get()->GetFirstMatchingFont(
(DWRITE_FONT_WEIGHT)this.Weight,
(DWRITE_FONT_STRETCH)this.Stretch,
(DWRITE_FONT_STYLE)this.Style,
font.GetAddressOf()).ThrowOnError();
using var fface = default(ComPtr<IDWriteFontFace>);
font.Get()->CreateFontFace(fface.GetAddressOf()).ThrowOnError();
var fileCount = 0;
fface.Get()->GetFiles((uint*)&fileCount, null).ThrowOnError();
if (fileCount != 1)
throw new NotSupportedException();
using var ffile = default(ComPtr<IDWriteFontFile>);
fface.Get()->GetFiles((uint*)&fileCount, ffile.GetAddressOf()).ThrowOnError();
void* refKey;
var refKeySize = 0u;
ffile.Get()->GetReferenceKey(&refKey, &refKeySize).ThrowOnError();
using var floader = default(ComPtr<IDWriteFontFileLoader>);
ffile.Get()->GetLoader(floader.GetAddressOf()).ThrowOnError();
using var flocal = default(ComPtr<IDWriteLocalFontFileLoader>);
floader.As(&flocal).ThrowOnError();
var pathSize = 0u;
flocal.Get()->GetFilePathLengthFromKey(refKey, refKeySize, &pathSize).ThrowOnError();
var path = stackalloc char[(int)pathSize + 1];
flocal.Get()->GetFilePathFromKey(refKey, refKeySize, (ushort*)path, pathSize + 1).ThrowOnError();
return (new(path, 0, (int)pathSize), (int)fface.Get()->GetIndex());
}
private bool Equals(SystemFontId other) => this.Family.Equals(other.Family) && this.Weight == other.Weight &&
this.Stretch == other.Stretch && this.Style == other.Style;
}

File diff suppressed because it is too large Load diff

View file

@ -141,6 +141,13 @@ internal class DalamudCommands : IServiceType
"Toggle Dalamud UI display modes. Native UI modifications may also be affected by this, but that depends on the plugin."),
});
commandManager.AddHandler("/xlprofiler", new CommandInfo(this.OnOpenProfilerCommand)
{
HelpMessage = Loc.Localize(
"DalamudProfilerHelp",
"Open Dalamud's startup timing profiler."),
});
commandManager.AddHandler("/imdebug", new CommandInfo(this.OnDebugImInfoCommand)
{
HelpMessage = "ImGui DEBUG",
@ -409,4 +416,9 @@ internal class DalamudCommands : IServiceType
}
}
}
private void OnOpenProfilerCommand(string command, string arguments)
{
Service<DalamudInterface>.Get().ToggleProfilerWindow();
}
}

View file

@ -11,6 +11,7 @@ using System.Text.Unicode;
using Dalamud.Game.Text;
using Dalamud.Hooking.WndProcHook;
using Dalamud.Interface.GameFonts;
using Dalamud.Interface.Internal.ManagedAsserts;
using Dalamud.Interface.ManagedFontAtlas.Internals;
using Dalamud.Interface.Utility;
@ -28,7 +29,6 @@ namespace Dalamud.Interface.Internal;
[ServiceManager.BlockingEarlyLoadedService]
internal sealed unsafe class DalamudIme : IDisposable, IServiceType
{
private const int ImGuiContextTextStateOffset = 0x4588;
private const int CImGuiStbTextCreateUndoOffset = 0xB57A0;
private const int CImGuiStbTextUndoOffset = 0xB59C0;
@ -178,7 +178,7 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType
internal char InputModeIcon { get; private set; }
private static ImGuiInputTextState* TextState =>
(ImGuiInputTextState*)(ImGui.GetCurrentContext() + ImGuiContextTextStateOffset);
(ImGuiInputTextState*)(ImGui.GetCurrentContext() + ImGuiContextOffsets.TextStateOffset);
/// <inheritdoc/>
public void Dispose()

View file

@ -12,6 +12,7 @@ using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.ClientState.Keys;
using Dalamud.Game.Gui;
using Dalamud.Game.Internal;
using Dalamud.Hooking;
using Dalamud.Interface.Animation.EasingFunctions;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Internal.ManagedAsserts;
@ -89,7 +90,7 @@ internal class DalamudInterface : IDisposable, IServiceType
private bool isImPlotDrawDemoWindow = false;
private bool isImGuiTestWindowsInMonospace = false;
private bool isImGuiDrawMetricsWindow = false;
[ServiceManager.ServiceConstructor]
private DalamudInterface(
Dalamud dalamud,
@ -188,7 +189,9 @@ internal class DalamudInterface : IDisposable, IServiceType
this.creditsDarkeningAnimation.Point1 = Vector2.Zero;
this.creditsDarkeningAnimation.Point2 = new Vector2(CreditsDarkeningMaxAlpha);
}
private delegate nint CrashDebugDelegate(nint self);
/// <summary>
/// Gets the number of frames since Dalamud has loaded.
/// </summary>
@ -744,28 +747,48 @@ internal class DalamudInterface : IDisposable, IServiceType
}
ImGui.Separator();
if (ImGui.MenuItem("Access Violation"))
if (ImGui.BeginMenu("Crash game"))
{
Marshal.ReadByte(IntPtr.Zero);
}
if (ImGui.MenuItem("Crash game (nullptr)"))
{
unsafe
if (ImGui.MenuItem("Access Violation"))
{
var framework = Framework.Instance();
framework->UIModule = (UIModule*)0;
}
}
if (ImGui.MenuItem("Crash game (non-nullptr)"))
{
unsafe
Marshal.ReadByte(IntPtr.Zero);
}
if (ImGui.MenuItem("Set UiModule to NULL"))
{
var framework = Framework.Instance();
framework->UIModule = (UIModule*)0x12345678;
unsafe
{
var framework = Framework.Instance();
framework->UIModule = (UIModule*)0;
}
}
if (ImGui.MenuItem("Set UiModule to invalid ptr"))
{
unsafe
{
var framework = Framework.Instance();
framework->UIModule = (UIModule*)0x12345678;
}
}
if (ImGui.MenuItem("Deref nullptr in Hook"))
{
unsafe
{
var hook = Hook<CrashDebugDelegate>.FromAddress(
(nint)UIModule.StaticVTable.GetUIInputData,
self =>
{
_ = *(byte*)0;
return (nint)UIModule.Instance()->GetUIInputData();
});
hook.Enable();
}
}
ImGui.EndMenu();
}
if (ImGui.MenuItem("Report crashes at shutdown", null, this.configuration.ReportShutdownCrashes))

View file

@ -1,41 +1,14 @@
using System.Numerics;
using Dalamud.Utility;
using ImGuiScene;
namespace Dalamud.Interface.Internal;
/// <summary>
/// Base TextureWrap interface for all Dalamud-owned texture wraps.
/// Used to avoid referencing ImGuiScene.
/// </summary>
public interface IDalamudTextureWrap : IDisposable
{
/// <summary>
/// Gets a texture handle suitable for direct use with ImGui functions.
/// </summary>
IntPtr ImGuiHandle { get; }
/// <summary>
/// Gets the width of the texture.
/// </summary>
int Width { get; }
/// <summary>
/// Gets the height of the texture.
/// </summary>
int Height { get; }
/// <summary>
/// Gets the size vector of the texture using Width, Height.
/// </summary>
Vector2 Size => new(this.Width, this.Height);
}
/// <summary>
/// Safety harness for ImGuiScene textures that will defer destruction until
/// the end of the frame.
/// </summary>
public class DalamudTextureWrap : IDalamudTextureWrap
public class DalamudTextureWrap : IDalamudTextureWrap, IDeferredDisposable
{
private readonly TextureWrap wrappedWrap;
@ -83,7 +56,7 @@ public class DalamudTextureWrap : IDalamudTextureWrap
/// <summary>
/// Actually dispose the wrapped texture.
/// </summary>
internal void RealDispose()
void IDeferredDisposable.RealDispose()
{
this.wrappedWrap.Dispose();
}

View file

@ -0,0 +1,55 @@
using System.Numerics;
using TerraFX.Interop.Windows;
namespace Dalamud.Interface.Internal;
/// <summary>
/// Base TextureWrap interface for all Dalamud-owned texture wraps.
/// Used to avoid referencing ImGuiScene.
/// </summary>
public interface IDalamudTextureWrap : IDisposable
{
/// <summary>
/// Gets a texture handle suitable for direct use with ImGui functions.
/// </summary>
IntPtr ImGuiHandle { get; }
/// <summary>
/// Gets the width of the texture.
/// </summary>
int Width { get; }
/// <summary>
/// Gets the height of the texture.
/// </summary>
int Height { get; }
/// <summary>
/// Gets the size vector of the texture using Width, Height.
/// </summary>
Vector2 Size => new(this.Width, this.Height);
/// <summary>
/// Creates a new reference to the resource being pointed by this instance of <see cref="IDalamudTextureWrap"/>.
/// </summary>
/// <returns>The new reference to this texture wrap.</returns>
/// <remarks>
/// On calling this function, a new instance of <see cref="IDalamudTextureWrap"/> will be returned, but with
/// the same <see cref="ImGuiHandle"/>. The new instance must be <see cref="IDisposable.Dispose"/>d, as the backing
/// resource will stay alive until all the references are released. The old instance may be disposed as needed,
/// once this function returns; the new instance will stay alive regardless of whether the old instance has been
/// disposed.<br />
/// Primary purpose of this function is to share textures across plugin boundaries. When texture wraps get passed
/// across plugin boundaries for use for an indeterminate duration, the receiver should call this function to
/// obtain a new reference to the texture received, so that it gets its own "copy" of the texture and the caller
/// may dispose the texture anytime without any care for the receiver.<br />
/// The default implementation will treat <see cref="ImGuiHandle"/> as an <see cref="IUnknown"/>.
/// </remarks>
unsafe IDalamudTextureWrap CreateWrapSharingLowLevelResource()
{
// Dalamud specific: IDalamudTextureWrap always points to an ID3D11ShaderResourceView.
var handle = (IUnknown*)this.ImGuiHandle;
return new UnknownTextureWrap(handle, this.Width, this.Height, true);
}
}

View file

@ -52,7 +52,6 @@ internal sealed unsafe class ImGuiClipboardFunctionProvider : IServiceType, IDis
private ImGuiClipboardFunctionProvider(InterfaceManager.InterfaceManagerWithScene imws)
{
// Effectively waiting for ImGui to become available.
_ = imws;
Debug.Assert(ImGuiHelpers.IsImGuiInitialized, "IMWS initialized but IsImGuiInitialized is false?");
var io = ImGui.GetIO();

View file

@ -0,0 +1,133 @@
using System.Diagnostics;
using System.Linq;
using System.Numerics;
using Dalamud.Hooking;
using ImGuiNET;
namespace Dalamud.Interface.Internal;
/// <summary>
/// Fixes ImDrawList not correctly dealing with the current texture for that draw list not in tune with the global
/// state. Currently, ImDrawList::AddPolyLine and ImDrawList::AddRectFilled are affected.
///
/// * The implementation for AddRectFilled is entirely replaced with the hook below.
/// * The implementation for AddPolyLine is wrapped with Push/PopTextureID.
///
/// TODO:
/// * imgui_draw.cpp:1433 ImDrawList::AddRectFilled
/// The if block needs a PushTextureID(_Data->TexIdCommon)/PopTextureID() block,
/// if _Data->TexIdCommon != _CmdHeader.TextureId.
/// * imgui_draw.cpp:729 ImDrawList::AddPolyLine
/// The if block always needs to call PushTextureID if the abovementioned condition is not met.
/// Change push_texture_id to only have one condition.
/// </summary>
[ServiceManager.EarlyLoadedService]
internal sealed unsafe class ImGuiDrawListFixProvider : IServiceType, IDisposable
{
private const int CImGuiImDrawListAddPolyLineOffset = 0x589B0;
private const int CImGuiImDrawListAddRectFilled = 0x59FD0;
private const int CImGuiImDrawListSharedDataTexIdCommonOffset = 0;
private readonly Hook<ImDrawListAddPolyLine> hookImDrawListAddPolyline;
private readonly Hook<ImDrawListAddRectFilled> hookImDrawListAddRectFilled;
[ServiceManager.ServiceConstructor]
private ImGuiDrawListFixProvider(InterfaceManager.InterfaceManagerWithScene imws)
{
// Force cimgui.dll to be loaded.
_ = ImGui.GetCurrentContext();
var cimgui = Process.GetCurrentProcess().Modules.Cast<ProcessModule>()
.First(x => x.ModuleName == "cimgui.dll")
.BaseAddress;
this.hookImDrawListAddPolyline = Hook<ImDrawListAddPolyLine>.FromAddress(
cimgui + CImGuiImDrawListAddPolyLineOffset,
this.ImDrawListAddPolylineDetour);
this.hookImDrawListAddRectFilled = Hook<ImDrawListAddRectFilled>.FromAddress(
cimgui + CImGuiImDrawListAddRectFilled,
this.ImDrawListAddRectFilledDetour);
this.hookImDrawListAddPolyline.Enable();
this.hookImDrawListAddRectFilled.Enable();
}
private delegate void ImDrawListAddPolyLine(
ImDrawListPtr drawListPtr,
ref Vector2 points,
int pointsCount,
uint color,
ImDrawFlags flags,
float thickness);
private delegate void ImDrawListAddRectFilled(
ImDrawListPtr drawListPtr,
ref Vector2 min,
ref Vector2 max,
uint col,
float rounding,
ImDrawFlags flags);
/// <inheritdoc/>
public void Dispose()
{
this.hookImDrawListAddPolyline.Dispose();
this.hookImDrawListAddRectFilled.Dispose();
}
private void ImDrawListAddRectFilledDetour(
ImDrawListPtr drawListPtr,
ref Vector2 min,
ref Vector2 max,
uint col,
float rounding,
ImDrawFlags flags)
{
// Skip drawing if we're drawing something with alpha value of 0.
if ((col & 0xFF000000) == 0)
return;
if (rounding < 0.5f || (flags & ImDrawFlags.RoundCornersMask) == ImDrawFlags.RoundCornersMask)
{
// Take the fast path of drawing two triangles if no rounded corners are required.
var texIdCommon = *(nint*)(drawListPtr._Data + CImGuiImDrawListSharedDataTexIdCommonOffset);
var pushTextureId = texIdCommon != drawListPtr._CmdHeader.TextureId;
if (pushTextureId)
drawListPtr.PushTextureID(texIdCommon);
drawListPtr.PrimReserve(6, 4);
drawListPtr.PrimRect(min, max, col);
if (pushTextureId)
drawListPtr.PopTextureID();
}
else
{
// Defer drawing rectangle with rounded corners to path drawing operations.
// Note that this may have a slightly different extent behaviors from the above if case.
// This is how it is in imgui_draw.cpp.
drawListPtr.PathRect(min, max, rounding, flags);
drawListPtr.PathFillConvex(col);
}
}
private void ImDrawListAddPolylineDetour(
ImDrawListPtr drawListPtr,
ref Vector2 points,
int pointsCount,
uint color,
ImDrawFlags flags,
float thickness)
{
var texIdCommon = *(nint*)(drawListPtr._Data + CImGuiImDrawListSharedDataTexIdCommonOffset);
var pushTextureId = texIdCommon != drawListPtr._CmdHeader.TextureId;
if (pushTextureId)
drawListPtr.PushTextureID(texIdCommon);
this.hookImDrawListAddPolyline.Original(drawListPtr, ref points, pointsCount, color, flags, thickness);
if (pushTextureId)
drawListPtr.PopTextureID();
}
}

View file

@ -62,7 +62,7 @@ internal class InterfaceManager : IDisposable, IServiceType
/// </summary>
public const float DefaultFontSizePx = (DefaultFontSizePt * 4.0f) / 3.0f;
private readonly ConcurrentBag<DalamudTextureWrap> deferredDisposeTextures = new();
private readonly ConcurrentBag<IDeferredDisposable> deferredDisposeTextures = new();
private readonly ConcurrentBag<ILockedImFont> deferredDisposeImFontLockeds = new();
[ServiceManager.ServiceDependency]
@ -402,7 +402,7 @@ internal class InterfaceManager : IDisposable, IServiceType
/// Enqueue a texture to be disposed at the end of the frame.
/// </summary>
/// <param name="wrap">The texture.</param>
public void EnqueueDeferredDispose(DalamudTextureWrap wrap)
public void EnqueueDeferredDispose(IDeferredDisposable wrap)
{
this.deferredDisposeTextures.Add(wrap);
}
@ -705,13 +705,13 @@ internal class InterfaceManager : IDisposable, IServiceType
using (this.dalamudAtlas.SuppressAutoRebuild())
{
this.DefaultFontHandle = (FontHandle)this.dalamudAtlas.NewDelegateFontHandle(
e => e.OnPreBuild(tk => tk.AddDalamudDefaultFont(DefaultFontSizePx)));
e => e.OnPreBuild(tk => tk.AddDalamudDefaultFont(-1)));
this.IconFontHandle = (FontHandle)this.dalamudAtlas.NewDelegateFontHandle(
e => e.OnPreBuild(
tk => tk.AddFontAwesomeIconFont(
new()
{
SizePx = DefaultFontSizePx,
SizePx = Service<FontAtlasFactory>.Get().DefaultFontSpec.SizePx,
GlyphMinAdvanceX = DefaultFontSizePx,
GlyphMaxAdvanceX = DefaultFontSizePx,
})));
@ -719,7 +719,10 @@ internal class InterfaceManager : IDisposable, IServiceType
e => e.OnPreBuild(
tk => tk.AddDalamudAssetFont(
DalamudAsset.InconsolataRegular,
new() { SizePx = DefaultFontSizePx })));
new()
{
SizePx = Service<FontAtlasFactory>.Get().DefaultFontSpec.SizePx,
})));
this.dalamudAtlas.BuildStepChange += e => e.OnPostBuild(
tk =>
{

View file

@ -18,4 +18,6 @@ internal static class ImGuiContextOffsets
public const int FontStackOffset = 0x7A4;
public const int BeginPopupStackOffset = 0x7B8;
public const int TextStateOffset = 0x4588;
}

View file

@ -6,6 +6,7 @@ using System.Runtime.InteropServices;
using Dalamud.Game;
using Dalamud.Game.Gui;
using Dalamud.Interface.Utility;
using Dalamud.Memory;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Component.GUI;
using ImGuiNET;
@ -82,7 +83,7 @@ internal unsafe class UiDebug
private void DrawUnitBase(AtkUnitBase* atkUnitBase)
{
var isVisible = (atkUnitBase->Flags & 0x20) == 0x20;
var addonName = Marshal.PtrToStringAnsi(new IntPtr(atkUnitBase->Name));
var addonName = MemoryHelper.ReadSeStringAsString(out _, new IntPtr(atkUnitBase->Name));
var agent = Service<GameGui>.Get().FindAgentInterface(atkUnitBase);
ImGui.Text($"{addonName}");
@ -204,7 +205,7 @@ internal unsafe class UiDebug
{
case NodeType.Text:
var textNode = (AtkTextNode*)node;
ImGui.Text($"text: {Marshal.PtrToStringAnsi(new IntPtr(textNode->NodeText.StringPtr))}");
ImGui.Text($"text: {MemoryHelper.ReadSeStringAsString(out _, (nint)textNode->NodeText.StringPtr)}");
ImGui.InputText($"Replace Text##{(ulong)textNode:X}", new IntPtr(textNode->NodeText.StringPtr), (uint)textNode->NodeText.BufSize);
@ -231,7 +232,7 @@ internal unsafe class UiDebug
break;
case NodeType.Counter:
var counterNode = (AtkCounterNode*)node;
ImGui.Text($"text: {Marshal.PtrToStringAnsi(new IntPtr(counterNode->NodeText.StringPtr))}");
ImGui.Text($"text: {MemoryHelper.ReadSeStringAsString(out _, (nint)counterNode->NodeText.StringPtr)}");
break;
case NodeType.Image:
var imageNode = (AtkImageNode*)node;
@ -250,8 +251,8 @@ internal unsafe class UiDebug
{
var texFileNameStdString = &textureInfo->AtkTexture.Resource->TexFileResourceHandle->ResourceHandle.FileName;
var texString = texFileNameStdString->Length < 16
? Marshal.PtrToStringAnsi((IntPtr)texFileNameStdString->Buffer)
: Marshal.PtrToStringAnsi((IntPtr)texFileNameStdString->BufferPtr);
? MemoryHelper.ReadSeStringAsString(out _, (nint)texFileNameStdString->Buffer)
: MemoryHelper.ReadSeStringAsString(out _, (nint)texFileNameStdString->BufferPtr);
ImGui.Text($"texture path: {texString}");
var kernelTexture = textureInfo->AtkTexture.Resource->KernelTextureObject;
@ -352,13 +353,13 @@ internal unsafe class UiDebug
{
case ComponentType.TextInput:
var textInputComponent = (AtkComponentTextInput*)compNode->Component;
ImGui.Text($"InputBase Text1: {Marshal.PtrToStringAnsi(new IntPtr(textInputComponent->AtkComponentInputBase.UnkText1.StringPtr))}");
ImGui.Text($"InputBase Text2: {Marshal.PtrToStringAnsi(new IntPtr(textInputComponent->AtkComponentInputBase.UnkText2.StringPtr))}");
ImGui.Text($"Text1: {Marshal.PtrToStringAnsi(new IntPtr(textInputComponent->UnkText1.StringPtr))}");
ImGui.Text($"Text2: {Marshal.PtrToStringAnsi(new IntPtr(textInputComponent->UnkText2.StringPtr))}");
ImGui.Text($"Text3: {Marshal.PtrToStringAnsi(new IntPtr(textInputComponent->UnkText3.StringPtr))}");
ImGui.Text($"Text4: {Marshal.PtrToStringAnsi(new IntPtr(textInputComponent->UnkText4.StringPtr))}");
ImGui.Text($"Text5: {Marshal.PtrToStringAnsi(new IntPtr(textInputComponent->UnkText5.StringPtr))}");
ImGui.Text($"InputBase Text1: {MemoryHelper.ReadSeStringAsString(out _, new IntPtr(textInputComponent->AtkComponentInputBase.UnkText1.StringPtr))}");
ImGui.Text($"InputBase Text2: {MemoryHelper.ReadSeStringAsString(out _, new IntPtr(textInputComponent->AtkComponentInputBase.UnkText2.StringPtr))}");
ImGui.Text($"Text1: {MemoryHelper.ReadSeStringAsString(out _, new IntPtr(textInputComponent->UnkText1.StringPtr))}");
ImGui.Text($"Text2: {MemoryHelper.ReadSeStringAsString(out _, new IntPtr(textInputComponent->UnkText2.StringPtr))}");
ImGui.Text($"Text3: {MemoryHelper.ReadSeStringAsString(out _, new IntPtr(textInputComponent->UnkText3.StringPtr))}");
ImGui.Text($"Text4: {MemoryHelper.ReadSeStringAsString(out _, new IntPtr(textInputComponent->UnkText4.StringPtr))}");
ImGui.Text($"Text5: {MemoryHelper.ReadSeStringAsString(out _, new IntPtr(textInputComponent->UnkText5.StringPtr))}");
break;
}
@ -474,7 +475,7 @@ internal unsafe class UiDebug
foundSelected = true;
}
var name = Marshal.PtrToStringAnsi(new IntPtr(unitBase->Name));
var name = MemoryHelper.ReadSeStringAsString(out _, new IntPtr(unitBase->Name));
if (searching)
{
if (name == null || !name.ToLower().Contains(searchStr.ToLower())) continue;

View file

@ -0,0 +1,77 @@
using System.Threading;
using Dalamud.Utility;
using TerraFX.Interop.Windows;
namespace Dalamud.Interface.Internal;
/// <summary>
/// A texture wrap that is created by cloning the underlying <see cref="IDalamudTextureWrap.ImGuiHandle"/>.
/// </summary>
internal sealed unsafe class UnknownTextureWrap : IDalamudTextureWrap, IDeferredDisposable
{
private IntPtr imGuiHandle;
/// <summary>
/// Initializes a new instance of the <see cref="UnknownTextureWrap"/> class.
/// </summary>
/// <param name="unknown">The pointer to <see cref="IUnknown"/> that is suitable for use with
/// <see cref="IDalamudTextureWrap.ImGuiHandle"/>.</param>
/// <param name="width">The width of the texture.</param>
/// <param name="height">The height of the texture.</param>
/// <param name="callAddRef">If <c>true</c>, call <see cref="IUnknown.AddRef"/>.</param>
public UnknownTextureWrap(IUnknown* unknown, int width, int height, bool callAddRef)
{
ObjectDisposedException.ThrowIf(unknown is null, typeof(IUnknown));
this.imGuiHandle = (nint)unknown;
this.Width = width;
this.Height = height;
if (callAddRef)
unknown->AddRef();
}
/// <summary>
/// Finalizes an instance of the <see cref="UnknownTextureWrap"/> class.
/// </summary>
~UnknownTextureWrap() => this.Dispose(false);
/// <inheritdoc/>
public nint ImGuiHandle =>
this.imGuiHandle == nint.Zero
? throw new ObjectDisposedException(nameof(UnknownTextureWrap))
: this.imGuiHandle;
/// <inheritdoc/>
public int Width { get; }
/// <inheritdoc/>
public int Height { get; }
/// <summary>
/// Queue the texture to be disposed once the frame ends.
/// </summary>
public void Dispose()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Actually dispose the wrapped texture.
/// </summary>
void IDeferredDisposable.RealDispose()
{
var handle = Interlocked.Exchange(ref this.imGuiHandle, nint.Zero);
if (handle != nint.Zero)
((IUnknown*)handle)->Release();
}
private void Dispose(bool disposing)
{
if (disposing)
Service<InterfaceManager>.GetNullable()?.EnqueueDeferredDispose(this);
else
((IDeferredDisposable)this).RealDispose();
}
}

View file

@ -6,6 +6,7 @@ using System.Numerics;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using Dalamud.Configuration.Internal;
using Dalamud.Game.Command;
@ -28,7 +29,11 @@ namespace Dalamud.Interface.Internal.Windows;
/// </summary>
internal class ConsoleWindow : Window, IDisposable
{
private readonly List<LogEntry> logText = new();
private const int LogLinesMinimum = 100;
private const int LogLinesMaximum = 1000000;
private readonly RollingList<LogEntry> logText;
private volatile int newRolledLines;
private readonly object renderLock = new();
private readonly List<string> history = new();
@ -42,12 +47,14 @@ internal class ConsoleWindow : Window, IDisposable
private string pluginFilter = string.Empty;
private bool filterShowUncaughtExceptions;
private bool settingsPopupWasOpen;
private bool showFilterToolbar;
private bool clearLog;
private bool copyLog;
private bool copyMode;
private bool killGameArmed;
private bool autoScroll;
private int logLinesLimit;
private bool autoOpen;
private bool regexError;
@ -71,13 +78,20 @@ internal class ConsoleWindow : Window, IDisposable
this.SizeConstraints = new WindowSizeConstraints
{
MinimumSize = new Vector2(600.0f, 200.0f),
MaximumSize = new Vector2(9999.0f, 9999.0f),
};
this.RespectCloseHotkey = false;
this.logLinesLimit = configuration.LogLinesLimit;
var limit = Math.Max(LogLinesMinimum, this.logLinesLimit);
this.logText = new(limit);
this.FilteredLogEntries = new(limit);
configuration.DalamudConfigurationSaved += this.OnDalamudConfigurationSaved;
}
private List<LogEntry> FilteredLogEntries { get; set; } = new();
private RollingList<LogEntry> FilteredLogEntries { get; set; }
/// <inheritdoc/>
public override void OnOpen()
@ -92,6 +106,7 @@ internal class ConsoleWindow : Window, IDisposable
public void Dispose()
{
SerilogEventSink.Instance.LogLine -= this.OnLogLine;
Service<DalamudConfiguration>.Get().DalamudConfigurationSaved -= this.OnDalamudConfigurationSaved;
}
/// <summary>
@ -152,8 +167,11 @@ internal class ConsoleWindow : Window, IDisposable
ImGui.SetCursorPosX(ImGui.GetContentRegionMax().X / 2.0f - ImGui.CalcTextSize(regexErrorString).X / 2.0f);
ImGui.TextColored(ImGuiColors.DalamudRed, regexErrorString);
}
ImGui.BeginChild("scrolling", new Vector2(0, ImGui.GetFrameHeightWithSpacing() - 55 * ImGuiHelpers.GlobalScale), false, ImGuiWindowFlags.AlwaysHorizontalScrollbar | ImGuiWindowFlags.AlwaysVerticalScrollbar);
var sendButtonSize = ImGui.CalcTextSize("Send") +
((new Vector2(16, 0) + (ImGui.GetStyle().FramePadding * 2)) * ImGuiHelpers.GlobalScale);
var scrollingHeight = ImGui.GetContentRegionAvail().Y - sendButtonSize.Y;
ImGui.BeginChild("scrolling", new Vector2(0, scrollingHeight), false, ImGuiWindowFlags.AlwaysHorizontalScrollbar | ImGuiWindowFlags.AlwaysVerticalScrollbar);
if (this.clearLog) this.Clear();
@ -173,9 +191,13 @@ internal class ConsoleWindow : Window, IDisposable
var childDrawList = ImGui.GetWindowDrawList();
var childSize = ImGui.GetWindowSize();
var cursorDiv = ImGuiHelpers.GlobalScale * 93;
var cursorLogLevel = ImGuiHelpers.GlobalScale * 100;
var cursorLogLine = ImGuiHelpers.GlobalScale * 135;
var cursorDiv = ImGui.CalcTextSize("00:00:00.000 ").X;
var cursorLogLevel = ImGui.CalcTextSize("00:00:00.000 | ").X;
var dividerOffset = ImGui.CalcTextSize("00:00:00.000 | AAA ").X + (ImGui.CalcTextSize(" ").X / 2);
var cursorLogLine = ImGui.CalcTextSize("00:00:00.000 | AAA | ").X;
var lastLinePosY = 0.0f;
var logLineHeight = 0.0f;
lock (this.renderLock)
{
@ -184,7 +206,8 @@ internal class ConsoleWindow : Window, IDisposable
{
for (var i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
{
var line = this.FilteredLogEntries[i];
var index = Math.Max(i - this.newRolledLines, 0); // Prevents flicker effect. Also workaround to avoid negative indexes.
var line = this.FilteredLogEntries[index];
if (!line.IsMultiline && !this.copyLog)
ImGui.Separator();
@ -225,6 +248,10 @@ internal class ConsoleWindow : Window, IDisposable
ImGui.SetCursorPosX(cursorLogLine);
ImGui.TextUnformatted(line.Line);
var currentLinePosY = ImGui.GetCursorPosY();
logLineHeight = currentLinePosY - lastLinePosY;
lastLinePosY = currentLinePosY;
}
}
@ -236,14 +263,19 @@ internal class ConsoleWindow : Window, IDisposable
ImGui.PopStyleVar();
var newRolledLinesCount = Interlocked.Exchange(ref this.newRolledLines, 0);
if (!this.autoScroll || ImGui.GetScrollY() < ImGui.GetScrollMaxY())
{
ImGui.SetScrollY(ImGui.GetScrollY() - (logLineHeight * newRolledLinesCount));
}
if (this.autoScroll && ImGui.GetScrollY() >= ImGui.GetScrollMaxY())
{
ImGui.SetScrollHereY(1.0f);
}
// Draw dividing line
var offset = ImGuiHelpers.GlobalScale * 127;
childDrawList.AddLine(new Vector2(childPos.X + offset, childPos.Y), new Vector2(childPos.X + offset, childPos.Y + childSize.Y), 0x4FFFFFFF, 1.0f);
childDrawList.AddLine(new Vector2(childPos.X + dividerOffset, childPos.Y), new Vector2(childPos.X + dividerOffset, childPos.Y + childSize.Y), 0x4FFFFFFF, 1.0f);
ImGui.EndChild();
@ -261,7 +293,7 @@ internal class ConsoleWindow : Window, IDisposable
}
}
ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X - (80.0f * ImGuiHelpers.GlobalScale) - (ImGui.GetStyle().ItemSpacing.X * ImGuiHelpers.GlobalScale));
ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X - sendButtonSize.X - (ImGui.GetStyle().ItemSpacing.X * ImGuiHelpers.GlobalScale));
var getFocus = false;
unsafe
@ -280,7 +312,7 @@ internal class ConsoleWindow : Window, IDisposable
if (hadColor) ImGui.PopStyleColor();
if (ImGui.Button("Send", ImGuiHelpers.ScaledVector2(80.0f, 23.0f)))
if (ImGui.Button("Send", sendButtonSize))
{
this.ProcessCommand();
}
@ -361,21 +393,21 @@ internal class ConsoleWindow : Window, IDisposable
ImGui.SameLine();
this.autoScroll = configuration.LogAutoScroll;
if (this.DrawToggleButtonWithTooltip("auto_scroll", "Auto-scroll", FontAwesomeIcon.Sync, ref this.autoScroll))
var settingsPopup = ImGui.BeginPopup("##console_settings");
if (settingsPopup)
{
configuration.LogAutoScroll = !configuration.LogAutoScroll;
configuration.QueueSave();
this.DrawSettingsPopup(configuration);
ImGui.EndPopup();
}
else if (this.settingsPopupWasOpen)
{
// Prevent side effects in case Apply wasn't clicked
this.logLinesLimit = configuration.LogLinesLimit;
}
ImGui.SameLine();
this.settingsPopupWasOpen = settingsPopup;
this.autoOpen = configuration.LogOpenAtStartup;
if (this.DrawToggleButtonWithTooltip("auto_open", "Open at startup", FontAwesomeIcon.WindowRestore, ref this.autoOpen))
{
configuration.LogOpenAtStartup = !configuration.LogOpenAtStartup;
configuration.QueueSave();
}
if (this.DrawToggleButtonWithTooltip("show_settings", "Show settings", FontAwesomeIcon.List, ref settingsPopup)) ImGui.OpenPopup("##console_settings");
ImGui.SameLine();
@ -445,6 +477,33 @@ internal class ConsoleWindow : Window, IDisposable
}
}
private void DrawSettingsPopup(DalamudConfiguration configuration)
{
if (ImGui.Checkbox("Open at startup", ref this.autoOpen))
{
configuration.LogOpenAtStartup = this.autoOpen;
configuration.QueueSave();
}
if (ImGui.Checkbox("Auto-scroll", ref this.autoScroll))
{
configuration.LogAutoScroll = this.autoScroll;
configuration.QueueSave();
}
ImGui.TextUnformatted("Logs buffer");
ImGui.SliderInt("lines", ref this.logLinesLimit, LogLinesMinimum, LogLinesMaximum);
if (ImGui.Button("Apply"))
{
this.logLinesLimit = Math.Max(LogLinesMinimum, this.logLinesLimit);
configuration.LogLinesLimit = this.logLinesLimit;
configuration.QueueSave();
ImGui.CloseCurrentPopup();
}
}
private void DrawFilterToolbar()
{
if (!this.showFilterToolbar) return;
@ -684,8 +743,12 @@ internal class ConsoleWindow : Window, IDisposable
this.logText.Add(entry);
var avoidScroll = this.FilteredLogEntries.Count == this.FilteredLogEntries.Size;
if (this.IsFilterApplicable(entry))
{
this.FilteredLogEntries.Add(entry);
if (avoidScroll) Interlocked.Increment(ref this.newRolledLines);
}
}
private bool IsFilterApplicable(LogEntry entry)
@ -729,8 +792,6 @@ internal class ConsoleWindow : Window, IDisposable
return false;
}
this.regexError = false;
// else we couldn't find a filter for this entry, if we have any filters, we need to block this entry.
return !this.pluginFilters.Any();
}
@ -739,7 +800,8 @@ internal class ConsoleWindow : Window, IDisposable
{
lock (this.renderLock)
{
this.FilteredLogEntries = this.logText.Where(this.IsFilterApplicable).ToList();
this.regexError = false;
this.FilteredLogEntries = new RollingList<LogEntry>(this.logText.Where(this.IsFilterApplicable), Math.Max(LogLinesMinimum, this.logLinesLimit));
}
}
@ -788,6 +850,14 @@ internal class ConsoleWindow : Window, IDisposable
return result;
}
private void OnDalamudConfigurationSaved(DalamudConfiguration dalamudConfiguration)
{
this.logLinesLimit = dalamudConfiguration.LogLinesLimit;
var limit = Math.Max(LogLinesMinimum, this.logLinesLimit);
this.logText.Size = limit;
this.FilteredLogEntries.Size = limit;
}
private class LogEntry
{
public string Line { get; init; } = string.Empty;

View file

@ -5,7 +5,10 @@ using System.Numerics;
using System.Text;
using System.Threading.Tasks;
using Dalamud.Game;
using Dalamud.Interface.FontIdentifier;
using Dalamud.Interface.GameFonts;
using Dalamud.Interface.ImGuiFontChooserDialog;
using Dalamud.Interface.ManagedFontAtlas;
using Dalamud.Interface.ManagedFontAtlas.Internals;
using Dalamud.Interface.Utility;
@ -22,10 +25,20 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
/// </summary>
internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable
{
private static readonly string[] FontScaleModes =
{
nameof(FontScaleMode.Default),
nameof(FontScaleMode.SkipHandling),
nameof(FontScaleMode.UndoGlobalScale),
};
private ImVectorWrapper<byte> testStringBuffer;
private IFontAtlas? privateAtlas;
private SingleFontSpec fontSpec = new() { FontId = DalamudDefaultFontAndFamilyId.Instance };
private IFontHandle? fontDialogHandle;
private IReadOnlyDictionary<GameFontFamily, (GameFontStyle Size, Lazy<IFontHandle> Handle)[]>? fontHandles;
private bool useGlobalScale;
private bool atlasScaleMode = true;
private int fontScaleMode = (int)FontScaleMode.UndoGlobalScale;
private bool useWordWrap;
private bool useItalic;
private bool useBold;
@ -47,12 +60,14 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable
public unsafe void Draw()
{
ImGui.AlignTextToFramePadding();
fixed (byte* labelPtr = "Global Scale"u8)
if (ImGui.Combo("Global Scale per Font", ref this.fontScaleMode, FontScaleModes, FontScaleModes.Length))
this.ClearAtlas();
fixed (byte* labelPtr = "Global Scale for Atlas"u8)
{
var v = (byte)(this.useGlobalScale ? 1 : 0);
var v = (byte)(this.atlasScaleMode ? 1 : 0);
if (ImGuiNative.igCheckbox(labelPtr, &v) != 0)
{
this.useGlobalScale = v != 0;
this.atlasScaleMode = v != 0;
this.ClearAtlas();
}
}
@ -111,36 +126,76 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable
if (ImGui.Button("Test Lock"))
Task.Run(this.TestLock);
fixed (byte* labelPtr = "Test Input"u8)
ImGui.SameLine();
if (ImGui.Button("Choose Editor Font"))
{
if (ImGuiNative.igInputTextMultiline(
labelPtr,
this.testStringBuffer.Data,
(uint)this.testStringBuffer.Capacity,
new(ImGui.GetContentRegionAvail().X, 32 * ImGuiHelpers.GlobalScale),
0,
null,
null) != 0)
{
var len = this.testStringBuffer.StorageSpan.IndexOf((byte)0);
if (len + 4 >= this.testStringBuffer.Capacity)
this.testStringBuffer.EnsureCapacityExponential(len + 4);
if (len < this.testStringBuffer.Capacity)
{
this.testStringBuffer.LengthUnsafe = len;
this.testStringBuffer.StorageSpan[len] = default;
}
var fcd = new SingleFontChooserDialog(
Service<FontAtlasFactory>.Get().CreateFontAtlas(
$"{nameof(GamePrebakedFontsTestWidget)}:EditorFont",
FontAtlasAutoRebuildMode.Async));
fcd.SelectedFont = this.fontSpec;
fcd.IgnorePreviewGlobalScale = !this.atlasScaleMode;
Service<InterfaceManager>.Get().Draw += fcd.Draw;
fcd.ResultTask.ContinueWith(
r => Service<Framework>.Get().RunOnFrameworkThread(
() =>
{
Service<InterfaceManager>.Get().Draw -= fcd.Draw;
fcd.Dispose();
if (this.useMinimumBuild)
_ = this.privateAtlas?.BuildFontsAsync();
}
_ = r.Exception;
if (!r.IsCompletedSuccessfully)
return;
this.fontSpec = r.Result;
Log.Information("Selected font: {font}", this.fontSpec);
this.fontDialogHandle?.Dispose();
this.fontDialogHandle = null;
}));
}
this.privateAtlas ??=
Service<FontAtlasFactory>.Get().CreateFontAtlas(
nameof(GamePrebakedFontsTestWidget),
FontAtlasAutoRebuildMode.Async,
this.useGlobalScale);
this.atlasScaleMode);
this.fontDialogHandle ??= this.fontSpec.CreateFontHandle(
this.privateAtlas,
e => e.OnPreBuild(tk => tk.SetFontScaleMode(tk.Font, (FontScaleMode)this.fontScaleMode)));
fixed (byte* labelPtr = "Test Input"u8)
{
if (!this.atlasScaleMode)
ImGuiNative.igSetWindowFontScale(1 / ImGuiHelpers.GlobalScale);
using (this.fontDialogHandle.Push())
{
if (ImGuiNative.igInputTextMultiline(
labelPtr,
this.testStringBuffer.Data,
(uint)this.testStringBuffer.Capacity,
new(ImGui.GetContentRegionAvail().X, ImGui.GetTextLineHeight() * 3),
0,
null,
null) != 0)
{
var len = this.testStringBuffer.StorageSpan.IndexOf((byte)0);
if (len + 4 >= this.testStringBuffer.Capacity)
this.testStringBuffer.EnsureCapacityExponential(len + 4);
if (len < this.testStringBuffer.Capacity)
{
this.testStringBuffer.LengthUnsafe = len;
this.testStringBuffer.StorageSpan[len] = default;
}
if (this.useMinimumBuild)
_ = this.privateAtlas?.BuildFontsAsync();
}
}
if (!this.atlasScaleMode)
ImGuiNative.igSetWindowFontScale(1);
}
this.fontHandles ??=
Enum.GetValues<GameFontFamilyAndSize>()
.Where(x => x.GetAttribute<GameFontFamilyAndSizeAttribute>() is not null)
@ -149,17 +204,29 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable
.ToImmutableDictionary(
x => x.Key,
x => x.Select(
y => (y, new Lazy<IFontHandle>(
() => this.useMinimumBuild
? this.privateAtlas.NewDelegateFontHandle(
e =>
e.OnPreBuild(
tk => tk.AddGameGlyphs(
y,
Encoding.UTF8.GetString(
this.testStringBuffer.DataSpan).ToGlyphRange(),
default)))
: this.privateAtlas.NewGameFontHandle(y))))
y =>
{
var range = Encoding.UTF8.GetString(this.testStringBuffer.DataSpan).ToGlyphRange();
Lazy<IFontHandle> l;
if (this.useMinimumBuild
|| (this.atlasScaleMode && this.fontScaleMode != (int)FontScaleMode.Default))
{
l = new(
() => this.privateAtlas!.NewDelegateFontHandle(
e =>
e.OnPreBuild(
tk => tk.SetFontScaleMode(
tk.AddGameGlyphs(y, range, default),
(FontScaleMode)this.fontScaleMode))));
}
else
{
l = new(() => this.privateAtlas!.NewGameFontHandle(y));
}
return (y, l);
})
.ToArray());
var offsetX = ImGui.CalcTextSize("99.9pt").X + (ImGui.GetStyle().FramePadding.X * 2);
@ -187,7 +254,7 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable
}
else
{
if (!this.useGlobalScale)
if (!this.atlasScaleMode)
ImGuiNative.igSetWindowFontScale(1 / ImGuiHelpers.GlobalScale);
if (counter++ % 2 == 0)
{
@ -208,8 +275,8 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable
}
finally
{
ImGuiNative.igPopTextWrapPos();
ImGuiNative.igSetWindowFontScale(1);
ImGuiNative.igPopTextWrapPos();
}
}
}
@ -227,6 +294,8 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable
this.fontHandles?.Values.SelectMany(x => x.Where(y => y.Handle.IsValueCreated).Select(y => y.Handle.Value))
.AggregateToDisposable().Dispose();
this.fontHandles = null;
this.fontDialogHandle?.Dispose();
this.fontDialogHandle = null;
this.privateAtlas?.Dispose();
this.privateAtlas = null;
}

View file

@ -119,6 +119,10 @@ internal class TexWidget : IDataWindowWidget
if (ImGui.Button($"X##{i}"))
toRemove = tex;
ImGui.SameLine();
if (ImGui.Button($"Clone##{i}"))
this.addedTextures.Add(tex.CreateWrapSharingLowLevelResource());
}
}

View file

@ -148,7 +148,6 @@ internal class PluginInstallerWindow : Window, IDisposable
this.SizeConstraints = new WindowSizeConstraints
{
MinimumSize = this.Size.Value,
MaximumSize = new Vector2(5000, 5000),
};
Service<PluginManager>.GetAsync().ContinueWith(pluginManagerTask =>

View file

@ -1,10 +1,17 @@
/*using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Dalamud.Data;
using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Game.Gui.ContextMenu;
using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Utility;
using ImGuiNET;
using Lumina.Excel;
using Lumina.Excel.GeneratedSheets;
using Serilog;*/
using Serilog;
namespace Dalamud.Interface.Internal.Windows.SelfTest.AgingSteps;
@ -13,31 +20,22 @@ namespace Dalamud.Interface.Internal.Windows.SelfTest.AgingSteps;
/// </summary>
internal class ContextMenuAgingStep : IAgingStep
{
/*
private SubStep currentSubStep;
private uint clickedItemId;
private bool clickedItemHq;
private uint clickedItemCount;
private bool? targetInventorySubmenuOpened;
private PlayerCharacter? targetCharacter;
private string? clickedPlayerName;
private ushort? clickedPlayerWorld;
private ulong? clickedPlayerCid;
private uint? clickedPlayerId;
private bool multipleTriggerOne;
private bool multipleTriggerTwo;
private ExcelSheet<Item> itemSheet;
private ExcelSheet<Materia> materiaSheet;
private ExcelSheet<Stain> stainSheet;
private enum SubStep
{
Start,
TestItem,
TestGameObject,
TestSubMenu,
TestMultiple,
TestInventoryAndSubmenu,
TestDefault,
Finish,
}
*/
/// <inheritdoc/>
public string Name => "Test Context Menu";
@ -45,23 +43,24 @@ internal class ContextMenuAgingStep : IAgingStep
/// <inheritdoc/>
public SelfTestStepResult RunStep()
{
/*
var contextMenu = Service<ContextMenu>.Get();
var dataMgr = Service<DataManager>.Get();
this.itemSheet = dataMgr.GetExcelSheet<Item>()!;
this.materiaSheet = dataMgr.GetExcelSheet<Materia>()!;
this.stainSheet = dataMgr.GetExcelSheet<Stain>()!;
ImGui.Text(this.currentSubStep.ToString());
switch (this.currentSubStep)
{
case SubStep.Start:
contextMenu.ContextMenuOpened += this.ContextMenuOnContextMenuOpened;
contextMenu.OnMenuOpened += this.OnMenuOpened;
this.currentSubStep++;
break;
case SubStep.TestItem:
if (this.clickedItemId != 0)
case SubStep.TestInventoryAndSubmenu:
if (this.targetInventorySubmenuOpened == true)
{
var item = dataMgr.GetExcelSheet<Item>()!.GetRow(this.clickedItemId);
ImGui.Text($"Did you click \"{item!.Name.RawString}\", hq:{this.clickedItemHq}, count:{this.clickedItemCount}?");
ImGui.Text($"Is the data in the submenu correct?");
if (ImGui.Button("Yes"))
this.currentSubStep++;
@ -73,7 +72,7 @@ internal class ContextMenuAgingStep : IAgingStep
}
else
{
ImGui.Text("Right-click an item.");
ImGui.Text("Right-click an item and select \"Self Test\".");
if (ImGui.Button("Skip"))
this.currentSubStep++;
@ -81,10 +80,10 @@ internal class ContextMenuAgingStep : IAgingStep
break;
case SubStep.TestGameObject:
if (!this.clickedPlayerName.IsNullOrEmpty())
case SubStep.TestDefault:
if (this.targetCharacter is { } character)
{
ImGui.Text($"Did you click \"{this.clickedPlayerName}\", world:{this.clickedPlayerWorld}, cid:{this.clickedPlayerCid}, id:{this.clickedPlayerId}?");
ImGui.Text($"Did you click \"{character.Name}\" ({character.ClassJob.GameData!.Abbreviation.ToDalamudString()})?");
if (ImGui.Button("Yes"))
this.currentSubStep++;
@ -103,149 +102,195 @@ internal class ContextMenuAgingStep : IAgingStep
}
break;
case SubStep.TestSubMenu:
if (this.multipleTriggerOne && this.multipleTriggerTwo)
{
this.currentSubStep++;
this.multipleTriggerOne = this.multipleTriggerTwo = false;
}
else
{
ImGui.Text("Right-click a character and select both options in the submenu.");
case SubStep.Finish:
return SelfTestStepResult.Pass;
if (ImGui.Button("Skip"))
this.currentSubStep++;
}
break;
case SubStep.TestMultiple:
if (this.multipleTriggerOne && this.multipleTriggerTwo)
{
this.currentSubStep = SubStep.Finish;
return SelfTestStepResult.Pass;
}
ImGui.Text("Select both options on any context menu.");
if (ImGui.Button("Skip"))
this.currentSubStep++;
break;
default:
throw new ArgumentOutOfRangeException();
}
return SelfTestStepResult.Waiting;
*/
return SelfTestStepResult.Pass;
}
/// <inheritdoc/>
public void CleanUp()
{
/*
var contextMenu = Service<ContextMenu>.Get();
contextMenu.ContextMenuOpened -= this.ContextMenuOnContextMenuOpened;
contextMenu.OnMenuOpened -= this.OnMenuOpened;
this.currentSubStep = SubStep.Start;
this.clickedItemId = 0;
this.clickedPlayerName = null;
this.multipleTriggerOne = this.multipleTriggerTwo = false;
*/
this.targetInventorySubmenuOpened = null;
this.targetCharacter = null;
}
/*
private void ContextMenuOnContextMenuOpened(ContextMenuOpenedArgs args)
private void OnMenuOpened(MenuOpenedArgs args)
{
Log.Information("Got context menu with parent addon: {ParentAddonName}, title:{Title}, itemcnt:{ItemCount}", args.ParentAddonName, args.Title, args.Items.Count);
if (args.GameObjectContext != null)
{
Log.Information(" => GameObject:{GameObjectName} world:{World} cid:{Cid} id:{Id}", args.GameObjectContext.Name, args.GameObjectContext.WorldId, args.GameObjectContext.ContentId, args.GameObjectContext.Id);
}
if (args.InventoryItemContext != null)
{
Log.Information(" => Inventory:{ItemId} hq:{Hq} count:{Count}", args.InventoryItemContext.Id, args.InventoryItemContext.IsHighQuality, args.InventoryItemContext.Count);
}
LogMenuOpened(args);
switch (this.currentSubStep)
{
case SubStep.TestSubMenu:
args.AddCustomSubMenu("Aging Submenu", openedArgs =>
case SubStep.TestInventoryAndSubmenu:
if (args.MenuType == ContextMenuType.Inventory)
{
openedArgs.AddCustomItem("Submenu Item 1", _ =>
args.AddMenuItem(new()
{
this.multipleTriggerOne = true;
});
openedArgs.AddCustomItem("Submenu Item 2", _ =>
{
this.multipleTriggerTwo = true;
});
});
return;
case SubStep.TestMultiple:
args.AddCustomItem("Aging Item 1", _ =>
{
this.multipleTriggerOne = true;
});
args.AddCustomItem("Aging Item 2", _ =>
{
this.multipleTriggerTwo = true;
});
return;
case SubStep.Finish:
return;
default:
switch (args.ParentAddonName)
{
case "Inventory":
if (this.currentSubStep != SubStep.TestItem)
return;
args.AddCustomItem("Aging Item", _ =>
Name = "Self Test",
Prefix = SeIconChar.Hyadelyn,
PrefixColor = 56,
Priority = -1,
IsSubmenu = true,
OnClicked = (MenuItemClickedArgs a) =>
{
this.clickedItemId = args.InventoryItemContext!.Id;
this.clickedItemHq = args.InventoryItemContext!.IsHighQuality;
this.clickedItemCount = args.InventoryItemContext!.Count;
Log.Warning("Clicked item: {Id} hq:{Hq} count:{Count}", this.clickedItemId, this.clickedItemHq, this.clickedItemCount);
});
break;
SeString name;
uint count;
var targetItem = (a.Target as MenuTargetInventory).TargetItem;
if (targetItem is { } item)
{
name = (this.itemSheet.GetRow(item.ItemId)?.Name.ToDalamudString() ?? $"Unknown ({item.ItemId})") + (item.IsHq ? $" {SeIconChar.HighQuality.ToIconString()}" : string.Empty);
count = item.Quantity;
}
else
{
name = "None";
count = 0;
}
case null:
case "_PartyList":
case "ChatLog":
case "ContactList":
case "ContentMemberList":
case "CrossWorldLinkshell":
case "FreeCompany":
case "FriendList":
case "LookingForGroup":
case "LinkShell":
case "PartyMemberList":
case "SocialList":
if (this.currentSubStep != SubStep.TestGameObject || args.GameObjectContext == null || args.GameObjectContext.Name.IsNullOrEmpty())
return;
a.OpenSubmenu(new MenuItem[]
{
new()
{
Name = "Name: " + name,
IsEnabled = false,
},
new()
{
Name = $"Count: {count}",
IsEnabled = false,
},
});
args.AddCustomItem("Aging Character", _ =>
{
this.clickedPlayerName = args.GameObjectContext.Name!;
this.clickedPlayerWorld = args.GameObjectContext.WorldId;
this.clickedPlayerCid = args.GameObjectContext.ContentId;
this.clickedPlayerId = args.GameObjectContext.Id;
Log.Warning("Clicked player: {Name} world:{World} cid:{Cid} id:{Id}", this.clickedPlayerName, this.clickedPlayerWorld, this.clickedPlayerCid, this.clickedPlayerId);
});
break;
this.targetInventorySubmenuOpened = true;
},
});
}
break;
case SubStep.TestDefault:
if (args.Target is MenuTargetDefault { TargetObject: PlayerCharacter { } character })
this.targetCharacter = character;
break;
case SubStep.Finish:
return;
}
}
private void LogMenuOpened(MenuOpenedArgs args)
{
Log.Verbose($"Got {args.MenuType} context menu with addon 0x{args.AddonPtr:X8} ({args.AddonName}) and agent 0x{args.AgentPtr:X8}");
if (args.Target is MenuTargetDefault targetDefault)
{
{
var b = new StringBuilder();
b.AppendLine($"Target: {targetDefault.TargetName}");
b.AppendLine($"Home World: {targetDefault.TargetHomeWorld.GameData?.Name.ToDalamudString() ?? "Unknown"} ({targetDefault.TargetHomeWorld.Id})");
b.AppendLine($"Content Id: 0x{targetDefault.TargetContentId:X8}");
b.AppendLine($"Object Id: 0x{targetDefault.TargetObjectId:X8}");
Log.Verbose(b.ToString());
}
if (targetDefault.TargetCharacter is { } character)
{
var b = new StringBuilder();
b.AppendLine($"Character: {character.Name}");
b.AppendLine($"Name: {character.Name}");
b.AppendLine($"Content Id: 0x{character.ContentId:X8}");
b.AppendLine($"FC Tag: {character.FCTag}");
b.AppendLine($"Job: {character.ClassJob.GameData?.Abbreviation.ToDalamudString() ?? "Unknown"} ({character.ClassJob.Id})");
b.AppendLine($"Statuses: {string.Join(", ", character.Statuses.Select(s => s.GameData?.Name.ToDalamudString() ?? s.Id.ToString()))}");
b.AppendLine($"Home World: {character.HomeWorld.GameData?.Name.ToDalamudString() ?? "Unknown"} ({character.HomeWorld.Id})");
b.AppendLine($"Current World: {character.CurrentWorld.GameData?.Name.ToDalamudString() ?? "Unknown"} ({character.CurrentWorld.Id})");
b.AppendLine($"Is From Other Server: {character.IsFromOtherServer}");
b.Append("Location: ");
if (character.Location.GameData is { } location)
b.Append($"{location.PlaceNameRegion.Value?.Name.ToDalamudString() ?? "Unknown"}/{location.PlaceNameZone.Value?.Name.ToDalamudString() ?? "Unknown"}/{location.PlaceName.Value?.Name.ToDalamudString() ?? "Unknown"}");
else
b.Append("Unknown");
b.AppendLine($" ({character.Location.Id})");
b.AppendLine($"Grand Company: {character.GrandCompany.GameData?.Name.ToDalamudString() ?? "Unknown"} ({character.GrandCompany.Id})");
b.AppendLine($"Client Language: {character.ClientLanguage}");
b.AppendLine($"Languages: {string.Join(", ", character.Languages)}");
b.AppendLine($"Gender: {character.Gender}");
b.AppendLine($"Display Group: {character.DisplayGroup}");
b.AppendLine($"Sort: {character.Sort}");
Log.Verbose(b.ToString());
}
else
{
Log.Verbose($"Character: null");
}
}
else if (args.Target is MenuTargetInventory targetInventory)
{
if (targetInventory.TargetItem is { } item)
{
var b = new StringBuilder();
b.AppendLine($"Item: {(item.IsEmpty ? "None" : this.itemSheet.GetRow(item.ItemId)?.Name.ToDalamudString())} ({item.ItemId})");
b.AppendLine($"Container: {item.ContainerType}");
b.AppendLine($"Slot: {item.InventorySlot}");
b.AppendLine($"Quantity: {item.Quantity}");
b.AppendLine($"{(item.IsCollectable ? "Collectability" : "Spiritbond")}: {item.Spiritbond}");
b.AppendLine($"Condition: {item.Condition / 300f:0.00}% ({item.Condition})");
b.AppendLine($"Is HQ: {item.IsHq}");
b.AppendLine($"Is Company Crest Applied: {item.IsCompanyCrestApplied}");
b.AppendLine($"Is Relic: {item.IsRelic}");
b.AppendLine($"Is Collectable: {item.IsCollectable}");
b.Append("Materia: ");
var materias = new List<string>();
foreach (var (materiaId, materiaGrade) in item.Materia.ToArray().Zip(item.MateriaGrade.ToArray()).Where(m => m.First != 0))
{
Log.Verbose($"{materiaId} {materiaGrade}");
if (this.materiaSheet.GetRow(materiaId) is { } materia &&
materia.Item[materiaGrade].Value is { } materiaItem)
materias.Add($"{materiaItem.Name.ToDalamudString()}");
else
materias.Add($"Unknown (Id: {materiaId}, Grade: {materiaGrade})");
}
if (materias.Count == 0)
b.AppendLine("None");
else
b.AppendLine(string.Join(", ", materias));
b.Append($"Dye/Stain: ");
if (item.Stain != 0)
b.AppendLine($"{this.stainSheet.GetRow(item.Stain)?.Name.ToDalamudString() ?? "Unknown"} ({item.Stain})");
else
b.AppendLine("None");
b.Append("Glamoured Item: ");
if (item.GlamourId != 0)
b.AppendLine($"{this.itemSheet.GetRow(item.GlamourId)?.Name.ToDalamudString() ?? "Unknown"} ({item.GlamourId})");
else
b.AppendLine("None");
Log.Verbose(b.ToString());
}
else
{
Log.Verbose("Item: null");
}
}
else
{
Log.Verbose($"Target: Unknown ({args.Target?.GetType().Name ?? "null"})");
}
}
*/
}

View file

@ -68,11 +68,11 @@ internal class SettingsWindow : Window
var interfaceManager = Service<InterfaceManager>.Get();
var fontAtlasFactory = Service<FontAtlasFactory>.Get();
var rebuildFont = fontAtlasFactory.UseAxis != configuration.UseAxisFontsFromGame;
var rebuildFont = !Equals(fontAtlasFactory.DefaultFontSpec, configuration.DefaultFontSpec);
rebuildFont |= !Equals(ImGui.GetIO().FontGlobalScale, configuration.GlobalUiScale);
ImGui.GetIO().FontGlobalScale = configuration.GlobalUiScale;
fontAtlasFactory.UseAxisOverride = null;
fontAtlasFactory.DefaultFontSpecOverride = null;
if (rebuildFont)
interfaceManager.RebuildFonts();

View file

@ -5,9 +5,14 @@ using System.Text;
using CheapLoc;
using Dalamud.Configuration.Internal;
using Dalamud.Game;
using Dalamud.Interface.Colors;
using Dalamud.Interface.FontIdentifier;
using Dalamud.Interface.GameFonts;
using Dalamud.Interface.ImGuiFontChooserDialog;
using Dalamud.Interface.Internal.Windows.PluginInstaller;
using Dalamud.Interface.Internal.Windows.Settings.Widgets;
using Dalamud.Interface.ManagedFontAtlas;
using Dalamud.Interface.ManagedFontAtlas.Internals;
using Dalamud.Interface.Utility;
using Dalamud.Utility;
@ -21,31 +26,19 @@ public class SettingsTabLook : SettingsTab
{
private static readonly (string, float)[] GlobalUiScalePresets =
{
("9.6pt##DalamudSettingsGlobalUiScaleReset96", 9.6f / InterfaceManager.DefaultFontSizePt),
("12pt##DalamudSettingsGlobalUiScaleReset12", 12f / InterfaceManager.DefaultFontSizePt),
("14pt##DalamudSettingsGlobalUiScaleReset14", 14f / InterfaceManager.DefaultFontSizePt),
("18pt##DalamudSettingsGlobalUiScaleReset18", 18f / InterfaceManager.DefaultFontSizePt),
("24pt##DalamudSettingsGlobalUiScaleReset24", 24f / InterfaceManager.DefaultFontSizePt),
("36pt##DalamudSettingsGlobalUiScaleReset36", 36f / InterfaceManager.DefaultFontSizePt),
("80%##DalamudSettingsGlobalUiScaleReset96", 0.8f),
("100%##DalamudSettingsGlobalUiScaleReset12", 1f),
("117%##DalamudSettingsGlobalUiScaleReset14", 14 / 12f),
("150%##DalamudSettingsGlobalUiScaleReset18", 1.5f),
("200%##DalamudSettingsGlobalUiScaleReset24", 2f),
("300%##DalamudSettingsGlobalUiScaleReset36", 3f),
};
private float globalUiScale;
private IFontSpec defaultFontSpec = null!;
public override SettingsEntry[] Entries { get; } =
{
new GapSettingsEntry(5),
new SettingsEntry<bool>(
Loc.Localize("DalamudSettingToggleAxisFonts", "Use AXIS fonts as default Dalamud font"),
Loc.Localize("DalamudSettingToggleUiAxisFontsHint", "Use AXIS fonts (the game's main UI fonts) as default Dalamud font."),
c => c.UseAxisFontsFromGame,
(v, c) => c.UseAxisFontsFromGame = v,
v =>
{
Service<FontAtlasFactory>.Get().UseAxisOverride = v;
Service<InterfaceManager>.Get().RebuildFonts();
}),
new GapSettingsEntry(5, true),
new ButtonSettingsEntry(
@ -178,10 +171,10 @@ public class SettingsTabLook : SettingsTab
}
}
var globalUiScaleInPt = 12f * this.globalUiScale;
if (ImGui.DragFloat("##DalamudSettingsGlobalUiScaleDrag", ref globalUiScaleInPt, 0.1f, 9.6f, 36f, "%.1fpt", ImGuiSliderFlags.AlwaysClamp))
var globalUiScaleInPct = 100f * this.globalUiScale;
if (ImGui.DragFloat("##DalamudSettingsGlobalUiScaleDrag", ref globalUiScaleInPct, 1f, 80f, 300f, "%.0f%%", ImGuiSliderFlags.AlwaysClamp))
{
this.globalUiScale = globalUiScaleInPt / 12f;
this.globalUiScale = globalUiScaleInPct / 100f;
ImGui.GetIO().FontGlobalScale = this.globalUiScale;
interfaceManager.RebuildFonts();
}
@ -201,12 +194,53 @@ public class SettingsTabLook : SettingsTab
}
}
ImGuiHelpers.ScaledDummy(5);
if (ImGui.Button(Loc.Localize("DalamudSettingChooseDefaultFont", "Choose Default Font")))
{
var faf = Service<FontAtlasFactory>.Get();
var fcd = new SingleFontChooserDialog(
faf.CreateFontAtlas($"{nameof(SettingsTabLook)}:Default", FontAtlasAutoRebuildMode.Async));
fcd.SelectedFont = (SingleFontSpec)this.defaultFontSpec;
fcd.FontFamilyExcludeFilter = x => x is DalamudDefaultFontAndFamilyId;
interfaceManager.Draw += fcd.Draw;
fcd.ResultTask.ContinueWith(
r => Service<Framework>.Get().RunOnFrameworkThread(
() =>
{
interfaceManager.Draw -= fcd.Draw;
fcd.Dispose();
_ = r.Exception;
if (!r.IsCompletedSuccessfully)
return;
faf.DefaultFontSpecOverride = this.defaultFontSpec = r.Result;
interfaceManager.RebuildFonts();
}));
}
ImGui.SameLine();
using (interfaceManager.MonoFontHandle?.Push())
{
if (ImGui.Button(Loc.Localize("DalamudSettingResetDefaultFont", "Reset Default Font")))
{
var faf = Service<FontAtlasFactory>.Get();
faf.DefaultFontSpecOverride =
this.defaultFontSpec =
new SingleFontSpec { FontId = new GameFontAndFamilyId(GameFontFamily.Axis) };
interfaceManager.RebuildFonts();
}
}
base.Draw();
}
public override void Load()
{
this.globalUiScale = Service<DalamudConfiguration>.Get().GlobalUiScale;
this.defaultFontSpec = Service<FontAtlasFactory>.Get().DefaultFontSpec;
base.Load();
}
@ -214,6 +248,7 @@ public class SettingsTabLook : SettingsTab
public override void Save()
{
Service<DalamudConfiguration>.Get().GlobalUiScale = this.globalUiScale;
Service<DalamudConfiguration>.Get().DefaultFontSpec = this.defaultFontSpec;
base.Save();
}

View file

@ -43,7 +43,6 @@ public class StyleEditorWindow : Window
this.SizeConstraints = new WindowSizeConstraints
{
MinimumSize = new Vector2(890, 560),
MaximumSize = new Vector2(10000, 10000),
};
}

View file

@ -0,0 +1,33 @@
using Dalamud.Interface.Utility;
using ImGuiNET;
namespace Dalamud.Interface.ManagedFontAtlas;
/// <summary>
/// Specifies how should global font scale affect a font.
/// </summary>
public enum FontScaleMode
{
/// <summary>
/// Do the default handling. Dalamud will load the sufficienty large font that will accomodate the global scale,
/// and stretch the loaded glyphs so that they look pixel-perfect after applying global scale on drawing.
/// Note that bitmap fonts and game fonts will always look blurry if they're not in their original sizes.
/// </summary>
Default,
/// <summary>
/// Do nothing with the font. Dalamud will load the font with the size that is exactly as specified.
/// On drawing, the font will look blurry due to stretching.
/// Intended for use with custom scale handling.
/// </summary>
SkipHandling,
/// <summary>
/// Stretch the glyphs of the loaded font by the inverse of the global scale.
/// On drawing, the font will always render exactly as the requested size without blurring, as long as
/// <see cref="ImGuiHelpers.GlobalScale"/> and <see cref="ImGui.SetWindowFontScale"/> do not affect the scale any
/// further. Note that bitmap fonts and game fonts will always look blurry if they're not in their original sizes.
/// </summary>
UndoGlobalScale,
}

View file

@ -8,7 +8,8 @@ using ImGuiNET;
namespace Dalamud.Interface.ManagedFontAtlas;
/// <summary>
/// Wrapper for <see cref="ImFontAtlasPtr"/>.
/// Wrapper for <see cref="ImFontAtlasPtr"/>.<br />
/// Not intended for plugins to implement.
/// </summary>
public interface IFontAtlas : IDisposable
{
@ -93,11 +94,15 @@ public interface IFontAtlas : IDisposable
/// </summary>
/// <param name="buildStepDelegate">Callback for <see cref="IFontAtlas.BuildStepChange"/>.</param>
/// <returns>Handle to a font that may or may not be ready yet.</returns>
/// <remarks>
/// Consider calling <see cref="IFontAtlasBuildToolkitPreBuild.AttachExtraGlyphsForDalamudLanguage"/> to support
/// glyphs that are not supplied by the game by default; this mostly affects Chinese and Korean language users.
/// </remarks>
/// <example>
/// <b>On initialization</b>:
/// <code>
/// this.fontHandle = atlas.NewDelegateFontHandle(e => e.OnPreBuild(tk => {
/// var config = new SafeFontConfig { SizePx = 16 };
/// var config = new SafeFontConfig { SizePx = UiBuilder.DefaultFontSizePx };
/// config.MergeFont = tk.AddFontFromFile(@"C:\Windows\Fonts\comic.ttf", config);
/// tk.AddGameSymbol(config);
/// tk.AddExtraGlyphsForDalamudLanguage(config);

View file

@ -9,7 +9,8 @@ using ImGuiNET;
namespace Dalamud.Interface.ManagedFontAtlas;
/// <summary>
/// Common stuff for <see cref="IFontAtlasBuildToolkitPreBuild"/> and <see cref="IFontAtlasBuildToolkitPostBuild"/>.
/// Common stuff for <see cref="IFontAtlasBuildToolkitPreBuild"/> and <see cref="IFontAtlasBuildToolkitPostBuild"/>.<br />
/// Not intended for plugins to implement.
/// </summary>
public interface IFontAtlasBuildToolkit
{

View file

@ -1,20 +1,23 @@
using Dalamud.Interface.Internal;
using Dalamud.Utility;
using ImGuiNET;
namespace Dalamud.Interface.ManagedFontAtlas;
/// <summary>
/// Toolkit for use when the build state is <see cref="FontAtlasBuildStep.PostBuild"/>.
/// Toolkit for use when the build state is <see cref="FontAtlasBuildStep.PostBuild"/>.<br />
/// Not intended for plugins to implement.
/// </summary>
public interface IFontAtlasBuildToolkitPostBuild : IFontAtlasBuildToolkit
{
/// <summary>
/// Gets whether global scaling is ignored for the given font.
/// </summary>
/// <param name="fontPtr">The font.</param>
/// <returns>True if ignored.</returns>
bool IsGlobalScaleIgnored(ImFontPtr fontPtr);
/// <inheritdoc cref="IFontAtlasBuildToolkitPreBuild.IsGlobalScaleIgnored"/>
[Obsolete($"Use {nameof(this.GetFontScaleMode)}")]
[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)]
bool IsGlobalScaleIgnored(ImFontPtr fontPtr) => this.GetFontScaleMode(fontPtr) == FontScaleMode.UndoGlobalScale;
/// <inheritdoc cref="IFontAtlasBuildToolkitPreBuild.GetFontScaleMode"/>
FontScaleMode GetFontScaleMode(ImFontPtr fontPtr);
/// <summary>
/// Stores a texture to be managed with the atlas.

View file

@ -1,8 +1,10 @@
using System.IO;
using System.Runtime.InteropServices;
using Dalamud.Interface.FontIdentifier;
using Dalamud.Interface.GameFonts;
using Dalamud.Interface.Utility;
using Dalamud.Utility;
using ImGuiNET;
@ -10,6 +12,7 @@ namespace Dalamud.Interface.ManagedFontAtlas;
/// <summary>
/// Toolkit for use when the build state is <see cref="FontAtlasBuildStep.PreBuild"/>.<br />
/// Not intended for plugins to implement.<br />
/// <br />
/// After <see cref="FontAtlasBuildStepDelegate"/> returns,
/// either <see cref="IFontAtlasBuildToolkit.Font"/> must be set,
@ -43,14 +46,43 @@ public interface IFontAtlasBuildToolkitPreBuild : IFontAtlasBuildToolkit
/// </summary>
/// <param name="fontPtr">The font.</param>
/// <returns>Same <see cref="ImFontPtr"/> with <paramref name="fontPtr"/>.</returns>
ImFontPtr IgnoreGlobalScale(ImFontPtr fontPtr);
[Obsolete(
$"Use {nameof(this.SetFontScaleMode)} with {nameof(FontScaleMode)}.{nameof(FontScaleMode.UndoGlobalScale)}")]
[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)]
ImFontPtr IgnoreGlobalScale(ImFontPtr fontPtr) => this.SetFontScaleMode(fontPtr, FontScaleMode.UndoGlobalScale);
/// <summary>
/// Gets whether global scaling is ignored for the given font.
/// </summary>
/// <param name="fontPtr">The font.</param>
/// <returns>True if ignored.</returns>
bool IsGlobalScaleIgnored(ImFontPtr fontPtr);
[Obsolete($"Use {nameof(this.GetFontScaleMode)}")]
[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)]
bool IsGlobalScaleIgnored(ImFontPtr fontPtr) => this.GetFontScaleMode(fontPtr) == FontScaleMode.UndoGlobalScale;
/// <summary>
/// Sets the scaling mode for the given font.
/// </summary>
/// <param name="fontPtr">The font, returned from <see cref="AddFontFromFile"/> and alike.
/// Note that <see cref="IFontAtlasBuildToolkit.Font"/> property is not guaranteed to be automatically updated upon
/// calling font adding functions. Pass the return value from font adding functions, not
/// <see cref="IFontAtlasBuildToolkit.Font"/> property.</param>
/// <param name="mode">The scaling mode.</param>
/// <returns><paramref name="fontPtr"/>.</returns>
ImFontPtr SetFontScaleMode(ImFontPtr fontPtr, FontScaleMode mode);
/// <summary>
/// Gets the scaling mode for the given font.
/// </summary>
/// <param name="fontPtr">The font.</param>
/// <returns>The scaling mode.</returns>
FontScaleMode GetFontScaleMode(ImFontPtr fontPtr);
/// <summary>
/// Registers a function to be run after build.
/// </summary>
/// <param name="action">The action to run.</param>
void RegisterPostBuild(Action action);
/// <summary>
/// Adds a font from memory region allocated using <see cref="ImGuiHelpers.AllocateMemory"/>.<br />
@ -134,7 +166,12 @@ public interface IFontAtlasBuildToolkitPreBuild : IFontAtlasBuildToolkit
/// As this involves adding multiple fonts, calling this function will set <see cref="IFontAtlasBuildToolkit.Font"/>
/// as the return value of this function, if it was empty before.
/// </summary>
/// <param name="sizePx">Font size in pixels.</param>
/// <param name="sizePx">
/// Font size in pixels.
/// If a negative value is supplied,
/// (<see cref="UiBuilder.DefaultFontSpec"/>.<see cref="IFontSpec.SizePx"/> * <paramref name="sizePx"/>) will be
/// used as the font size. Specify -1 to use the default font size.
/// </param>
/// <param name="glyphRanges">The glyph ranges. Use <see cref="FontAtlasBuildToolkitUtilities"/>.ToGlyphRange to build.</param>
/// <returns>A font returned from <see cref="ImFontAtlasPtr.AddFont"/>.</returns>
ImFontPtr AddDalamudDefaultFont(float sizePx, ushort[]? glyphRanges = null);

View file

@ -5,7 +5,8 @@ using ImGuiNET;
namespace Dalamud.Interface.ManagedFontAtlas;
/// <summary>
/// Represents a reference counting handle for fonts.
/// Represents a reference counting handle for fonts.<br />
/// Not intended for plugins to implement.
/// </summary>
public interface IFontHandle : IDisposable
{

View file

@ -4,7 +4,8 @@ namespace Dalamud.Interface.ManagedFontAtlas;
/// <summary>
/// The wrapper for <see cref="ImFontPtr"/>, guaranteeing that the associated data will be available as long as
/// this struct is not disposed.
/// this struct is not disposed.<br />
/// Not intended for plugins to implement.
/// </summary>
public interface ILockedImFont : IDisposable
{

View file

@ -6,6 +6,7 @@ using System.Runtime.InteropServices;
using System.Text.Unicode;
using Dalamud.Configuration.Internal;
using Dalamud.Interface.FontIdentifier;
using Dalamud.Interface.GameFonts;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Utility;
@ -42,6 +43,7 @@ internal sealed partial class FontAtlasFactory
private readonly GamePrebakedFontHandle.HandleSubstance gameFontHandleSubstance;
private readonly FontAtlasFactory factory;
private readonly FontAtlasBuiltData data;
private readonly List<Action> registeredPostBuildActions = new();
/// <summary>
/// Initializes a new instance of the <see cref="BuildToolkit"/> class.
@ -81,9 +83,9 @@ internal sealed partial class FontAtlasFactory
public ImVectorWrapper<ImFontPtr> Fonts => this.data.Fonts;
/// <summary>
/// Gets the list of fonts to ignore global scale.
/// Gets the font scale modes.
/// </summary>
public List<ImFontPtr> GlobalScaleExclusions { get; } = new();
private Dictionary<ImFontPtr, FontScaleMode> FontScaleModes { get; } = new();
/// <inheritdoc/>
public void Dispose() => this.disposeAfterBuild.Dispose();
@ -149,19 +151,22 @@ internal sealed partial class FontAtlasFactory
}
/// <inheritdoc/>
public ImFontPtr IgnoreGlobalScale(ImFontPtr fontPtr)
public ImFontPtr SetFontScaleMode(ImFontPtr fontPtr, FontScaleMode scaleMode)
{
this.GlobalScaleExclusions.Add(fontPtr);
this.FontScaleModes[fontPtr] = scaleMode;
return fontPtr;
}
/// <inheritdoc cref="IFontAtlasBuildToolkitPreBuild.IsGlobalScaleIgnored"/>
public bool IsGlobalScaleIgnored(ImFontPtr fontPtr) =>
this.GlobalScaleExclusions.Contains(fontPtr);
/// <inheritdoc cref="IFontAtlasBuildToolkitPreBuild.GetFontScaleMode"/>
public FontScaleMode GetFontScaleMode(ImFontPtr fontPtr) =>
this.FontScaleModes.GetValueOrDefault(fontPtr, FontScaleMode.Default);
/// <inheritdoc/>
public int StoreTexture(IDalamudTextureWrap textureWrap, bool disposeOnError) =>
this.data.AddNewTexture(textureWrap, disposeOnError);
/// <inheritdoc/>
public void RegisterPostBuild(Action action) => this.registeredPostBuildActions.Add(action);
/// <inheritdoc/>
public unsafe ImFontPtr AddFontFromImGuiHeapAllocatedMemory(
@ -180,6 +185,7 @@ internal sealed partial class FontAtlasFactory
dataSize,
debugTag);
var font = default(ImFontPtr);
try
{
fontConfig.ThrowOnInvalidValues();
@ -187,6 +193,7 @@ internal sealed partial class FontAtlasFactory
var raw = fontConfig.Raw with
{
FontData = dataPointer,
FontDataOwnedByAtlas = 1,
FontDataSize = dataSize,
};
@ -198,7 +205,7 @@ internal sealed partial class FontAtlasFactory
TrueTypeUtils.CheckImGuiCompatibleOrThrow(raw);
var font = this.NewImAtlas.AddFont(&raw);
font = this.NewImAtlas.AddFont(&raw);
var dataHash = default(HashCode);
dataHash.AddBytes(new(dataPointer, dataSize));
@ -235,8 +242,23 @@ internal sealed partial class FontAtlasFactory
}
catch
{
if (!font.IsNull())
{
// Note that for both RemoveAt calls, corresponding destructors will be called.
var configIndex = this.data.ConfigData.FindIndex(x => x.DstFont == font.NativePtr);
if (configIndex >= 0)
this.data.ConfigData.RemoveAt(configIndex);
var index = this.Fonts.IndexOf(font);
if (index >= 0)
this.Fonts.RemoveAt(index);
}
// ImFontConfig has no destructor, and does not free the data.
if (freeOnException)
ImGuiNative.igMemFree(dataPointer);
throw;
}
}
@ -314,18 +336,32 @@ internal sealed partial class FontAtlasFactory
/// <inheritdoc/>
public ImFontPtr AddDalamudDefaultFont(float sizePx, ushort[]? glyphRanges)
{
ImFontPtr font;
ImFontPtr font = default;
glyphRanges ??= this.factory.DefaultGlyphRanges;
if (this.factory.UseAxis)
var dfid = this.factory.DefaultFontSpec;
if (sizePx < 0f)
sizePx *= -dfid.SizePx;
if (dfid is SingleFontSpec sfs)
{
font = this.AddGameGlyphs(new(GameFontFamily.Axis, sizePx), glyphRanges, default);
if (sfs.FontId is DalamudDefaultFontAndFamilyId)
{
// invalid; calling sfs.AddToBuildToolkit calls this function, causing infinite recursion
}
else
{
sfs = sfs with { SizePx = sizePx };
font = sfs.AddToBuildToolkit(this);
if (sfs.FontId is not GameFontAndFamilyId { GameFontFamily: GameFontFamily.Axis })
this.AddGameSymbol(new() { SizePx = sizePx, MergeFont = font });
}
}
else
if (font.IsNull())
{
font = this.AddDalamudAssetFont(
DalamudAsset.NotoSansJpMedium,
new() { SizePx = sizePx, GlyphRanges = glyphRanges });
this.AddGameSymbol(new() { SizePx = sizePx, MergeFont = font });
// fall back to AXIS fonts
font = this.AddGameGlyphs(new(GameFontFamily.Axis, sizePx), glyphRanges, default);
}
this.AttachExtraGlyphsForDalamudLanguage(new() { SizePx = sizePx, MergeFont = font });
@ -460,17 +496,17 @@ internal sealed partial class FontAtlasFactory
var configData = this.data.ConfigData;
foreach (ref var config in configData.DataSpan)
{
if (this.GlobalScaleExclusions.Contains(new(config.DstFont)))
if (this.GetFontScaleMode(config.DstFont) != FontScaleMode.Default)
continue;
config.SizePixels *= this.Scale;
config.GlyphMaxAdvanceX *= this.Scale;
if (float.IsInfinity(config.GlyphMaxAdvanceX))
if (float.IsInfinity(config.GlyphMaxAdvanceX) || float.IsNaN(config.GlyphMaxAdvanceX))
config.GlyphMaxAdvanceX = config.GlyphMaxAdvanceX > 0 ? float.MaxValue : -float.MaxValue;
config.GlyphMinAdvanceX *= this.Scale;
if (float.IsInfinity(config.GlyphMinAdvanceX))
if (float.IsInfinity(config.GlyphMinAdvanceX) || float.IsNaN(config.GlyphMinAdvanceX))
config.GlyphMinAdvanceX = config.GlyphMinAdvanceX > 0 ? float.MaxValue : -float.MaxValue;
config.GlyphOffset *= this.Scale;
@ -500,7 +536,7 @@ internal sealed partial class FontAtlasFactory
var scale = this.Scale;
foreach (ref var font in this.Fonts.DataSpan)
{
if (!this.GlobalScaleExclusions.Contains(font))
if (this.GetFontScaleMode(font) != FontScaleMode.SkipHandling)
font.AdjustGlyphMetrics(1 / scale, 1 / scale);
foreach (var c in FallbackCodepoints)
@ -531,6 +567,13 @@ internal sealed partial class FontAtlasFactory
substance.OnPostBuild(this);
}
public void PostBuildCallbacks()
{
foreach (var ac in this.registeredPostBuildActions)
ac.InvokeSafely();
this.registeredPostBuildActions.Clear();
}
public unsafe void UploadTextures()
{
var buf = Array.Empty<byte>();

View file

@ -46,6 +46,9 @@ internal sealed partial class FontAtlasFactory
private class FontAtlasBuiltData : IRefCountable
{
// Field for debugging.
private static int numActiveInstances;
private readonly List<IDalamudTextureWrap> wraps;
private readonly List<IFontHandleSubstance> substances;
@ -73,6 +76,9 @@ internal sealed partial class FontAtlasFactory
this.Garbage.Add(() => ImGuiNative.ImFontAtlas_destroy(atlasPtr));
this.IsBuildInProgress = true;
Interlocked.Increment(ref numActiveInstances);
this.Garbage.Add(() => Interlocked.Decrement(ref numActiveInstances));
}
catch
{
@ -658,7 +664,7 @@ internal sealed partial class FontAtlasFactory
toolkit = res.CreateToolkit(this.factory, isAsync);
// PreBuildSubstances deals with toolkit.Add... function family. Do this first.
var defaultFont = toolkit.AddDalamudDefaultFont(InterfaceManager.DefaultFontSizePx, null);
var defaultFont = toolkit.AddDalamudDefaultFont(-1, null);
this.BuildStepChange?.Invoke(toolkit);
toolkit.PreBuildSubstances();
@ -679,6 +685,7 @@ internal sealed partial class FontAtlasFactory
toolkit.PostBuild();
toolkit.PostBuildSubstances();
toolkit.PostBuildCallbacks();
this.BuildStepChange?.Invoke(toolkit);
foreach (var font in toolkit.Fonts)

View file

@ -8,6 +8,7 @@ using System.Threading.Tasks;
using Dalamud.Configuration.Internal;
using Dalamud.Data;
using Dalamud.Game;
using Dalamud.Interface.FontIdentifier;
using Dalamud.Interface.GameFonts;
using Dalamud.Interface.Internal;
using Dalamud.Storage.Assets;
@ -108,14 +109,29 @@ internal sealed partial class FontAtlasFactory
}
/// <summary>
/// Gets or sets a value indicating whether to override configuration for UseAxis.
/// Gets or sets a value indicating whether to override configuration for <see cref="DefaultFontSpec"/>.
/// </summary>
public bool? UseAxisOverride { get; set; } = null;
public IFontSpec? DefaultFontSpecOverride { get; set; } = null;
/// <summary>
/// Gets a value indicating whether to use AXIS fonts.
/// Gets the default font ID.
/// </summary>
public bool UseAxis => this.UseAxisOverride ?? Service<DalamudConfiguration>.Get().UseAxisFontsFromGame;
public IFontSpec DefaultFontSpec =>
this.DefaultFontSpecOverride
?? Service<DalamudConfiguration>.Get().DefaultFontSpec
#pragma warning disable CS0618 // Type or member is obsolete
?? (Service<DalamudConfiguration>.Get().UseAxisFontsFromGame
#pragma warning restore CS0618 // Type or member is obsolete
? new()
{
FontId = new GameFontAndFamilyId(GameFontFamily.Axis),
SizePx = InterfaceManager.DefaultFontSizePx,
}
: new SingleFontSpec
{
FontId = new DalamudAssetFontAndFamilyId(DalamudAsset.NotoSansJpMedium),
SizePx = InterfaceManager.DefaultFontSizePx + 1,
});
/// <summary>
/// Gets the service instance of <see cref="Framework"/>.
@ -229,6 +245,25 @@ internal sealed partial class FontAtlasFactory
private static T ExtractResult<T>(Task<T> t) => t.IsCompleted ? t.Result : t.GetAwaiter().GetResult();
/// <summary>
/// Clones a texture wrap, by getting a new reference to the underlying <see cref="ShaderResourceView"/> and the
/// texture behind.
/// </summary>
/// <param name="wrap">The <see cref="IDalamudTextureWrap"/> to clone from.</param>
/// <returns>The cloned <see cref="IDalamudTextureWrap"/>.</returns>
private static IDalamudTextureWrap CloneTextureWrap(IDalamudTextureWrap wrap)
{
var srv = CppObject.FromPointer<ShaderResourceView>(wrap.ImGuiHandle);
using var res = srv.Resource;
using var tex2D = res.QueryInterface<Texture2D>();
var description = tex2D.Description;
return new DalamudTextureWrap(
new D3DTextureWrap(
srv.QueryInterface<ShaderResourceView>(),
description.Width,
description.Height));
}
private static unsafe void ExtractChannelFromB8G8R8A8(
Span<byte> target,
ReadOnlySpan<byte> source,
@ -266,25 +301,6 @@ internal sealed partial class FontAtlasFactory
}
}
/// <summary>
/// Clones a texture wrap, by getting a new reference to the underlying <see cref="ShaderResourceView"/> and the
/// texture behind.
/// </summary>
/// <param name="wrap">The <see cref="IDalamudTextureWrap"/> to clone from.</param>
/// <returns>The cloned <see cref="IDalamudTextureWrap"/>.</returns>
private static IDalamudTextureWrap CloneTextureWrap(IDalamudTextureWrap wrap)
{
var srv = CppObject.FromPointer<ShaderResourceView>(wrap.ImGuiHandle);
using var res = srv.Resource;
using var tex2D = res.QueryInterface<Texture2D>();
var description = tex2D.Description;
return new DalamudTextureWrap(
new D3DTextureWrap(
srv.QueryInterface<ShaderResourceView>(),
description.Width,
description.Height));
}
private static unsafe void ExtractChannelFromB4G4R4A4(
Span<byte> target,
ReadOnlySpan<byte> source,
@ -317,7 +333,7 @@ internal sealed partial class FontAtlasFactory
v |= v << 4;
*wptr = (uint)((v << 24) | 0x00FFFFFF);
wptr++;
rptr += 4;
rptr += 2;
}
}
}

View file

@ -345,17 +345,36 @@ internal class GamePrebakedFontHandle : FontHandle
{
foreach (var (font, style, ranges) in this.attachments)
{
var effectiveStyle =
toolkitPreBuild.IsGlobalScaleIgnored(font)
? style.Scale(1 / toolkitPreBuild.Scale)
: style;
if (!this.fonts.TryGetValue(style, out var plan))
{
plan = new(
effectiveStyle,
toolkitPreBuild.Scale,
this.handleManager.GameFontTextureProvider,
this.CreateTemplateFont(toolkitPreBuild, style.SizePx));
switch (toolkitPreBuild.GetFontScaleMode(font))
{
case FontScaleMode.Default:
default:
plan = new(
style,
toolkitPreBuild.Scale,
this.handleManager.GameFontTextureProvider,
this.CreateTemplateFont(toolkitPreBuild, style.SizePx));
break;
case FontScaleMode.SkipHandling:
plan = new(
style,
1f,
this.handleManager.GameFontTextureProvider,
this.CreateTemplateFont(toolkitPreBuild, style.SizePx));
break;
case FontScaleMode.UndoGlobalScale:
plan = new(
style.Scale(1 / toolkitPreBuild.Scale),
toolkitPreBuild.Scale,
this.handleManager.GameFontTextureProvider,
this.CreateTemplateFont(toolkitPreBuild, style.SizePx));
break;
}
this.fonts[style] = plan;
}
@ -620,15 +639,14 @@ internal class GamePrebakedFontHandle : FontHandle
public unsafe void CopyGlyphsToRanges(IFontAtlasBuildToolkitPostBuild toolkitPostBuild)
{
var scale = this.Style.SizePt / this.Fdt.FontHeader.Size;
var atlasScale = toolkitPostBuild.Scale;
var round = 1 / atlasScale;
foreach (var (font, rangeBits) in this.Ranges)
{
if (font.NativePtr == this.FullRangeFont.NativePtr)
continue;
var noGlobalScale = toolkitPostBuild.IsGlobalScaleIgnored(font);
var fontScaleMode = toolkitPostBuild.GetFontScaleMode(font);
var round = fontScaleMode == FontScaleMode.SkipHandling ? 1 : 1 / toolkitPostBuild.Scale;
var lookup = font.IndexLookupWrapped();
var glyphs = font.GlyphsWrapped();
@ -649,7 +667,7 @@ internal class GamePrebakedFontHandle : FontHandle
ref var g = ref glyphs[glyphIndex];
g = sourceGlyph;
if (noGlobalScale)
if (fontScaleMode == FontScaleMode.SkipHandling)
{
g.XY *= scale;
g.AdvanceX *= scale;
@ -673,7 +691,7 @@ internal class GamePrebakedFontHandle : FontHandle
continue;
if (!rangeBits[leftInt] || !rangeBits[rightInt])
continue;
if (noGlobalScale)
if (fontScaleMode == FontScaleMode.SkipHandling)
{
font.AddKerningPair((ushort)leftInt, (ushort)rightInt, k.RightOffset * scale);
}

View file

@ -26,7 +26,7 @@ public struct SafeFontConfig
this.PixelSnapH = true;
this.GlyphMaxAdvanceX = float.MaxValue;
this.RasterizerMultiply = 1f;
this.RasterizerGamma = 1.4f;
this.RasterizerGamma = 1.7f;
this.EllipsisChar = unchecked((char)-1);
this.Raw.FontDataOwnedByAtlas = 1;
}

View file

@ -7,6 +7,7 @@ using Dalamud.Game;
using Dalamud.Game.ClientState;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.Gui;
using Dalamud.Interface.FontIdentifier;
using Dalamud.Interface.GameFonts;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Internal.ManagedAsserts;
@ -173,12 +174,12 @@ public sealed class UiBuilder : IDisposable
/// <summary>
/// Gets the default Dalamud font size in points.
/// </summary>
public static float DefaultFontSizePt => InterfaceManager.DefaultFontSizePt;
public static float DefaultFontSizePt => Service<FontAtlasFactory>.Get().DefaultFontSpec.SizePt;
/// <summary>
/// Gets the default Dalamud font size in pixels.
/// </summary>
public static float DefaultFontSizePx => InterfaceManager.DefaultFontSizePx;
public static float DefaultFontSizePx => Service<FontAtlasFactory>.Get().DefaultFontSpec.SizePx;
/// <summary>
/// Gets the default Dalamud font - supporting all game languages and icons.<br />
@ -198,6 +199,11 @@ public sealed class UiBuilder : IDisposable
/// </summary>
public static ImFontPtr MonoFont => InterfaceManager.MonoFont;
/// <summary>
/// Gets the default font specifications.
/// </summary>
public IFontSpec DefaultFontSpec => Service<FontAtlasFactory>.Get().DefaultFontSpec;
/// <summary>
/// Gets the handle to the default Dalamud font - supporting all game languages and icons.
/// </summary>
@ -206,7 +212,7 @@ public sealed class UiBuilder : IDisposable
/// <code>
/// fontAtlas.NewDelegateFontHandle(
/// e => e.OnPreBuild(
/// tk => tk.AddDalamudDefaultFont(UiBuilder.DefaultFontSizePt)));
/// tk => tk.AddDalamudDefaultFont(UiBuilder.DefaultFontSizePx)));
/// </code>
/// </remarks>
public IFontHandle DefaultFontHandle =>
@ -225,6 +231,8 @@ public sealed class UiBuilder : IDisposable
/// fontAtlas.NewDelegateFontHandle(
/// e => e.OnPreBuild(
/// tk => tk.AddFontAwesomeIconFont(new() { SizePt = UiBuilder.DefaultFontSizePt })));
/// // or use
/// tk => tk.AddFontAwesomeIconFont(new() { SizePx = UiBuilder.DefaultFontSizePx })));
/// </code>
/// </remarks>
public IFontHandle IconFontHandle =>
@ -245,6 +253,8 @@ public sealed class UiBuilder : IDisposable
/// tk => tk.AddDalamudAssetFont(
/// DalamudAsset.InconsolataRegular,
/// new() { SizePt = UiBuilder.DefaultFontSizePt })));
/// // or use
/// new() { SizePx = UiBuilder.DefaultFontSizePx })));
/// </code>
/// </remarks>
public IFontHandle MonoFontHandle =>

View file

@ -4,6 +4,7 @@ using System.Linq;
using System.Numerics;
using System.Reactive.Disposables;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Unicode;
using Dalamud.Configuration.Internal;
@ -543,6 +544,24 @@ public static class ImGuiHelpers
var pageIndex = unchecked((ushort)(codepoint / 4096));
font.NativePtr->Used4kPagesMap[pageIndex >> 3] |= unchecked((byte)(1 << (pageIndex & 7)));
}
/// <summary>
/// Sets the text for a text input, during the callback.
/// </summary>
/// <param name="data">The callback data.</param>
/// <param name="s">The new text.</param>
internal static unsafe void SetTextFromCallback(ImGuiInputTextCallbackData* data, string s)
{
if (data->BufTextLen != 0)
ImGuiNative.ImGuiInputTextCallbackData_DeleteChars(data, 0, data->BufTextLen);
var len = Encoding.UTF8.GetByteCount(s);
var buf = len < 1024 ? stackalloc byte[len] : new byte[len];
Encoding.UTF8.GetBytes(s, buf);
fixed (byte* pBuf = buf)
ImGuiNative.ImGuiInputTextCallbackData_InsertChars(data, 0, pBuf, pBuf + len);
ImGuiNative.ImGuiInputTextCallbackData_SelectAll(data);
}
/// <summary>
/// Finds the corresponding ImGui viewport ID for the given window handle.

View file

@ -623,15 +623,38 @@ public abstract class Window
/// </summary>
public struct WindowSizeConstraints
{
private Vector2 internalMaxSize = new(float.MaxValue);
/// <summary>
/// Initializes a new instance of the <see cref="WindowSizeConstraints"/> struct.
/// </summary>
public WindowSizeConstraints()
{
}
/// <summary>
/// Gets or sets the minimum size of the window.
/// </summary>
public Vector2 MinimumSize { get; set; }
public Vector2 MinimumSize { get; set; } = new(0);
/// <summary>
/// Gets or sets the maximum size of the window.
/// </summary>
public Vector2 MaximumSize { get; set; }
public Vector2 MaximumSize
{
get => this.GetSafeMaxSize();
set => this.internalMaxSize = value;
}
private Vector2 GetSafeMaxSize()
{
var currentMin = this.MinimumSize;
if (this.internalMaxSize.X < currentMin.X || this.internalMaxSize.Y < currentMin.Y)
return new Vector2(float.MaxValue);
return this.internalMaxSize;
}
}
/// <summary>

View file

@ -1,15 +1,21 @@
using System;
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Memory.Exceptions;
using FFXIVClientStructs.FFXIV.Client.System.Memory;
using FFXIVClientStructs.FFXIV.Client.System.String;
using Microsoft.Extensions.ObjectPool;
using static Dalamud.NativeFunctions;
using LPayloadType = Lumina.Text.Payloads.PayloadType;
using LSeString = Lumina.Text.SeString;
// Heavily inspired from Reloaded (https://github.com/Reloaded-Project/Reloaded.Memory)
namespace Dalamud.Memory;
@ -19,6 +25,47 @@ namespace Dalamud.Memory;
/// </summary>
public static unsafe class MemoryHelper
{
private static readonly ObjectPool<StringBuilder> StringBuilderPool =
ObjectPool.Create(new StringBuilderPooledObjectPolicy());
#region Cast
/// <summary>Casts the given memory address as the reference to the live object.</summary>
/// <param name="memoryAddress">The memory address.</param>
/// <typeparam name="T">The unmanaged type.</typeparam>
/// <returns>The reference to the live object.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ref T Cast<T>(nint memoryAddress) where T : unmanaged => ref *(T*)memoryAddress;
/// <summary>Casts the given memory address as the span of the live object(s).</summary>
/// <param name="memoryAddress">The memory address.</param>
/// <param name="length">The number of items.</param>
/// <typeparam name="T">The unmanaged type.</typeparam>
/// <returns>The span containing reference to the live object(s).</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Span<T> Cast<T>(nint memoryAddress, int length) where T : unmanaged =>
new((void*)memoryAddress, length);
/// <summary>Casts the given memory address as the span of the live object(s), until it encounters a zero.</summary>
/// <param name="memoryAddress">The memory address.</param>
/// <param name="maxLength">The maximum number of items.</param>
/// <typeparam name="T">The unmanaged type.</typeparam>
/// <returns>The span containing reference to the live object(s).</returns>
/// <remarks>If <typeparamref name="T"/> is <c>byte</c> or <c>char</c> and <paramref name="maxLength"/> is not
/// specified, consider using <see cref="MemoryMarshal.CreateReadOnlySpanFromNullTerminated(byte*)"/> or
/// <see cref="MemoryMarshal.CreateReadOnlySpanFromNullTerminated(char*)"/>.</remarks>
public static Span<T> CastNullTerminated<T>(nint memoryAddress, int maxLength = int.MaxValue)
where T : unmanaged, IEquatable<T>
{
var typedPointer = (T*)memoryAddress;
var length = 0;
while (length < maxLength && !default(T).Equals(*typedPointer++))
length++;
return new((void*)memoryAddress, length);
}
#endregion
#region Read
/// <summary>
@ -27,7 +74,9 @@ public static unsafe class MemoryHelper
/// <typeparam name="T">An individual struct type of a class with an explicit StructLayout.LayoutKind attribute.</typeparam>
/// <param name="memoryAddress">The memory address to read from.</param>
/// <returns>The read in struct.</returns>
public static T Read<T>(IntPtr memoryAddress) where T : unmanaged
/// <remarks>If you do not need to make a copy, use <see cref="Cast{T}(nint)"/> instead.</remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static T Read<T>(nint memoryAddress) where T : unmanaged
=> Read<T>(memoryAddress, false);
/// <summary>
@ -37,12 +86,13 @@ public static unsafe class MemoryHelper
/// <param name="memoryAddress">The memory address to read from.</param>
/// <param name="marshal">Set this to true to enable struct marshalling.</param>
/// <returns>The read in struct.</returns>
public static T Read<T>(IntPtr memoryAddress, bool marshal)
{
return marshal
? Marshal.PtrToStructure<T>(memoryAddress)
: Unsafe.Read<T>((void*)memoryAddress);
}
/// <remarks>If you do not need to make a copy and <paramref name="marshal"/> is <c>false</c>,
/// use <see cref="Cast{T}(nint)"/> instead.</remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static T Read<T>(nint memoryAddress, bool marshal) =>
marshal
? Marshal.PtrToStructure<T>(memoryAddress)
: Unsafe.Read<T>((void*)memoryAddress);
/// <summary>
/// Reads a byte array from a specified memory address.
@ -50,12 +100,9 @@ public static unsafe class MemoryHelper
/// <param name="memoryAddress">The memory address to read from.</param>
/// <param name="length">The amount of bytes to read starting from the memoryAddress.</param>
/// <returns>The read in byte array.</returns>
public static byte[] ReadRaw(IntPtr memoryAddress, int length)
{
var value = new byte[length];
Marshal.Copy(memoryAddress, value, 0, value.Length);
return value;
}
/// <remarks>If you do not need to make a copy, use <see cref="Cast{T}(nint,int)"/> instead.</remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static byte[] ReadRaw(nint memoryAddress, int length) => Cast<byte>(memoryAddress, length).ToArray();
/// <summary>
/// Reads a generic type array from a specified memory address.
@ -64,8 +111,10 @@ public static unsafe class MemoryHelper
/// <param name="memoryAddress">The memory address to read from.</param>
/// <param name="arrayLength">The amount of array items to read.</param>
/// <returns>The read in struct array.</returns>
public static T[] Read<T>(IntPtr memoryAddress, int arrayLength) where T : unmanaged
=> Read<T>(memoryAddress, arrayLength, false);
/// <remarks>If you do not need to make a copy, use <see cref="Cast{T}(nint,int)"/> instead.</remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static T[] Read<T>(nint memoryAddress, int arrayLength) where T : unmanaged
=> Cast<T>(memoryAddress, arrayLength).ToArray();
/// <summary>
/// Reads a generic type array from a specified memory address.
@ -75,16 +124,18 @@ public static unsafe class MemoryHelper
/// <param name="arrayLength">The amount of array items to read.</param>
/// <param name="marshal">Set this to true to enable struct marshalling.</param>
/// <returns>The read in struct array.</returns>
public static T[] Read<T>(IntPtr memoryAddress, int arrayLength, bool marshal)
/// <remarks>If you do not need to make a copy and <paramref name="marshal"/> is <c>false</c>,
/// use <see cref="Cast{T}(nint,int)"/> instead.</remarks>
public static T[] Read<T>(nint memoryAddress, int arrayLength, bool marshal)
{
var structSize = SizeOf<T>(marshal);
var value = new T[arrayLength];
for (var i = 0; i < arrayLength; i++)
{
var address = memoryAddress + (structSize * i);
Read(address, out T result, marshal);
Read(memoryAddress, out T result, marshal);
value[i] = result;
memoryAddress += structSize;
}
return value;
@ -95,16 +146,10 @@ public static unsafe class MemoryHelper
/// </summary>
/// <param name="memoryAddress">The memory address to read from.</param>
/// <returns>The read in byte array.</returns>
public static unsafe byte[] ReadRawNullTerminated(IntPtr memoryAddress)
{
var byteCount = 0;
while (*(byte*)(memoryAddress + byteCount) != 0x00)
{
byteCount++;
}
return ReadRaw(memoryAddress, byteCount);
}
/// <remarks>If you do not need to make a copy, use <see cref="CastNullTerminated{T}(nint,int)"/> instead.</remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static byte[] ReadRawNullTerminated(nint memoryAddress) =>
MemoryMarshal.CreateReadOnlySpanFromNullTerminated((byte*)memoryAddress).ToArray();
#endregion
@ -116,7 +161,9 @@ public static unsafe class MemoryHelper
/// <typeparam name="T">An individual struct type of a class with an explicit StructLayout.LayoutKind attribute.</typeparam>
/// <param name="memoryAddress">The memory address to read from.</param>
/// <param name="value">Local variable to receive the read in struct.</param>
public static void Read<T>(IntPtr memoryAddress, out T value) where T : unmanaged
/// <remarks>If you do not need to make a copy, use <see cref="Cast{T}(nint)"/> instead.</remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Read<T>(nint memoryAddress, out T value) where T : unmanaged
=> value = Read<T>(memoryAddress);
/// <summary>
@ -126,7 +173,10 @@ public static unsafe class MemoryHelper
/// <param name="memoryAddress">The memory address to read from.</param>
/// <param name="value">Local variable to receive the read in struct.</param>
/// <param name="marshal">Set this to true to enable struct marshalling.</param>
public static void Read<T>(IntPtr memoryAddress, out T value, bool marshal)
/// <remarks>If you do not need to make a copy and <paramref name="marshal"/> is <c>false</c>,
/// use <see cref="Cast{T}(nint)"/> instead.</remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Read<T>(nint memoryAddress, out T value, bool marshal)
=> value = Read<T>(memoryAddress, marshal);
/// <summary>
@ -135,7 +185,9 @@ public static unsafe class MemoryHelper
/// <param name="memoryAddress">The memory address to read from.</param>
/// <param name="length">The amount of bytes to read starting from the memoryAddress.</param>
/// <param name="value">Local variable to receive the read in bytes.</param>
public static void ReadRaw(IntPtr memoryAddress, int length, out byte[] value)
/// <remarks>If you do not need to make a copy, use <see cref="Cast{T}(nint,int)"/> instead.</remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ReadRaw(nint memoryAddress, int length, out byte[] value)
=> value = ReadRaw(memoryAddress, length);
/// <summary>
@ -145,7 +197,9 @@ public static unsafe class MemoryHelper
/// <param name="memoryAddress">The memory address to read from.</param>
/// <param name="arrayLength">The amount of array items to read.</param>
/// <param name="value">The read in struct array.</param>
public static void Read<T>(IntPtr memoryAddress, int arrayLength, out T[] value) where T : unmanaged
/// <remarks>If you do not need to make a copy, use <see cref="Cast{T}(nint,int)"/> instead.</remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Read<T>(nint memoryAddress, int arrayLength, out T[] value) where T : unmanaged
=> value = Read<T>(memoryAddress, arrayLength);
/// <summary>
@ -156,7 +210,10 @@ public static unsafe class MemoryHelper
/// <param name="arrayLength">The amount of array items to read.</param>
/// <param name="marshal">Set this to true to enable struct marshalling.</param>
/// <param name="value">The read in struct array.</param>
public static void Read<T>(IntPtr memoryAddress, int arrayLength, bool marshal, out T[] value)
/// <remarks>If you do not need to make a copy and <paramref name="marshal"/> is <c>false</c>,
/// use <see cref="Cast{T}(nint,int)"/> instead.</remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Read<T>(nint memoryAddress, int arrayLength, bool marshal, out T[] value)
=> value = Read<T>(memoryAddress, arrayLength, marshal);
#endregion
@ -184,15 +241,27 @@ public static unsafe class MemoryHelper
var length = 0;
while (length < maxLength && pmem[length] != 0)
length++;
var mem = new Span<byte>(pmem, length);
var memCharCount = encoding.GetCharCount(mem);
if (memCharCount != charSpan.Length)
return false;
Span<char> chars = stackalloc char[memCharCount];
encoding.GetChars(mem, chars);
return charSpan.SequenceEqual(chars);
if (memCharCount < 1024)
{
Span<char> chars = stackalloc char[memCharCount];
encoding.GetChars(mem, chars);
return charSpan.SequenceEqual(chars);
}
else
{
var rented = ArrayPool<char>.Shared.Rent(memCharCount);
var chars = rented.AsSpan(0, memCharCount);
encoding.GetChars(mem, chars);
var equals = charSpan.SequenceEqual(chars);
ArrayPool<char>.Shared.Return(rented);
return equals;
}
}
/// <summary>
@ -203,8 +272,9 @@ public static unsafe class MemoryHelper
/// </remarks>
/// <param name="memoryAddress">The memory address to read from.</param>
/// <returns>The read in string.</returns>
public static string ReadStringNullTerminated(IntPtr memoryAddress)
=> ReadStringNullTerminated(memoryAddress, Encoding.UTF8);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string ReadStringNullTerminated(nint memoryAddress)
=> Encoding.UTF8.GetString(MemoryMarshal.CreateReadOnlySpanFromNullTerminated((byte*)memoryAddress));
/// <summary>
/// Read a string with the given encoding from a specified memory address.
@ -215,10 +285,25 @@ public static unsafe class MemoryHelper
/// <param name="memoryAddress">The memory address to read from.</param>
/// <param name="encoding">The encoding to use to decode the string.</param>
/// <returns>The read in string.</returns>
public static string ReadStringNullTerminated(IntPtr memoryAddress, Encoding encoding)
public static string ReadStringNullTerminated(nint memoryAddress, Encoding encoding)
{
var buffer = ReadRawNullTerminated(memoryAddress);
return encoding.GetString(buffer);
switch (encoding)
{
case UTF8Encoding:
case var _ when encoding.IsSingleByte:
return encoding.GetString(MemoryMarshal.CreateReadOnlySpanFromNullTerminated((byte*)memoryAddress));
case UnicodeEncoding:
// Note that it may be in little or big endian, so using `new string(...)` is not always correct.
return encoding.GetString(
MemoryMarshal.Cast<char, byte>(
MemoryMarshal.CreateReadOnlySpanFromNullTerminated((char*)memoryAddress)));
case UTF32Encoding:
return encoding.GetString(MemoryMarshal.Cast<int, byte>(CastNullTerminated<int>(memoryAddress)));
default:
// For correctness' sake; if there does not exist an encoding which will contain a (byte)0 for a
// non-null character, then this branch can be merged with UTF8Encoding one.
return encoding.GetString(ReadRawNullTerminated(memoryAddress));
}
}
/// <summary>
@ -228,10 +313,12 @@ public static unsafe class MemoryHelper
/// Attention! If this is an <see cref="SeString"/>, use the applicable helper methods to decode.
/// </remarks>
/// <param name="memoryAddress">The memory address to read from.</param>
/// <param name="maxLength">The maximum length of the string.</param>
/// <param name="maxLength">The maximum number of bytes to read.
/// Note that this is NOT the maximum length of the returned string.</param>
/// <returns>The read in string.</returns>
public static string ReadString(IntPtr memoryAddress, int maxLength)
=> ReadString(memoryAddress, Encoding.UTF8, maxLength);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string ReadString(nint memoryAddress, int maxLength)
=> Encoding.UTF8.GetString(CastNullTerminated<byte>(memoryAddress, maxLength));
/// <summary>
/// Read a string with the given encoding from a specified memory address.
@ -241,18 +328,32 @@ public static unsafe class MemoryHelper
/// </remarks>
/// <param name="memoryAddress">The memory address to read from.</param>
/// <param name="encoding">The encoding to use to decode the string.</param>
/// <param name="maxLength">The maximum length of the string.</param>
/// <param name="maxLength">The maximum number of bytes to read.
/// Note that this is NOT the maximum length of the returned string.</param>
/// <returns>The read in string.</returns>
public static string ReadString(IntPtr memoryAddress, Encoding encoding, int maxLength)
public static string ReadString(nint memoryAddress, Encoding encoding, int maxLength)
{
if (maxLength <= 0)
return string.Empty;
ReadRaw(memoryAddress, maxLength, out var buffer);
var data = encoding.GetString(buffer);
var eosPos = data.IndexOf('\0');
return eosPos >= 0 ? data.Substring(0, eosPos) : data;
switch (encoding)
{
case UTF8Encoding:
case var _ when encoding.IsSingleByte:
return encoding.GetString(CastNullTerminated<byte>(memoryAddress, maxLength));
case UnicodeEncoding:
return encoding.GetString(
MemoryMarshal.Cast<char, byte>(CastNullTerminated<char>(memoryAddress, maxLength / 2)));
case UTF32Encoding:
return encoding.GetString(
MemoryMarshal.Cast<int, byte>(CastNullTerminated<int>(memoryAddress, maxLength / 4)));
default:
// For correctness' sake; if there does not exist an encoding which will contain a (byte)0 for a
// non-null character, then this branch can be merged with UTF8Encoding one.
var data = encoding.GetString(Cast<byte>(memoryAddress, maxLength));
var eosPos = data.IndexOf('\0');
return eosPos >= 0 ? data[..eosPos] : data;
}
}
/// <summary>
@ -260,11 +361,9 @@ public static unsafe class MemoryHelper
/// </summary>
/// <param name="memoryAddress">The memory address to read from.</param>
/// <returns>The read in string.</returns>
public static SeString ReadSeStringNullTerminated(IntPtr memoryAddress)
{
var buffer = ReadRawNullTerminated(memoryAddress);
return SeString.Parse(buffer);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static SeString ReadSeStringNullTerminated(nint memoryAddress) =>
SeString.Parse(MemoryMarshal.CreateReadOnlySpanFromNullTerminated((byte*)memoryAddress));
/// <summary>
/// Read an SeString from a specified memory address.
@ -272,40 +371,165 @@ public static unsafe class MemoryHelper
/// <param name="memoryAddress">The memory address to read from.</param>
/// <param name="maxLength">The maximum length of the string.</param>
/// <returns>The read in string.</returns>
public static SeString ReadSeString(IntPtr memoryAddress, int maxLength)
{
ReadRaw(memoryAddress, maxLength, out var buffer);
var eos = Array.IndexOf(buffer, (byte)0);
if (eos < 0)
{
return SeString.Parse(buffer);
}
else
{
var newBuffer = new byte[eos];
Buffer.BlockCopy(buffer, 0, newBuffer, 0, eos);
return SeString.Parse(newBuffer);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static SeString ReadSeString(nint memoryAddress, int maxLength) =>
// Note that a valid SeString never contains a null character, other than for the sequence terminator purpose.
SeString.Parse(CastNullTerminated<byte>(memoryAddress, maxLength));
/// <summary>
/// Read an SeString from a specified Utf8String structure.
/// </summary>
/// <param name="utf8String">The memory address to read from.</param>
/// <returns>The read in string.</returns>
public static unsafe SeString ReadSeString(Utf8String* utf8String)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static SeString ReadSeString(Utf8String* utf8String) =>
utf8String == null ? string.Empty : SeString.Parse(utf8String->AsSpan());
/// <summary>
/// Reads an SeString from a specified memory address, and extracts the outermost string.<br />
/// If the SeString is malformed, behavior is undefined.
/// </summary>
/// <param name="containsNonRepresentedPayload">Whether the SeString contained a non-represented payload.</param>
/// <param name="memoryAddress">The memory address to read from.</param>
/// <param name="maxLength">The maximum length of the string.</param>
/// <param name="stopOnFirstNonRepresentedPayload">Stop reading on encountering the first non-represented payload.
/// What payloads are represented via this function may change.</param>
/// <param name="nonRepresentedPayloadReplacement">Replacement for non-represented payloads.</param>
/// <returns>The read in string.</returns>
public static string ReadSeStringAsString(
out bool containsNonRepresentedPayload,
nint memoryAddress,
int maxLength = int.MaxValue,
bool stopOnFirstNonRepresentedPayload = false,
string nonRepresentedPayloadReplacement = "*")
{
if (utf8String == null)
return string.Empty;
var sb = StringBuilderPool.Get();
sb.EnsureCapacity(maxLength = CastNullTerminated<byte>(memoryAddress, maxLength).Length);
var ptr = utf8String->StringPtr;
if (ptr == null)
return string.Empty;
// 1 utf-8 codepoint can spill up to 2 characters.
Span<char> tmp = stackalloc char[2];
var len = Math.Max(utf8String->BufUsed, utf8String->StringLength);
var pin = (byte*)memoryAddress;
containsNonRepresentedPayload = false;
while (*pin != 0 && maxLength > 0)
{
if (*pin != LSeString.StartByte)
{
var len = *pin switch
{
< 0x80 => 1,
>= 0b11000000 and <= 0b11011111 => 2,
>= 0b11100000 and <= 0b11101111 => 3,
>= 0b11110000 and <= 0b11110111 => 4,
_ => 0,
};
if (len == 0 || len > maxLength)
break;
return ReadSeString((IntPtr)ptr, (int)len);
var numChars = Encoding.UTF8.GetChars(new(pin, len), tmp);
sb.Append(tmp[..numChars]);
pin += len;
maxLength -= len;
continue;
}
// Start byte
++pin;
--maxLength;
// Payload type
var payloadType = (LPayloadType)(*pin++);
// Payload length
if (!ReadIntExpression(ref pin, ref maxLength, out var expressionLength))
break;
if (expressionLength > maxLength)
break;
pin += expressionLength;
maxLength -= unchecked((int)expressionLength);
// End byte
if (*pin++ != LSeString.EndByte)
break;
--maxLength;
switch (payloadType)
{
case LPayloadType.NewLine:
sb.AppendLine();
break;
case LPayloadType.Hyphen:
sb.Append('');
break;
case LPayloadType.SoftHyphen:
sb.Append('\u00AD');
break;
default:
sb.Append(nonRepresentedPayloadReplacement);
containsNonRepresentedPayload = true;
if (stopOnFirstNonRepresentedPayload)
maxLength = 0;
break;
}
}
var res = sb.ToString();
StringBuilderPool.Return(sb);
return res;
static bool ReadIntExpression(ref byte* p, ref int maxLength, out uint value)
{
if (maxLength <= 0)
{
value = 0;
return false;
}
var typeByte = *p++;
--maxLength;
switch (typeByte)
{
case > 0 and < 0xD0:
value = (uint)typeByte - 1;
return true;
case >= 0xF0 and <= 0xFE:
++typeByte;
value = 0u;
if ((typeByte & 8) != 0)
{
if (maxLength <= 0 || *p == 0)
return false;
value |= (uint)*p++ << 24;
}
if ((typeByte & 4) != 0)
{
if (maxLength <= 0 || *p == 0)
return false;
value |= (uint)*p++ << 16;
}
if ((typeByte & 2) != 0)
{
if (maxLength <= 0 || *p == 0)
return false;
value |= (uint)*p++ << 8;
}
if ((typeByte & 1) != 0)
{
if (maxLength <= 0 || *p == 0)
return false;
value |= *p++;
}
return true;
default:
value = 0;
return false;
}
}
}
#endregion
@ -320,7 +544,8 @@ public static unsafe class MemoryHelper
/// </remarks>
/// <param name="memoryAddress">The memory address to read from.</param>
/// <param name="value">The read in string.</param>
public static void ReadStringNullTerminated(IntPtr memoryAddress, out string value)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ReadStringNullTerminated(nint memoryAddress, out string value)
=> value = ReadStringNullTerminated(memoryAddress);
/// <summary>
@ -332,7 +557,8 @@ public static unsafe class MemoryHelper
/// <param name="memoryAddress">The memory address to read from.</param>
/// <param name="encoding">The encoding to use to decode the string.</param>
/// <param name="value">The read in string.</param>
public static void ReadStringNullTerminated(IntPtr memoryAddress, Encoding encoding, out string value)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ReadStringNullTerminated(nint memoryAddress, Encoding encoding, out string value)
=> value = ReadStringNullTerminated(memoryAddress, encoding);
/// <summary>
@ -344,7 +570,8 @@ public static unsafe class MemoryHelper
/// <param name="memoryAddress">The memory address to read from.</param>
/// <param name="value">The read in string.</param>
/// <param name="maxLength">The maximum length of the string.</param>
public static void ReadString(IntPtr memoryAddress, out string value, int maxLength)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ReadString(nint memoryAddress, out string value, int maxLength)
=> value = ReadString(memoryAddress, maxLength);
/// <summary>
@ -357,7 +584,8 @@ public static unsafe class MemoryHelper
/// <param name="encoding">The encoding to use to decode the string.</param>
/// <param name="maxLength">The maximum length of the string.</param>
/// <param name="value">The read in string.</param>
public static void ReadString(IntPtr memoryAddress, Encoding encoding, int maxLength, out string value)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ReadString(nint memoryAddress, Encoding encoding, int maxLength, out string value)
=> value = ReadString(memoryAddress, encoding, maxLength);
/// <summary>
@ -365,7 +593,8 @@ public static unsafe class MemoryHelper
/// </summary>
/// <param name="memoryAddress">The memory address to read from.</param>
/// <param name="value">The read in SeString.</param>
public static void ReadSeStringNullTerminated(IntPtr memoryAddress, out SeString value)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ReadSeStringNullTerminated(nint memoryAddress, out SeString value)
=> value = ReadSeStringNullTerminated(memoryAddress);
/// <summary>
@ -374,7 +603,8 @@ public static unsafe class MemoryHelper
/// <param name="memoryAddress">The memory address to read from.</param>
/// <param name="maxLength">The maximum length of the string.</param>
/// <param name="value">The read in SeString.</param>
public static void ReadSeString(IntPtr memoryAddress, int maxLength, out SeString value)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ReadSeString(nint memoryAddress, int maxLength, out SeString value)
=> value = ReadSeString(memoryAddress, maxLength);
/// <summary>
@ -382,6 +612,7 @@ public static unsafe class MemoryHelper
/// </summary>
/// <param name="utf8String">The memory address to read from.</param>
/// <param name="value">The read in string.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static unsafe void ReadSeString(Utf8String* utf8String, out SeString value)
=> value = ReadSeString(utf8String);
@ -395,7 +626,8 @@ public static unsafe class MemoryHelper
/// <typeparam name="T">An individual struct type of a class with an explicit StructLayout.LayoutKind attribute.</typeparam>
/// <param name="memoryAddress">The memory address to read from.</param>
/// <param name="item">The item to write to the address.</param>
public static void Write<T>(IntPtr memoryAddress, T item) where T : unmanaged
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Write<T>(nint memoryAddress, T item) where T : unmanaged
=> Write(memoryAddress, item, false);
/// <summary>
@ -405,7 +637,7 @@ public static unsafe class MemoryHelper
/// <param name="memoryAddress">The memory address to read from.</param>
/// <param name="item">The item to write to the address.</param>
/// <param name="marshal">Set this to true to enable struct marshalling.</param>
public static void Write<T>(IntPtr memoryAddress, T item, bool marshal)
public static void Write<T>(nint memoryAddress, T item, bool marshal)
{
if (marshal)
Marshal.StructureToPtr(item, memoryAddress, false);
@ -418,10 +650,8 @@ public static unsafe class MemoryHelper
/// </summary>
/// <param name="memoryAddress">The memory address to read from.</param>
/// <param name="data">The bytes to write to memoryAddress.</param>
public static void WriteRaw(IntPtr memoryAddress, byte[] data)
{
Marshal.Copy(data, 0, memoryAddress, data.Length);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void WriteRaw(nint memoryAddress, byte[] data) => Marshal.Copy(data, 0, memoryAddress, data.Length);
/// <summary>
/// Writes a generic type array to a specified memory address.
@ -429,7 +659,8 @@ public static unsafe class MemoryHelper
/// <typeparam name="T">An individual struct type of a class with an explicit StructLayout.LayoutKind attribute.</typeparam>
/// <param name="memoryAddress">The memory address to write to.</param>
/// <param name="items">The array of items to write to the address.</param>
public static void Write<T>(IntPtr memoryAddress, T[] items) where T : unmanaged
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Write<T>(nint memoryAddress, T[] items) where T : unmanaged
=> Write(memoryAddress, items, false);
/// <summary>
@ -439,7 +670,8 @@ public static unsafe class MemoryHelper
/// <param name="memoryAddress">The memory address to write to.</param>
/// <param name="items">The array of items to write to the address.</param>
/// <param name="marshal">Set this to true to enable struct marshalling.</param>
public static void Write<T>(IntPtr memoryAddress, T[] items, bool marshal)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Write<T>(nint memoryAddress, T[] items, bool marshal)
{
var structSize = SizeOf<T>(marshal);
@ -462,7 +694,8 @@ public static unsafe class MemoryHelper
/// </remarks>
/// <param name="memoryAddress">The memory address to write to.</param>
/// <param name="value">The string to write.</param>
public static void WriteString(IntPtr memoryAddress, string value)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void WriteString(nint memoryAddress, string? value)
=> WriteString(memoryAddress, value, Encoding.UTF8);
/// <summary>
@ -474,14 +707,12 @@ public static unsafe class MemoryHelper
/// <param name="memoryAddress">The memory address to write to.</param>
/// <param name="value">The string to write.</param>
/// <param name="encoding">The encoding to use.</param>
public static void WriteString(IntPtr memoryAddress, string value, Encoding encoding)
public static void WriteString(nint memoryAddress, string? value, Encoding encoding)
{
if (string.IsNullOrEmpty(value))
return;
var bytes = encoding.GetBytes(value + '\0');
WriteRaw(memoryAddress, bytes);
var ptr = 0;
if (value is not null)
ptr = encoding.GetBytes(value, Cast<byte>(memoryAddress, encoding.GetMaxByteCount(value.Length)));
encoding.GetBytes("\0", Cast<byte>(memoryAddress + ptr, 4));
}
/// <summary>
@ -489,7 +720,8 @@ public static unsafe class MemoryHelper
/// </summary>
/// <param name="memoryAddress">The memory address to write to.</param>
/// <param name="value">The SeString to write.</param>
public static void WriteSeString(IntPtr memoryAddress, SeString value)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void WriteSeString(nint memoryAddress, SeString? value)
{
if (value is null)
return;
@ -507,15 +739,16 @@ public static unsafe class MemoryHelper
/// </summary>
/// <param name="length">Amount of bytes to be allocated.</param>
/// <returns>Address to the newly allocated memory.</returns>
public static IntPtr Allocate(int length)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static nint Allocate(int length)
{
var address = VirtualAlloc(
IntPtr.Zero,
(UIntPtr)length,
nint.Zero,
(nuint)length,
AllocationType.Commit | AllocationType.Reserve,
MemoryProtection.ExecuteReadWrite);
if (address == IntPtr.Zero)
if (address == nint.Zero)
throw new MemoryAllocationException($"Unable to allocate {length} bytes.");
return address;
@ -527,7 +760,8 @@ public static unsafe class MemoryHelper
/// </summary>
/// <param name="length">Amount of bytes to be allocated.</param>
/// <param name="memoryAddress">Address to the newly allocated memory.</param>
public static void Allocate(int length, out IntPtr memoryAddress)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Allocate(int length, out nint memoryAddress)
=> memoryAddress = Allocate(length);
/// <summary>
@ -535,9 +769,10 @@ public static unsafe class MemoryHelper
/// </summary>
/// <param name="memoryAddress">The address of the memory to free.</param>
/// <returns>True if the operation is successful.</returns>
public static bool Free(IntPtr memoryAddress)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool Free(nint memoryAddress)
{
return VirtualFree(memoryAddress, UIntPtr.Zero, AllocationType.Release);
return VirtualFree(memoryAddress, nuint.Zero, AllocationType.Release);
}
/// <summary>
@ -547,9 +782,9 @@ public static unsafe class MemoryHelper
/// <param name="length">The region size for which to change permissions for.</param>
/// <param name="newPermissions">The new permissions to set.</param>
/// <returns>The old page permissions.</returns>
public static MemoryProtection ChangePermission(IntPtr memoryAddress, int length, MemoryProtection newPermissions)
public static MemoryProtection ChangePermission(nint memoryAddress, int length, MemoryProtection newPermissions)
{
var result = VirtualProtect(memoryAddress, (UIntPtr)length, newPermissions, out var oldPermissions);
var result = VirtualProtect(memoryAddress, (nuint)length, newPermissions, out var oldPermissions);
if (!result)
throw new MemoryPermissionException($"Unable to change permissions at 0x{memoryAddress.ToInt64():X} of length {length} and permission {newPermissions} (result={result})");
@ -568,7 +803,9 @@ public static unsafe class MemoryHelper
/// <param name="length">The region size for which to change permissions for.</param>
/// <param name="newPermissions">The new permissions to set.</param>
/// <param name="oldPermissions">The old page permissions.</param>
public static void ChangePermission(IntPtr memoryAddress, int length, MemoryProtection newPermissions, out MemoryProtection oldPermissions)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ChangePermission(
nint memoryAddress, int length, MemoryProtection newPermissions, out MemoryProtection oldPermissions)
=> oldPermissions = ChangePermission(memoryAddress, length, newPermissions);
/// <summary>
@ -580,7 +817,9 @@ public static unsafe class MemoryHelper
/// <param name="newPermissions">The new permissions to set.</param>
/// <param name="marshal">Set to true to calculate the size of the struct after marshalling instead of before.</param>
/// <returns>The old page permissions.</returns>
public static MemoryProtection ChangePermission<T>(IntPtr memoryAddress, ref T baseElement, MemoryProtection newPermissions, bool marshal)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static MemoryProtection ChangePermission<T>(
nint memoryAddress, ref T baseElement, MemoryProtection newPermissions, bool marshal)
=> ChangePermission(memoryAddress, SizeOf<T>(marshal), newPermissions);
/// <summary>
@ -590,7 +829,8 @@ public static unsafe class MemoryHelper
/// <param name="memoryAddress">The memory address to read from.</param>
/// <param name="length">The amount of bytes to read starting from the memoryAddress.</param>
/// <returns>The read in bytes.</returns>
public static byte[] ReadProcessMemory(IntPtr memoryAddress, int length)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static byte[] ReadProcessMemory(nint memoryAddress, int length)
{
var value = new byte[length];
ReadProcessMemory(memoryAddress, ref value);
@ -604,7 +844,8 @@ public static unsafe class MemoryHelper
/// <param name="memoryAddress">The memory address to read from.</param>
/// <param name="length">The amount of bytes to read starting from the memoryAddress.</param>
/// <param name="value">The read in bytes.</param>
public static void ReadProcessMemory(IntPtr memoryAddress, int length, out byte[] value)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ReadProcessMemory(nint memoryAddress, int length, out byte[] value)
=> value = ReadProcessMemory(memoryAddress, length);
/// <summary>
@ -613,12 +854,12 @@ public static unsafe class MemoryHelper
/// </summary>
/// <param name="memoryAddress">The memory address to read from.</param>
/// <param name="value">The read in bytes.</param>
public static void ReadProcessMemory(IntPtr memoryAddress, ref byte[] value)
public static void ReadProcessMemory(nint memoryAddress, ref byte[] value)
{
unchecked
{
var length = value.Length;
var result = NativeFunctions.ReadProcessMemory((IntPtr)0xFFFFFFFF, memoryAddress, value, length, out _);
var result = NativeFunctions.ReadProcessMemory((nint)0xFFFFFFFF, memoryAddress, value, length, out _);
if (!result)
throw new MemoryReadException($"Unable to read memory at 0x{memoryAddress.ToInt64():X} of length {length} (result={result})");
@ -635,12 +876,12 @@ public static unsafe class MemoryHelper
/// </summary>
/// <param name="memoryAddress">The memory address to write to.</param>
/// <param name="data">The bytes to write to memoryAddress.</param>
public static void WriteProcessMemory(IntPtr memoryAddress, byte[] data)
public static void WriteProcessMemory(nint memoryAddress, byte[] data)
{
unchecked
{
var length = data.Length;
var result = NativeFunctions.WriteProcessMemory((IntPtr)0xFFFFFFFF, memoryAddress, data, length, out _);
var result = NativeFunctions.WriteProcessMemory((nint)0xFFFFFFFF, memoryAddress, data, length, out _);
if (!result)
throw new MemoryWriteException($"Unable to write memory at 0x{memoryAddress.ToInt64():X} of length {length} (result={result})");
@ -660,6 +901,7 @@ public static unsafe class MemoryHelper
/// </summary>
/// <typeparam name="T">An individual struct type of a class with an explicit StructLayout.LayoutKind attribute.</typeparam>
/// <returns>The size of the primitive or struct.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int SizeOf<T>()
=> SizeOf<T>(false);
@ -669,6 +911,7 @@ public static unsafe class MemoryHelper
/// <typeparam name="T">An individual struct type of a class with an explicit StructLayout.LayoutKind attribute.</typeparam>
/// <param name="marshal">If set to true; will return the size of an element after marshalling.</param>
/// <returns>The size of the primitive or struct.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int SizeOf<T>(bool marshal)
=> marshal ? Marshal.SizeOf<T>() : Unsafe.SizeOf<T>();
@ -678,6 +921,7 @@ public static unsafe class MemoryHelper
/// <typeparam name="T">An individual struct type of a class with an explicit StructLayout.LayoutKind attribute.</typeparam>
/// <param name="elementCount">The number of array elements present.</param>
/// <returns>The size of the primitive or struct array.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int SizeOf<T>(int elementCount) where T : unmanaged
=> SizeOf<T>() * elementCount;
@ -688,6 +932,7 @@ public static unsafe class MemoryHelper
/// <param name="elementCount">The number of array elements present.</param>
/// <param name="marshal">If set to true; will return the size of an element after marshalling.</param>
/// <returns>The size of the primitive or struct array.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int SizeOf<T>(int elementCount, bool marshal)
=> SizeOf<T>(marshal) * elementCount;
@ -701,9 +946,10 @@ public static unsafe class MemoryHelper
/// <param name="size">Amount of bytes to allocate.</param>
/// <param name="alignment">The alignment of the allocation.</param>
/// <returns>Pointer to the allocated region.</returns>
public static IntPtr GameAllocateUi(ulong size, ulong alignment = 0)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static nint GameAllocateUi(ulong size, ulong alignment = 0)
{
return new IntPtr(IMemorySpace.GetUISpace()->Malloc(size, alignment));
return new nint(IMemorySpace.GetUISpace()->Malloc(size, alignment));
}
/// <summary>
@ -712,9 +958,10 @@ public static unsafe class MemoryHelper
/// <param name="size">Amount of bytes to allocate.</param>
/// <param name="alignment">The alignment of the allocation.</param>
/// <returns>Pointer to the allocated region.</returns>
public static IntPtr GameAllocateDefault(ulong size, ulong alignment = 0)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static nint GameAllocateDefault(ulong size, ulong alignment = 0)
{
return new IntPtr(IMemorySpace.GetDefaultSpace()->Malloc(size, alignment));
return new nint(IMemorySpace.GetDefaultSpace()->Malloc(size, alignment));
}
/// <summary>
@ -723,9 +970,10 @@ public static unsafe class MemoryHelper
/// <param name="size">Amount of bytes to allocate.</param>
/// <param name="alignment">The alignment of the allocation.</param>
/// <returns>Pointer to the allocated region.</returns>
public static IntPtr GameAllocateAnimation(ulong size, ulong alignment = 0)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static nint GameAllocateAnimation(ulong size, ulong alignment = 0)
{
return new IntPtr(IMemorySpace.GetAnimationSpace()->Malloc(size, alignment));
return new nint(IMemorySpace.GetAnimationSpace()->Malloc(size, alignment));
}
/// <summary>
@ -734,9 +982,10 @@ public static unsafe class MemoryHelper
/// <param name="size">Amount of bytes to allocate.</param>
/// <param name="alignment">The alignment of the allocation.</param>
/// <returns>Pointer to the allocated region.</returns>
public static IntPtr GameAllocateApricot(ulong size, ulong alignment = 0)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static nint GameAllocateApricot(ulong size, ulong alignment = 0)
{
return new IntPtr(IMemorySpace.GetApricotSpace()->Malloc(size, alignment));
return new nint(IMemorySpace.GetApricotSpace()->Malloc(size, alignment));
}
/// <summary>
@ -745,9 +994,10 @@ public static unsafe class MemoryHelper
/// <param name="size">Amount of bytes to allocate.</param>
/// <param name="alignment">The alignment of the allocation.</param>
/// <returns>Pointer to the allocated region.</returns>
public static IntPtr GameAllocateSound(ulong size, ulong alignment = 0)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static nint GameAllocateSound(ulong size, ulong alignment = 0)
{
return new IntPtr(IMemorySpace.GetSoundSpace()->Malloc(size, alignment));
return new nint(IMemorySpace.GetSoundSpace()->Malloc(size, alignment));
}
/// <summary>
@ -756,15 +1006,15 @@ public static unsafe class MemoryHelper
/// <remarks>The memory you are freeing must be allocated with game allocators.</remarks>
/// <param name="ptr">Position at which the memory to be freed is located.</param>
/// <param name="size">Amount of bytes to free.</param>
public static void GameFree(ref IntPtr ptr, ulong size)
public static void GameFree(ref nint ptr, ulong size)
{
if (ptr == IntPtr.Zero)
if (ptr == nint.Zero)
{
return;
}
IMemorySpace.Free((void*)ptr, size);
ptr = IntPtr.Zero;
ptr = nint.Zero;
}
#endregion

View file

@ -0,0 +1,37 @@
using Dalamud.Game.Gui.ContextMenu;
using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling;
namespace Dalamud.Plugin.Services;
/// <summary>
/// This class provides methods for interacting with the game's context menu.
/// </summary>
public interface IContextMenu
{
/// <summary>
/// A delegate type used for the <see cref="OnMenuOpened"/> event.
/// </summary>
/// <param name="args">Information about the currently opening menu.</param>
public delegate void OnMenuOpenedDelegate(MenuOpenedArgs args);
/// <summary>
/// Event that gets fired every time the game framework updates.
/// </summary>
event OnMenuOpenedDelegate OnMenuOpened;
/// <summary>
/// Adds a menu item to a context menu.
/// </summary>
/// <param name="menuType">The type of context menu to add the item to.</param>
/// <param name="item">The item to add.</param>
void AddMenuItem(ContextMenuType menuType, MenuItem item);
/// <summary>
/// Removes a menu item from a context menu.
/// </summary>
/// <param name="menuType">The type of context menu to remove the item from.</param>
/// <param name="item">The item to add.</param>
/// <returns><see langword="true"/> if the item was removed, <see langword="false"/> if it was not found.</returns>
bool RemoveMenuItem(ContextMenuType menuType, MenuItem item);
}

View file

@ -1,14 +1,23 @@
using System;
using System.Diagnostics;
using System.Diagnostics;
using System.Threading.Tasks;
using Dalamud.Game.Config;
using FFXIVClientStructs.FFXIV.Common.Configuration;
using Dalamud.Plugin.Internal.Types;
namespace Dalamud.Plugin.Services;
/// <summary>
/// This class represents the game's configuration.
/// </summary>
/// <remarks>
/// Accessing <see cref="GameConfigSection"/>-typed properties such as <see cref="System"/>, directly or indirectly
/// via <see cref="TryGet(Game.Config.SystemConfigOption,out bool)"/>,
/// <see cref="Set(Game.Config.SystemConfigOption,bool)"/>, or alike will block, if the game is not done loading.<br />
/// Therefore, avoid accessing configuration from your plugin constructor, especially if your plugin sets
/// <see cref="PluginManifest.LoadRequiredState"/> to <c>2</c> and <see cref="PluginManifest.LoadSync"/> to <c>true</c>.
/// If property access from the plugin constructor is desired, do the value retrieval asynchronously via
/// <see cref="IFramework.RunOnFrameworkThread{T}(Func{T})"/>; do not wait for the result right away.
/// </remarks>
public interface IGameConfig
{
/// <summary>

View file

@ -194,12 +194,14 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA
try
{
await using var tempPathStream = File.Open(tempPath, FileMode.Create, FileAccess.Write);
await url.DownloadAsync(
this.httpClient.SharedHttpClient,
tempPathStream,
this.cancellationTokenSource.Token);
tempPathStream.Dispose();
await using (var tempPathStream = File.Open(tempPath, FileMode.Create, FileAccess.Write))
{
await url.DownloadAsync(
this.httpClient.SharedHttpClient,
tempPathStream,
this.cancellationTokenSource.Token);
}
for (var j = RenameAttemptCount; ; j--)
{
try
@ -265,7 +267,7 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA
/// <inheritdoc/>
[Pure]
public IDalamudTextureWrap GetDalamudTextureWrap(DalamudAsset asset) =>
ExtractResult(this.GetDalamudTextureWrapAsync(asset));
this.GetDalamudTextureWrapAsync(asset).Result;
/// <inheritdoc/>
[Pure]
@ -332,8 +334,6 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA
}
}
private static T ExtractResult<T>(Task<T> t) => t.IsCompleted ? t.Result : t.GetAwaiter().GetResult();
private Task<TOut> TransformImmediate<TIn, TOut>(Task<TIn> task, Func<TIn, TOut> transformer)
{
if (task.IsCompletedSuccessfully)

View file

@ -16,7 +16,7 @@ namespace Dalamud.Storage.Assets;
/// Think of C++ [[nodiscard]]. Also, like the intended meaning of the attribute, such methods will not have
/// externally visible state changes.
/// </summary>
internal interface IDalamudAssetManager
public interface IDalamudAssetManager
{
/// <summary>
/// Gets the shared texture wrap for <see cref="DalamudAsset.Empty4X4"/>.

View file

@ -97,4 +97,76 @@ internal static class ArrayExtensions
/// <returns><paramref name="array"/> casted as a <see cref="IReadOnlyCollection{T}"/> if it is one; otherwise the result of <see cref="Enumerable.ToArray{TSource}"/>.</returns>
public static IReadOnlyCollection<T> AsReadOnlyCollection<T>(this IEnumerable<T> array) =>
array as IReadOnlyCollection<T> ?? array.ToArray();
/// <inheritdoc cref="List{T}.FindIndex(System.Predicate{T})"/>
public static int FindIndex<T>(this IReadOnlyList<T> list, Predicate<T> match)
=> list.FindIndex(0, list.Count, match);
/// <inheritdoc cref="List{T}.FindIndex(int,System.Predicate{T})"/>
public static int FindIndex<T>(this IReadOnlyList<T> list, int startIndex, Predicate<T> match)
=> list.FindIndex(startIndex, list.Count - startIndex, match);
/// <inheritdoc cref="List{T}.FindIndex(int,int,System.Predicate{T})"/>
public static int FindIndex<T>(this IReadOnlyList<T> list, int startIndex, int count, Predicate<T> match)
{
if ((uint)startIndex > (uint)list.Count)
throw new ArgumentOutOfRangeException(nameof(startIndex), startIndex, null);
if (count < 0 || startIndex > list.Count - count)
throw new ArgumentOutOfRangeException(nameof(count), count, null);
if (match == null)
throw new ArgumentNullException(nameof(match));
var endIndex = startIndex + count;
for (var i = startIndex; i < endIndex; i++)
{
if (match(list[i])) return i;
}
return -1;
}
/// <inheritdoc cref="List{T}.FindLastIndex(System.Predicate{T})"/>
public static int FindLastIndex<T>(this IReadOnlyList<T> list, Predicate<T> match)
=> list.FindLastIndex(list.Count - 1, list.Count, match);
/// <inheritdoc cref="List{T}.FindLastIndex(int,System.Predicate{T})"/>
public static int FindLastIndex<T>(this IReadOnlyList<T> list, int startIndex, Predicate<T> match)
=> list.FindLastIndex(startIndex, startIndex + 1, match);
/// <inheritdoc cref="List{T}.FindLastIndex(int,int,System.Predicate{T})"/>
public static int FindLastIndex<T>(this IReadOnlyList<T> list, int startIndex, int count, Predicate<T> match)
{
if (match == null)
throw new ArgumentNullException(nameof(match));
if (list.Count == 0)
{
// Special case for 0 length List
if (startIndex != -1)
throw new ArgumentOutOfRangeException(nameof(startIndex), startIndex, null);
}
else
{
// Make sure we're not out of range
if ((uint)startIndex >= (uint)list.Count)
throw new ArgumentOutOfRangeException(nameof(startIndex), startIndex, null);
}
// 2nd have of this also catches when startIndex == MAXINT, so MAXINT - 0 + 1 == -1, which is < 0.
if (count < 0 || startIndex - count + 1 < 0)
throw new ArgumentOutOfRangeException(nameof(count), count, null);
var endIndex = startIndex - count;
for (var i = startIndex; i > endIndex; i--)
{
if (match(list[i]))
{
return i;
}
}
return -1;
}
}

View file

@ -39,21 +39,23 @@ public static class DisposeSafety
public static IDisposable ToDisposableIgnoreExceptions<T>(this Task<T> task)
where T : IDisposable
{
return Disposable.Create(() => task.ContinueWith(r =>
{
_ = r.Exception;
if (r.IsCompleted)
{
try
return Disposable.Create(
() => task.ContinueWith(
r =>
{
r.Dispose();
}
catch
{
// ignore
}
}
}));
_ = r.Exception;
if (r.IsCompleted)
{
try
{
r.Dispose();
}
catch
{
// ignore
}
}
}));
}
/// <summary>
@ -102,25 +104,26 @@ public static class DisposeSafety
if (disposables is not T[] array)
array = disposables?.ToArray() ?? Array.Empty<T>();
return Disposable.Create(() =>
{
List<Exception?> exceptions = null;
foreach (var d in array)
return Disposable.Create(
() =>
{
try
List<Exception?> exceptions = null;
foreach (var d in array)
{
d?.Dispose();
try
{
d?.Dispose();
}
catch (Exception de)
{
exceptions ??= new();
exceptions.Add(de);
}
}
catch (Exception de)
{
exceptions ??= new();
exceptions.Add(de);
}
}
if (exceptions is not null)
throw new AggregateException(exceptions);
});
if (exceptions is not null)
throw new AggregateException(exceptions);
});
}
/// <summary>
@ -137,7 +140,11 @@ public static class DisposeSafety
public event Action<IDisposeCallback, Exception?>? AfterDispose;
/// <inheritdoc cref="Stack{T}.EnsureCapacity"/>
public void EnsureCapacity(int capacity) => this.objects.EnsureCapacity(capacity);
public void EnsureCapacity(int capacity)
{
lock (this.objects)
this.objects.EnsureCapacity(capacity);
}
/// <inheritdoc cref="Stack{T}.Push"/>
/// <returns>The parameter.</returns>
@ -145,7 +152,10 @@ public static class DisposeSafety
public T? Add<T>(T? d) where T : IDisposable
{
if (d is not null)
this.objects.Add(this.CheckAdd(d));
{
lock (this.objects)
this.objects.Add(this.CheckAdd(d));
}
return d;
}
@ -155,7 +165,10 @@ public static class DisposeSafety
public Action? Add(Action? d)
{
if (d is not null)
this.objects.Add(this.CheckAdd(d));
{
lock (this.objects)
this.objects.Add(this.CheckAdd(d));
}
return d;
}
@ -165,7 +178,10 @@ public static class DisposeSafety
public Func<Task>? Add(Func<Task>? d)
{
if (d is not null)
this.objects.Add(this.CheckAdd(d));
{
lock (this.objects)
this.objects.Add(this.CheckAdd(d));
}
return d;
}
@ -174,7 +190,10 @@ public static class DisposeSafety
public GCHandle Add(GCHandle d)
{
if (d != default)
this.objects.Add(this.CheckAdd(d));
{
lock (this.objects)
this.objects.Add(this.CheckAdd(d));
}
return d;
}
@ -183,29 +202,41 @@ public static class DisposeSafety
/// Queue all the given <see cref="IDisposable"/> to be disposed later.
/// </summary>
/// <param name="ds">Disposables.</param>
public void AddRange(IEnumerable<IDisposable?> ds) =>
this.objects.AddRange(ds.Where(d => d is not null).Select(d => (object)this.CheckAdd(d)));
public void AddRange(IEnumerable<IDisposable?> ds)
{
lock (this.objects)
this.objects.AddRange(ds.Where(d => d is not null).Select(d => (object)this.CheckAdd(d)));
}
/// <summary>
/// Queue all the given <see cref="IDisposable"/> to be run later.
/// </summary>
/// <param name="ds">Actions.</param>
public void AddRange(IEnumerable<Action?> ds) =>
this.objects.AddRange(ds.Where(d => d is not null).Select(d => (object)this.CheckAdd(d)));
public void AddRange(IEnumerable<Action?> ds)
{
lock (this.objects)
this.objects.AddRange(ds.Where(d => d is not null).Select(d => (object)this.CheckAdd(d)));
}
/// <summary>
/// Queue all the given <see cref="Func{T}"/> returning <see cref="Task"/> to be run later.
/// </summary>
/// <param name="ds">Func{Task}s.</param>
public void AddRange(IEnumerable<Func<Task>?> ds) =>
this.objects.AddRange(ds.Where(d => d is not null).Select(d => (object)this.CheckAdd(d)));
public void AddRange(IEnumerable<Func<Task>?> ds)
{
lock (this.objects)
this.objects.AddRange(ds.Where(d => d is not null).Select(d => (object)this.CheckAdd(d)));
}
/// <summary>
/// Queue all the given <see cref="GCHandle"/> to be disposed later.
/// </summary>
/// <param name="ds">GCHandles.</param>
public void AddRange(IEnumerable<GCHandle> ds) =>
this.objects.AddRange(ds.Select(d => (object)this.CheckAdd(d)));
public void AddRange(IEnumerable<GCHandle> ds)
{
lock (this.objects)
this.objects.AddRange(ds.Select(d => (object)this.CheckAdd(d)));
}
/// <summary>
/// Cancel all pending disposals.
@ -213,9 +244,12 @@ public static class DisposeSafety
/// <remarks>Use this after successful initialization of multiple disposables.</remarks>
public void Cancel()
{
foreach (var o in this.objects)
this.CheckRemove(o);
this.objects.Clear();
lock (this.objects)
{
foreach (var o in this.objects)
this.CheckRemove(o);
this.objects.Clear();
}
}
/// <inheritdoc cref="Stack{T}.EnsureCapacity"/>
@ -264,11 +298,17 @@ public static class DisposeSafety
this.BeforeDispose?.InvokeSafely(this);
List<Exception>? exceptions = null;
while (this.objects.Any())
while (true)
{
var obj = this.objects[^1];
this.objects.RemoveAt(this.objects.Count - 1);
object obj;
lock (this.objects)
{
if (this.objects.Count == 0)
break;
obj = this.objects[^1];
this.objects.RemoveAt(this.objects.Count - 1);
}
try
{
switch (obj)
@ -294,7 +334,8 @@ public static class DisposeSafety
}
}
this.objects.TrimExcess();
lock (this.objects)
this.objects.TrimExcess();
if (exceptions is not null)
{
@ -318,10 +359,16 @@ public static class DisposeSafety
this.BeforeDispose?.InvokeSafely(this);
List<Exception>? exceptions = null;
while (this.objects.Any())
while (true)
{
var obj = this.objects[^1];
this.objects.RemoveAt(this.objects.Count - 1);
object obj;
lock (this.objects)
{
if (this.objects.Count == 0)
break;
obj = this.objects[^1];
this.objects.RemoveAt(this.objects.Count - 1);
}
try
{
@ -351,7 +398,8 @@ public static class DisposeSafety
}
}
this.objects.TrimExcess();
lock (this.objects)
this.objects.TrimExcess();
if (exceptions is not null)
{
@ -386,7 +434,8 @@ public static class DisposeSafety
private void OnItemDisposed(IDisposeCallback obj)
{
obj.BeforeDispose -= this.OnItemDisposed;
this.objects.Remove(obj);
lock (this.objects)
this.objects.Remove(obj);
}
}
}

View file

@ -1,6 +1,7 @@
using System.Linq;
using Dalamud.Game;
using Dalamud.Game.Gui.ContextMenu;
using Dalamud.Plugin.Services;
using Serilog;
@ -99,6 +100,23 @@ internal static class EventHandlerExtensions
}
}
/// <summary>
/// Replacement for Invoke() on OnMenuOpenedDelegate to catch exceptions that stop event propagation in case
/// of a thrown Exception inside of an invocation.
/// </summary>
/// <param name="openedDelegate">The OnMenuOpenedDelegate in question.</param>
/// <param name="argument">Templated argument for Action.</param>
public static void InvokeSafely(this IContextMenu.OnMenuOpenedDelegate? openedDelegate, MenuOpenedArgs argument)
{
if (openedDelegate == null)
return;
foreach (var action in openedDelegate.GetInvocationList().Cast<IContextMenu.OnMenuOpenedDelegate>())
{
HandleInvoke(() => action(argument));
}
}
private static void HandleInvoke(Action act)
{
try

View file

@ -0,0 +1,12 @@
namespace Dalamud.Utility;
/// <summary>
/// An extension of <see cref="IDisposable"/> which makes <see cref="IDisposable.Dispose"/> queue
/// <see cref="RealDispose"/> to be called at a later time.
/// </summary>
internal interface IDeferredDisposable : IDisposable
{
/// <summary>Actually dispose the object.</summary>
/// <remarks>Not to be called from the code that uses the end object.</remarks>
void RealDispose();
}

View file

@ -0,0 +1,234 @@
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.CompilerServices;
namespace Dalamud.Utility
{
/// <summary>
/// A list with limited capacity holding items of type <typeparamref name="T"/>.
/// Adding further items will result in the list rolling over.
/// </summary>
/// <typeparam name="T">Item type.</typeparam>
/// <remarks>
/// Implemented as a circular list using a <see cref="List{T}"/> internally.
/// Insertions and Removals are not supported.
/// Not thread-safe.
/// </remarks>
internal class RollingList<T> : IList<T>
{
private List<T> items;
private int size;
private int firstIndex;
/// <summary>Initializes a new instance of the <see cref="RollingList{T}"/> class.</summary>
/// <param name="size"><see cref="RollingList{T}"/> size.</param>
/// <param name="capacity">Internal <see cref="List{T}"/> initial capacity.</param>
public RollingList(int size, int capacity)
{
ThrowHelper.ThrowArgumentOutOfRangeExceptionIfLessThan(nameof(size), size, 0);
capacity = Math.Min(capacity, size);
this.size = size;
this.items = new List<T>(capacity);
}
/// <summary>Initializes a new instance of the <see cref="RollingList{T}"/> class.</summary>
/// <param name="size"><see cref="RollingList{T}"/> size.</param>
public RollingList(int size)
{
ThrowHelper.ThrowArgumentOutOfRangeExceptionIfLessThan(nameof(size), size, 0);
this.size = size;
this.items = new();
}
/// <summary>Initializes a new instance of the <see cref="RollingList{T}"/> class.</summary>
/// <param name="items">Collection where elements are copied from.</param>
/// <param name="size"><see cref="RollingList{T}"/> size.</param>
public RollingList(IEnumerable<T> items, int size)
{
if (!items.TryGetNonEnumeratedCount(out var capacity)) capacity = 4;
capacity = Math.Min(capacity, size);
this.size = size;
this.items = new List<T>(capacity);
this.AddRange(items);
}
/// <summary>Initializes a new instance of the <see cref="RollingList{T}"/> class.</summary>
/// <param name="items">Collection where elements are copied from.</param>
/// <param name="size"><see cref="RollingList{T}"/> size.</param>
/// <param name="capacity">Internal <see cref="List{T}"/> initial capacity.</param>
public RollingList(IEnumerable<T> items, int size, int capacity)
{
if (items.TryGetNonEnumeratedCount(out var count) && count > capacity) capacity = count;
capacity = Math.Min(capacity, size);
this.size = size;
this.items = new List<T>(capacity);
this.AddRange(items);
}
/// <summary>Gets item count.</summary>
public int Count => this.items.Count;
/// <summary>Gets or sets the internal list capacity.</summary>
public int Capacity
{
get => this.items.Capacity;
set => this.items.Capacity = Math.Min(value, this.size);
}
/// <summary>Gets or sets rolling list size.</summary>
public int Size
{
get => this.size;
set
{
if (value == this.size) return;
if (value > this.size)
{
if (this.firstIndex > 0)
{
this.items = new List<T>(this);
this.firstIndex = 0;
}
}
else // value < this._size
{
ThrowHelper.ThrowArgumentOutOfRangeExceptionIfLessThan(nameof(value), value, 0);
if (value < this.Count)
{
this.items = new List<T>(this.TakeLast(value));
this.firstIndex = 0;
}
}
this.size = value;
}
}
/// <summary>Gets a value indicating whether the item is read only.</summary>
public bool IsReadOnly => false;
/// <summary>Gets or sets an item by index.</summary>
/// <param name="index">Item index.</param>
/// <returns>Item at specified index.</returns>
public T this[int index]
{
get
{
ThrowHelper.ThrowArgumentOutOfRangeExceptionIfGreaterThanOrEqual(nameof(index), index, this.Count);
ThrowHelper.ThrowArgumentOutOfRangeExceptionIfLessThan(nameof(index), index, 0);
return this.items[this.GetRealIndex(index)];
}
set
{
ThrowHelper.ThrowArgumentOutOfRangeExceptionIfGreaterThanOrEqual(nameof(index), index, this.Count);
ThrowHelper.ThrowArgumentOutOfRangeExceptionIfLessThan(nameof(index), index, 0);
this.items[this.GetRealIndex(index)] = value;
}
}
/// <summary>Adds an item to this <see cref="RollingList{T}"/>.</summary>
/// <param name="item">Item to add.</param>
public void Add(T item)
{
if (this.size == 0) return;
if (this.items.Count >= this.size)
{
this.items[this.firstIndex] = item;
this.firstIndex = (this.firstIndex + 1) % this.size;
}
else
{
if (this.items.Count == this.items.Capacity)
{
// Manual list capacity resize
var newCapacity = Math.Max(Math.Min(this.size, this.items.Capacity * 2), this.items.Capacity);
this.items.Capacity = newCapacity;
}
this.items.Add(item);
}
Debug.Assert(this.items.Count <= this.size, "Item count should be less than Size");
}
/// <summary>Add items to this <see cref="RollingList{T}"/>.</summary>
/// <param name="items">Items to add.</param>
public void AddRange(IEnumerable<T> items)
{
if (this.size == 0) return;
foreach (var item in items) this.Add(item);
}
/// <summary>Removes all elements from the <see cref="RollingList{T}"/></summary>
public void Clear()
{
this.items.Clear();
this.firstIndex = 0;
}
/// <summary>Find the index of a specific item.</summary>
/// <param name="item">item to find.</param>
/// <returns>Index where <paramref name="item"/> is found. -1 if not found.</returns>
public int IndexOf(T item)
{
var index = this.items.IndexOf(item);
if (index == -1) return -1;
return this.GetVirtualIndex(index);
}
/// <summary>Not supported.</summary>
[SuppressMessage("Documentation Rules", "SA1611", Justification = "Not supported")]
void IList<T>.Insert(int index, T item) => throw new NotSupportedException();
/// <summary>Not supported.</summary>
[SuppressMessage("Documentation Rules", "SA1611", Justification = "Not supported")]
void IList<T>.RemoveAt(int index) => throw new NotSupportedException();
/// <summary>Find wether an item exists.</summary>
/// <param name="item">item to find.</param>
/// <returns>Wether <paramref name="item"/> is found.</returns>
public bool Contains(T item) => this.items.Contains(item);
/// <summary>Copies the content of this list into an array.</summary>
/// <param name="array">Array to copy into.</param>
/// <param name="arrayIndex"><paramref name="array"/> index to start coping into.</param>
public void CopyTo(T[] array, int arrayIndex)
{
ThrowHelper.ThrowArgumentOutOfRangeExceptionIfLessThan(nameof(arrayIndex), arrayIndex, 0);
if (array.Length - arrayIndex < this.Count) ThrowHelper.ThrowArgumentException("Not enough space");
for (var index = 0; index < this.Count; index++)
{
array[arrayIndex++] = this[index];
}
}
/// <summary>Not supported.</summary>
[SuppressMessage("Documentation Rules", "SA1611", Justification = "Not supported")]
[SuppressMessage("Documentation Rules", "SA1615", Justification = "Not supported")]
bool ICollection<T>.Remove(T item) => throw new NotSupportedException();
/// <summary>Gets an enumerator for this <see cref="RollingList{T}"/>.</summary>
/// <returns><see cref="RollingList{T}"/> enumerator.</returns>
public IEnumerator<T> GetEnumerator()
{
for (var index = 0; index < this.items.Count; index++)
{
yield return this.items[this.GetRealIndex(index)];
}
}
/// <summary>Gets an enumerator for this <see cref="RollingList{T}"/>.</summary>
/// <returns><see cref="RollingList{T}"/> enumerator.</returns>
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private int GetRealIndex(int index) => this.size > 0 ? (index + this.firstIndex) % this.size : 0;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private int GetVirtualIndex(int index) => this.size > 0 ? (this.size + index - this.firstIndex) % this.size : 0;
}
}

View file

@ -0,0 +1,52 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
namespace Dalamud.Utility
{
/// <summary>Helper methods for throwing exceptions.</summary>
internal static class ThrowHelper
{
/// <summary>Throws a <see cref="ArgumentException"/> with a specified <paramref name="message"/>.</summary>
/// <param name="message">Message for the exception.</param>
/// <exception cref="ArgumentException">Thrown by this method.</exception>
[DoesNotReturn]
public static void ThrowArgumentException(string message) => throw new ArgumentException(message);
/// <summary>Throws a <see cref="ArgumentOutOfRangeException"/> with a specified <paramref name="message"/> for a specified <paramref name="paramName"/>.</summary>
/// <param name="paramName">Parameter name.</param>
/// <param name="message">Message for the exception.</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown by this method.</exception>
[DoesNotReturn]
public static void ThrowArgumentOutOfRangeException(string paramName, string message) => throw new ArgumentOutOfRangeException(paramName, message);
/// <summary>Throws a <see cref="ArgumentOutOfRangeException"/> if the specified <paramref name="value"/> is less than <paramref name="comparand"/>.</summary>
/// <typeparam name="T"><see cref="IComparable{T}"/> value type.</typeparam>
/// <param name="paramName">Parameter name.</param>
/// <param name="value">Value to compare from.</param>
/// <param name="comparand">Value to compare with.</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown by this method if <paramref name="value"/> is less than <paramref name="comparand"/>.</exception>
public static void ThrowArgumentOutOfRangeExceptionIfLessThan<T>(string paramName, T value, T comparand) where T : IComparable<T>
{
#if NET8_0_OR_GREATER
ArgumentOutOfRangeException.ThrowIfLessThan(value, comparand);
#else
if (Comparer<T>.Default.Compare(value, comparand) <= -1) ThrowArgumentOutOfRangeException(paramName, $"{paramName} must be greater than or equal {comparand}");
#endif
}
/// <summary>Throws a <see cref="ArgumentOutOfRangeException"/> if the specified <paramref name="value"/> is greater than or equal to <paramref name="comparand"/>.</summary>
/// <typeparam name="T"><see cref="IComparable{T}"/> value type.</typeparam>
/// <param name="paramName">Parameter name.</param>
/// <param name="value">Value to compare from.</param>
/// <param name="comparand">Value to compare with.</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown by this method if <paramref name="value"/> is greater than or equal to<paramref name="comparand"/>.</exception>
public static void ThrowArgumentOutOfRangeExceptionIfGreaterThanOrEqual<T>(string paramName, T value, T comparand) where T : IComparable<T>
{
#if NET8_0_OR_GREATER
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(value, comparand);
#else
if (Comparer<T>.Default.Compare(value, comparand) >= 0) ThrowArgumentOutOfRangeException(paramName, $"{paramName} must be less than {comparand}");
#endif
}
}
}

View file

@ -10,6 +10,7 @@ using System.Reflection.Emit;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using Dalamud.Configuration.Internal;
using Dalamud.Data;
@ -22,6 +23,9 @@ using Dalamud.Logging.Internal;
using ImGuiNET;
using Lumina.Excel.GeneratedSheets;
using Serilog;
using TerraFX.Interop.Windows;
using Windows.Win32.Storage.FileSystem;
namespace Dalamud.Utility;
@ -684,6 +688,55 @@ public static class Util
return names.ElementAt(rng.Next(0, names.Count() - 1)).Singular.RawString;
}
/// <summary>
/// Throws a corresponding exception if <see cref="HRESULT.FAILED"/> is true.
/// </summary>
/// <param name="hr">The result value.</param>
internal static void ThrowOnError(this HRESULT hr)
{
if (hr.FAILED)
Marshal.ThrowExceptionForHR(hr.Value);
}
/// <summary>
/// Calls <see cref="TaskCompletionSource.SetException(System.Exception)"/> if the task is incomplete.
/// </summary>
/// <param name="t">The task.</param>
/// <param name="ex">The exception to set.</param>
internal static void SetExceptionIfIncomplete(this TaskCompletionSource t, Exception ex)
{
if (t.Task.IsCompleted)
return;
try
{
t.SetException(ex);
}
catch
{
// ignore
}
}
/// <summary>
/// Calls <see cref="TaskCompletionSource.SetException(System.Exception)"/> if the task is incomplete.
/// </summary>
/// <typeparam name="T">The type of the result.</typeparam>
/// <param name="t">The task.</param>
/// <param name="ex">The exception to set.</param>
internal static void SetExceptionIfIncomplete<T>(this TaskCompletionSource<T> t, Exception ex)
{
if (t.Task.IsCompleted)
return;
try
{
t.SetException(ex);
}
catch
{
// ignore
}
}
/// <summary>
/// Print formatted GameObject Information to ImGui.
/// </summary>

View file

@ -25,7 +25,7 @@
#include <shellapi.h>
#include <ShlGuid.h>
#include <ShObjIdl.h>
#include <winhttp.h>
#include <shlobj_core.h>
#pragma comment(lib, "comctl32.lib")
#pragma comment(linker, "/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"")
@ -152,7 +152,7 @@ std::wstring describe_module(const std::filesystem::path& path) {
WORD wLanguage;
WORD wCodePage;
};
const auto langs = std::span(reinterpret_cast<const LANGANDCODEPAGE*>(lpBuffer), size / sizeof(LANGANDCODEPAGE));
const auto langs = std::span(static_cast<const LANGANDCODEPAGE*>(lpBuffer), size / sizeof(LANGANDCODEPAGE));
for (const auto& lang : langs) {
if (!VerQueryValueW(block.data(), std::format(L"\\StringFileInfo\\{:04x}{:04x}\\FileDescription", lang.wLanguage, lang.wCodePage).c_str(), &lpBuffer, &size))
continue;
@ -441,6 +441,26 @@ std::wstring escape_shell_arg(const std::wstring& arg) {
return res;
}
void open_folder_and_select_items(HWND hwndOpener, const std::wstring& path) {
const auto piid = ILCreateFromPathW(path.c_str());
if (!piid
|| FAILED(SHOpenFolderAndSelectItems(piid, 0, nullptr, 0))) {
const auto args = std::format(L"/select,{}", escape_shell_arg(path));
SHELLEXECUTEINFOW seiw{
.cbSize = sizeof seiw,
.hwnd = hwndOpener,
.lpFile = L"explorer.exe",
.lpParameters = args.c_str(),
.nShow = SW_SHOW,
};
if (!ShellExecuteExW(&seiw))
throw_last_error("ShellExecuteExW");
}
if (piid)
ILFree(piid);
}
void export_tspack(HWND hWndParent, const std::filesystem::path& logDir, const std::string& crashLog, const std::string& troubleshootingPackData) {
static const char* SourceLogFiles[] = {
"output.log",
@ -457,7 +477,6 @@ void export_tspack(HWND hWndParent, const std::filesystem::path& logDir, const s
}};
std::optional<std::wstring> filePath;
std::fstream fileStream;
try {
IShellItemPtr pItem;
SYSTEMTIME st;
@ -482,7 +501,7 @@ void export_tspack(HWND hWndParent, const std::filesystem::path& logDir, const s
pItem.Release();
filePath.emplace(pFilePath);
fileStream.open(*filePath, std::ios::binary | std::ios::in | std::ios::out | std::ios::trunc);
std::fstream fileStream(*filePath, std::ios::binary | std::ios::in | std::ios::out | std::ios::trunc);
mz_zip_archive zipa{};
zipa.m_pIO_opaque = &fileStream;
@ -518,13 +537,14 @@ void export_tspack(HWND hWndParent, const std::filesystem::path& logDir, const s
mz_throw_if_failed(mz_zip_writer_init_v2(&zipa, 0, 0), "mz_zip_writer_init_v2");
mz_throw_if_failed(mz_zip_writer_add_mem(&zipa, "trouble.json", troubleshootingPackData.data(), troubleshootingPackData.size(), MZ_ZIP_FLAG_WRITE_HEADER_SET_SIZE | MZ_BEST_COMPRESSION), "mz_zip_writer_add_mem: trouble.json");
mz_throw_if_failed(mz_zip_writer_add_mem(&zipa, "crash.log", crashLog.data(), crashLog.size(), MZ_ZIP_FLAG_WRITE_HEADER_SET_SIZE | MZ_BEST_COMPRESSION), "mz_zip_writer_add_mem: crash.log");
std::string logExportLog;
struct HandleAndBaseOffset {
HANDLE h;
int64_t off;
};
const auto fnHandleReader = [](void* pOpaque, mz_uint64 file_ofs, void* pBuf, size_t n) -> size_t {
const auto& info = *reinterpret_cast<const HandleAndBaseOffset*>(pOpaque);
const auto& info = *static_cast<const HandleAndBaseOffset*>(pOpaque);
if (!SetFilePointerEx(info.h, { .QuadPart = static_cast<int64_t>(info.off + file_ofs) }, nullptr, SEEK_SET))
throw_last_error("fnHandleReader: SetFilePointerEx");
if (DWORD read; !ReadFile(info.h, pBuf, static_cast<DWORD>(n), &read, nullptr))
@ -534,8 +554,12 @@ void export_tspack(HWND hWndParent, const std::filesystem::path& logDir, const s
};
for (const auto& pcszLogFileName : SourceLogFiles) {
const auto logFilePath = logDir / pcszLogFileName;
if (!exists(logFilePath))
if (!exists(logFilePath)) {
logExportLog += std::format("File does not exist: {}\n", ws_to_u8(logFilePath.wstring()));
continue;
} else {
logExportLog += std::format("Including: {}\n", ws_to_u8(logFilePath.wstring()));
}
const auto hLogFile = CreateFileW(logFilePath.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, nullptr);
if (hLogFile == INVALID_HANDLE_VALUE)
@ -574,12 +598,12 @@ void export_tspack(HWND hWndParent, const std::filesystem::path& logDir, const s
), std::format("mz_zip_writer_add_read_buf_callback({})", ws_to_u8(logFilePath.wstring())));
}
mz_throw_if_failed(mz_zip_writer_add_mem(&zipa, "logexport.log", logExportLog.data(), logExportLog.size(), MZ_ZIP_FLAG_WRITE_HEADER_SET_SIZE | MZ_BEST_COMPRESSION), "mz_zip_writer_add_mem: logexport.log");
mz_throw_if_failed(mz_zip_writer_finalize_archive(&zipa), "mz_zip_writer_finalize_archive");
mz_throw_if_failed(mz_zip_writer_end(&zipa), "mz_zip_writer_end");
} catch (const std::exception& e) {
MessageBoxW(hWndParent, std::format(L"Failed to save file: {}", u8_to_ws(e.what())).c_str(), get_window_string(hWndParent).c_str(), MB_OK | MB_ICONERROR);
fileStream.close();
if (filePath) {
try {
std::filesystem::remove(*filePath);
@ -590,9 +614,10 @@ void export_tspack(HWND hWndParent, const std::filesystem::path& logDir, const s
return;
}
fileStream.close();
if (filePath) {
ShellExecuteW(hWndParent, nullptr, L"explorer.exe", escape_shell_arg(std::format(L"/select,{}", *filePath)).c_str(), nullptr, SW_SHOW);
// Not sure why, but without the wait, the selected file momentarily disappears and reappears
Sleep(1000);
open_folder_and_select_items(hWndParent, *filePath);
}
}
@ -603,6 +628,7 @@ enum {
IdRadioRestartWithoutDalamud,
IdButtonRestart = 201,
IdButtonSaveTsPack = 202,
IdButtonHelp = IDHELP,
IdButtonExit = IDCANCEL,
};
@ -664,6 +690,9 @@ int main() {
std::filesystem::path assetDir, logDir;
std::optional<std::vector<std::wstring>> launcherArgs;
auto fullDump = false;
// IFileSaveDialog only works on STA
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
std::vector<std::wstring> args;
if (int argc = 0; const auto argv = CommandLineToArgvW(GetCommandLineW(), &argc)) {
@ -710,6 +739,42 @@ int main() {
return InvalidParameter;
}
if (logDir.filename().wstring().ends_with(L".log")) {
std::wcout << L"logDir seems to be pointing to a file; stripping the last path component.\n" << std::endl;
std::wcout << L"Previous: " << logDir.wstring() << std::endl;
logDir = logDir.parent_path();
std::wcout << L"Stripped: " << logDir.wstring() << std::endl;
}
// Only keep the last 3 minidumps
if (!logDir.empty())
{
std::vector<std::pair<std::filesystem::path, std::filesystem::file_time_type>> minidumps;
for (const auto& entry : std::filesystem::directory_iterator(logDir)) {
if (entry.path().filename().wstring().ends_with(L".dmp")) {
minidumps.emplace_back(entry.path(), std::filesystem::last_write_time(entry));
}
}
if (minidumps.size() > 3)
{
std::sort(minidumps.begin(), minidumps.end(), [](const auto& a, const auto& b) { return a.second < b.second; });
for (size_t i = 0; i < minidumps.size() - 3; i++) {
if (std::filesystem::exists(minidumps[i].first))
{
std::wcout << std::format(L"Removing old minidump: {}", minidumps[i].first.wstring()) << std::endl;
std::filesystem::remove(minidumps[i].first);
}
// Also remove corresponding .log, if it exists
if (const auto logPath = minidumps[i].first.replace_extension(L".log"); std::filesystem::exists(logPath)) {
std::wcout << std::format(L"Removing corresponding log: {}", logPath.wstring()) << std::endl;
std::filesystem::remove(logPath);
}
}
}
}
while (true) {
std::cout << "Waiting for crash...\n";
@ -740,6 +805,36 @@ int main() {
std::cout << "Crash triggered" << std::endl;
std::cout << "Creating progress window" << std::endl;
IProgressDialog* pProgressDialog = NULL;
if (SUCCEEDED(CoCreateInstance(CLSID_ProgressDialog, NULL, CLSCTX_ALL, IID_IProgressDialog, (void**)&pProgressDialog)) && pProgressDialog) {
pProgressDialog->SetTitle(L"Dalamud Crash Handler");
pProgressDialog->SetLine(1, L"The game has crashed!", FALSE, NULL);
pProgressDialog->SetLine(2, L"Dalamud is collecting further information...", FALSE, NULL);
pProgressDialog->SetLine(3, L"Refreshing Game Module List", FALSE, NULL);
pProgressDialog->StartProgressDialog(NULL, NULL, PROGDLG_MARQUEEPROGRESS | PROGDLG_NOCANCEL | PROGDLG_NOMINIMIZE, NULL);
IOleWindow* pOleWindow;
HRESULT hr = pProgressDialog->QueryInterface(IID_IOleWindow, (LPVOID*)&pOleWindow);
if (SUCCEEDED(hr))
{
HWND hwndProgressDialog = NULL;
hr = pOleWindow->GetWindow(&hwndProgressDialog);
if (SUCCEEDED(hr))
{
SetWindowPos(hwndProgressDialog, HWND_TOPMOST, 0, 0, 0, 0,
SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW);
SetForegroundWindow(hwndProgressDialog);
}
pOleWindow->Release();
}
}
else {
std::cerr << "Failed to create progress window" << std::endl;
pProgressDialog = NULL;
}
auto shutup_mutex = CreateMutex(NULL, false, L"DALAMUD_CRASHES_NO_MORE");
bool shutup = false;
if (shutup_mutex == NULL && GetLastError() == ERROR_ALREADY_EXISTS)
@ -778,6 +873,9 @@ int main() {
std::wcerr << std::format(L"SymInitialize error: 0x{:x}", GetLastError()) << std::endl;
}
if (pProgressDialog)
pProgressDialog->SetLine(3, L"Reading troubleshooting data", FALSE, NULL);
std::wstring stackTrace(exinfo.dwStackTraceLength, L'\0');
if (exinfo.dwStackTraceLength) {
if (DWORD read; !ReadFile(hPipeRead, &stackTrace[0], 2 * exinfo.dwStackTraceLength, &read, nullptr)) {
@ -792,10 +890,12 @@ int main() {
}
}
if (pProgressDialog)
pProgressDialog->SetLine(3, fullDump ? L"Creating full dump" : L"Creating minidump", FALSE, NULL);
SYSTEMTIME st;
GetLocalTime(&st);
const auto dalamudLogPath = logDir.empty() ? std::filesystem::path() : logDir / L"Dalamud.log";
const auto dalamudBootLogPath = logDir.empty() ? std::filesystem::path() : logDir / L"Dalamud.boot.log";
const auto dumpPath = logDir.empty() ? std::filesystem::path() : logDir / std::format(L"dalamud_appcrash_{:04}{:02}{:02}_{:02}{:02}{:02}_{:03}_{}.dmp", st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, st.wMilliseconds, dwProcessId);
const auto logPath = logDir.empty() ? std::filesystem::path() : logDir / std::format(L"dalamud_appcrash_{:04}{:02}{:02}_{:02}{:02}{:02}_{:03}_{}.log", st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, st.wMilliseconds, dwProcessId);
std::wstring dumpError;
@ -844,70 +944,33 @@ int main() {
log << L"System Time: " << std::chrono::system_clock::now() << std::endl;
log << L"\n" << stackTrace << std::endl;
if (pProgressDialog)
pProgressDialog->SetLine(3, L"Refreshing Module List", FALSE, NULL);
SymRefreshModuleList(GetCurrentProcess());
print_exception_info(exinfo.hThreadHandle, exinfo.ExceptionPointers, exinfo.ContextRecord, log);
const auto window_log_str = log.str();
print_exception_info_extended(exinfo.ExceptionPointers, exinfo.ContextRecord, log);
std::wofstream(logPath) << log.str();
std::thread submitThread;
if (!getenv("DALAMUD_NO_METRIC")) {
auto url = std::format(L"/Dalamud/Metric/ReportCrash?lt={}&code={:x}", exinfo.nLifetime, exinfo.ExceptionRecord.ExceptionCode);
submitThread = std::thread([url = std::move(url)] {
const auto hInternet = WinHttpOpen(L"Dalamud Crash Handler/1.0",
WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
WINHTTP_NO_PROXY_NAME,
WINHTTP_NO_PROXY_BYPASS, 0);
const auto hConnect = !hInternet ? nullptr : WinHttpConnect(hInternet, L"kamori.goats.dev", INTERNET_DEFAULT_HTTP_PORT, 0);
const auto hRequest = !hConnect ? nullptr : WinHttpOpenRequest(hConnect, L"GET", url.c_str(), NULL, WINHTTP_NO_REFERER,
WINHTTP_DEFAULT_ACCEPT_TYPES,
0);
if (hRequest) WinHttpAddRequestHeaders(hRequest, L"Host: kamori.goats.dev", (ULONG)-1L, WINHTTP_ADDREQ_FLAG_ADD);
const auto bSent = !hRequest ? false : WinHttpSendRequest(hRequest, WINHTTP_NO_ADDITIONAL_HEADERS,
0, WINHTTP_NO_REQUEST_DATA, 0,
0, 0);
if (!bSent)
std::cerr << std::format("Failed to send metric: 0x{:x}", GetLastError()) << std::endl;
if (WinHttpReceiveResponse(hRequest, nullptr))
{
DWORD dwStatusCode = 0;
DWORD dwStatusCodeSize = sizeof(DWORD);
WinHttpQueryHeaders(hRequest,
WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER,
WINHTTP_HEADER_NAME_BY_INDEX,
&dwStatusCode, &dwStatusCodeSize, WINHTTP_NO_HEADER_INDEX);
if (dwStatusCode != 200)
std::cerr << std::format("Failed to send metric: {}", dwStatusCode) << std::endl;
}
if (hRequest) WinHttpCloseHandle(hRequest);
if (hConnect) WinHttpCloseHandle(hConnect);
if (hInternet) WinHttpCloseHandle(hInternet);
});
}
TASKDIALOGCONFIG config = { 0 };
const TASKDIALOG_BUTTON radios[]{
{IdRadioRestartNormal, L"Restart"},
{IdRadioRestartWithout3pPlugins, L"Restart without 3rd party plugins"},
{IdRadioRestartNormal, L"Restart normally"},
{IdRadioRestartWithout3pPlugins, L"Restart without custom repository plugins"},
{IdRadioRestartWithoutPlugins, L"Restart without any plugins"},
{IdRadioRestartWithoutDalamud, L"Restart without Dalamud"},
};
const TASKDIALOG_BUTTON buttons[]{
{IdButtonRestart, L"Restart\nRestart the game, optionally without plugins or Dalamud."},
{IdButtonRestart, L"Restart\nRestart the game with the above-selected option."},
{IdButtonSaveTsPack, L"Save Troubleshooting Info\nSave a .tspack file containing information about this crash for analysis."},
{IdButtonExit, L"Exit\nExit the game."},
};
config.cbSize = sizeof(config);
config.hInstance = GetModuleHandleW(nullptr);
config.dwFlags = TDF_ENABLE_HYPERLINKS | TDF_CAN_BE_MINIMIZED | TDF_ALLOW_DIALOG_CANCELLATION | TDF_USE_COMMAND_LINKS;
config.dwFlags = TDF_ENABLE_HYPERLINKS | TDF_CAN_BE_MINIMIZED | TDF_ALLOW_DIALOG_CANCELLATION | TDF_USE_COMMAND_LINKS | TDF_NO_DEFAULT_RADIO_BUTTON;
config.pszMainIcon = MAKEINTRESOURCE(IDI_ICON1);
config.pszMainInstruction = L"An error in the game occurred";
config.pszContent = (L""
@ -915,7 +978,7 @@ int main() {
"\n"
R"aa(Try running a game repair in XIVLauncher by right clicking the login button, and disabling plugins you don't need. Please also check your antivirus, see our <a href="help">help site</a> for more information.)aa" "\n"
"\n"
R"aa(Upload <a href="exporttspack">this file (click here)</a> if you want to ask for help in our <a href="discord">Discord server</a>.)aa" "\n"
R"aa(For further assistance, please upload <a href="exporttspack">a troubleshooting pack</a> to our <a href="discord">Discord server</a>.)aa" "\n"
);
config.pButtons = buttons;
@ -924,10 +987,9 @@ int main() {
config.pszExpandedControlText = L"Hide stack trace";
config.pszCollapsedControlText = L"Stack trace for plugin developers";
config.pszExpandedInformation = window_log_str.c_str();
config.pszWindowTitle = L"Dalamud Error";
config.pszWindowTitle = L"Dalamud Crash Handler";
config.pRadioButtons = radios;
config.cRadioButtons = ARRAYSIZE(radios);
config.nDefaultRadioButton = IdRadioRestartNormal;
config.cxWidth = 300;
#if _DEBUG
@ -949,6 +1011,7 @@ int main() {
case TDN_CREATED:
{
SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW);
SendMessage(hwnd, TDM_ENABLE_BUTTON, IdButtonRestart, 0);
return S_OK;
}
case TDN_HYPERLINK_CLICKED:
@ -957,7 +1020,7 @@ int main() {
if (link == L"help") {
ShellExecuteW(hwnd, nullptr, L"https://goatcorp.github.io/faq?utm_source=vectored", nullptr, nullptr, SW_SHOW);
} else if (link == L"logdir") {
ShellExecuteW(hwnd, nullptr, L"explorer.exe", escape_shell_arg(std::format(L"/select,{}", logPath.wstring())).c_str(), nullptr, SW_SHOW);
open_folder_and_select_items(hwnd, logPath.wstring());
} else if (link == L"logfile") {
ShellExecuteW(hwnd, nullptr, logPath.c_str(), nullptr, nullptr, SW_SHOW);
} else if (link == L"exporttspack") {
@ -970,6 +1033,18 @@ int main() {
}
return S_OK;
}
case TDN_RADIO_BUTTON_CLICKED:
SendMessage(hwnd, TDM_ENABLE_BUTTON, IdButtonRestart, 1);
return S_OK;
case TDN_BUTTON_CLICKED:
const auto button = static_cast<int>(wParam);
if (button == IdButtonSaveTsPack)
{
export_tspack(hwnd, logDir, ws_to_u8(log.str()), troubleshootingPackData);
return S_FALSE; // keep the dialog open
}
return S_OK;
}
return S_OK;
@ -979,10 +1054,11 @@ int main() {
return (*reinterpret_cast<decltype(callback)*>(dwRefData))(hwnd, uNotification, wParam, lParam);
};
config.lpCallbackData = reinterpret_cast<LONG_PTR>(&callback);
if (submitThread.joinable()) {
submitThread.join();
submitThread = {};
if (pProgressDialog) {
pProgressDialog->StopProgressDialog();
pProgressDialog->Release();
pProgressDialog = NULL;
}
if (shutup) {

View file

@ -27,7 +27,7 @@ void ConsoleTeardown()
std::optional<CoreCLR> g_clr;
int InitializeClrAndGetEntryPoint(
HRESULT InitializeClrAndGetEntryPoint(
void* calling_module,
bool enable_etw,
std::wstring runtimeconfig_path,
@ -76,7 +76,7 @@ int InitializeClrAndGetEntryPoint(
if (result != 0)
{
logging::E("Unable to get RoamingAppData path (err={})", result);
return result;
return HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND);
}
std::filesystem::path fs_app_data(_appdata);
@ -92,7 +92,7 @@ int InitializeClrAndGetEntryPoint(
if (!std::filesystem::exists(dotnet_path))
{
logging::E("Error: Unable to find .NET runtime path");
return 1;
return HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND);
}
get_hostfxr_parameters init_parameters
@ -137,12 +137,12 @@ int InitializeClrAndGetEntryPoint(
entrypoint_delegate_type_name.c_str(),
nullptr, entrypoint_fn)) != 0)
{
logging::E("Failed to load module (err={})", result);
logging::E("Failed to load module (err=0x{:X})", static_cast<uint32_t>(result));
return result;
}
logging::I("Done!");
// =========================================================================== //
return 0;
return S_OK;
}

View file

@ -1,7 +1,7 @@
void ConsoleSetup(const std::wstring console_name);
void ConsoleTeardown();
int InitializeClrAndGetEntryPoint(
HRESULT InitializeClrAndGetEntryPoint(
void* calling_module,
bool enable_etw,
std::wstring runtimeconfig_path,

@ -1 +1 @@
Subproject commit e3bd5910678683a718e68f0f940c88b08c24eba5
Subproject commit 722a2c512238ac4b5324e3d343b316d8c8633a02