Merge branch 'net5'

This commit is contained in:
goaaats 2022-06-22 17:38:01 +02:00
commit a78754ce22
No known key found for this signature in database
GPG key ID: 49E2AA8C6A76498B
100 changed files with 67306 additions and 923 deletions

6
.gitmodules vendored
View file

@ -4,3 +4,9 @@
[submodule "lib/FFXIVClientStructs"]
path = lib/FFXIVClientStructs
url = https://github.com/goatcorp/FFXIVClientStructs.git
[submodule "lib/Nomade040-nmd"]
path = lib/Nomade040-nmd
url = https://github.com/Nomade040/nmd.git
[submodule "lib/TsudaKageyu-minhook"]
path = lib/TsudaKageyu-minhook
url = https://github.com/TsudaKageyu/minhook.git

View file

@ -25,15 +25,19 @@
<PropertyGroup Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v142</PlatformToolset>
<PlatformToolset>v143</PlatformToolset>
<LinkIncremental>false</LinkIncremental>
<CharacterSet>Unicode</CharacterSet>
<OutDir>..\bin\$(Configuration)\</OutDir>
<IntDir>obj\$(Configuration)\</IntDir>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<LibraryPath>$(LibraryPath)</LibraryPath>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<LibraryPath>$(SolutionDir)bin\lib\$(Configuration)\libMinHook\;$(LibraryPath)</LibraryPath>
</PropertyGroup>
<ItemDefinitionGroup>
<ClCompile>
<WarningLevel>Level3</WarningLevel>
@ -49,7 +53,7 @@
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<EnableUAC>false</EnableUAC>
<AdditionalDependencies>dbghelp.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalDependencies>dbghelp.lib;Version.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalLibraryDirectories>..\lib\CoreCLR;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
</Link>
</ItemDefinitionGroup>
@ -58,6 +62,8 @@
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>false</IntrinsicFunctions>
<PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Use</PrecompiledHeader>
<DisableSpecificWarnings Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">26812</DisableSpecificWarnings>
</ClCompile>
<Link>
<EnableCOMDATFolding>false</EnableCOMDATFolding>
@ -69,6 +75,8 @@
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Use</PrecompiledHeader>
<DisableSpecificWarnings Condition="'$(Configuration)|$(Platform)'=='Release|x64'">26812</DisableSpecificWarnings>
</ClCompile>
<Link>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
@ -91,22 +99,54 @@
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
</ClCompile>
<ClCompile Include="..\lib\TsudaKageyu-minhook\src\buffer.c">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
</ClCompile>
<ClCompile Include="..\lib\TsudaKageyu-minhook\src\HDE\hde32.c">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|x64'">true</ExcludedFromBuild>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
</ClCompile>
<ClCompile Include="..\lib\TsudaKageyu-minhook\src\HDE\hde64.c">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
</ClCompile>
<ClCompile Include="..\lib\TsudaKageyu-minhook\src\hook.c">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
</ClCompile>
<ClCompile Include="..\lib\TsudaKageyu-minhook\src\trampoline.c">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
</ClCompile>
<ClCompile Include="DalamudStartInfo.cpp" />
<ClCompile Include="hooks.cpp" />
<ClCompile Include="logging.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
</ClCompile>
<ClCompile Include="unicode.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
</ClCompile>
<ClCompile Include="xivfixes.cpp" />
<ClCompile Include="utils.cpp" />
<ClCompile Include="pch_nmd_assembly_impl.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
</ClCompile>
<ClCompile Include="pch.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Create</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Create</PrecompiledHeader>
</ClCompile>
<ClCompile Include="dllmain.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Use</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Use</PrecompiledHeader>
</ClCompile>
<ClCompile Include="dllmain.cpp" />
<ClCompile Include="rewrite_entrypoint.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Use</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Use</PrecompiledHeader>
</ClCompile>
<ClCompile Include="veh.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Use</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Use</PrecompiledHeader>
</ClCompile>
<ClCompile Include="veh.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\lib\CoreCLR\boot.h" />
@ -114,8 +154,22 @@
<ClInclude Include="..\lib\CoreCLR\core\coreclr_delegates.h" />
<ClInclude Include="..\lib\CoreCLR\core\hostfxr.h" />
<ClInclude Include="..\lib\CoreCLR\nethost\nethost.h" />
<ClInclude Include="..\lib\TsudaKageyu-minhook\include\MinHook.h" />
<ClInclude Include="..\lib\TsudaKageyu-minhook\src\buffer.h" />
<ClInclude Include="..\lib\TsudaKageyu-minhook\src\HDE\hde32.h" />
<ClInclude Include="..\lib\TsudaKageyu-minhook\src\HDE\hde64.h" />
<ClInclude Include="..\lib\TsudaKageyu-minhook\src\HDE\pstdint.h" />
<ClInclude Include="..\lib\TsudaKageyu-minhook\src\HDE\table32.h" />
<ClInclude Include="..\lib\TsudaKageyu-minhook\src\HDE\table64.h" />
<ClInclude Include="..\lib\TsudaKageyu-minhook\src\trampoline.h" />
<ClInclude Include="DalamudStartInfo.h" />
<ClInclude Include="hooks.h" />
<ClInclude Include="logging.h" />
<ClInclude Include="unicode.h" />
<ClInclude Include="utils.h" />
<ClInclude Include="pch.h" />
<ClInclude Include="veh.h" />
<ClInclude Include="xivfixes.h" />
</ItemGroup>
<Target Name="RemoveExtraFiles" AfterTargets="PostBuildEvent">
<Delete Files="$(OutDir)$(TargetName).lib" />

View file

@ -8,14 +8,20 @@
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
<Filter Include="Project Files">
<UniqueIdentifier>{0c915688-91ea-431f-8b68-845cad422a50}</UniqueIdentifier>
</Filter>
<Filter Include="Common Boot">
<UniqueIdentifier>{e31f7ca0-db29-4198-8b91-bb11b339705f}</UniqueIdentifier>
</Filter>
<Filter Include="MinHook">
<UniqueIdentifier>{6ec5597d-e293-4d2a-a307-7444c8fac04b}</UniqueIdentifier>
</Filter>
</ItemGroup>
<ItemGroup>
<ClCompile Include="dllmain.cpp">
<Filter>Dalamud.Boot DLL</Filter>
</ClCompile>
<ClCompile Include="pch.cpp">
<Filter>Dalamud.Boot DLL</Filter>
</ClCompile>
<ClCompile Include="veh.cpp">
<Filter>Dalamud.Boot DLL</Filter>
</ClCompile>
@ -28,6 +34,45 @@
<ClCompile Include="..\lib\CoreCLR\boot.cpp">
<Filter>CoreCLR</Filter>
</ClCompile>
<ClCompile Include="pch_nmd_assembly_impl.cpp">
<Filter>Project Files</Filter>
</ClCompile>
<ClCompile Include="pch.cpp">
<Filter>Project Files</Filter>
</ClCompile>
<ClCompile Include="utils.cpp">
<Filter>Dalamud.Boot DLL</Filter>
</ClCompile>
<ClCompile Include="xivfixes.cpp">
<Filter>Dalamud.Boot DLL</Filter>
</ClCompile>
<ClCompile Include="logging.cpp">
<Filter>Common Boot</Filter>
</ClCompile>
<ClCompile Include="unicode.cpp">
<Filter>Common Boot</Filter>
</ClCompile>
<ClCompile Include="..\lib\TsudaKageyu-minhook\src\buffer.c">
<Filter>MinHook</Filter>
</ClCompile>
<ClCompile Include="..\lib\TsudaKageyu-minhook\src\HDE\hde32.c">
<Filter>MinHook</Filter>
</ClCompile>
<ClCompile Include="..\lib\TsudaKageyu-minhook\src\HDE\hde64.c">
<Filter>MinHook</Filter>
</ClCompile>
<ClCompile Include="..\lib\TsudaKageyu-minhook\src\hook.c">
<Filter>MinHook</Filter>
</ClCompile>
<ClCompile Include="..\lib\TsudaKageyu-minhook\src\trampoline.c">
<Filter>MinHook</Filter>
</ClCompile>
<ClCompile Include="hooks.cpp">
<Filter>Dalamud.Boot DLL</Filter>
</ClCompile>
<ClCompile Include="DalamudStartInfo.cpp">
<Filter>Dalamud.Boot DLL</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\lib\CoreCLR\CoreCLR.h">
@ -49,7 +94,49 @@
<Filter>Dalamud.Boot DLL</Filter>
</ClInclude>
<ClInclude Include="pch.h">
<Filter>Project Files</Filter>
</ClInclude>
<ClInclude Include="utils.h">
<Filter>Dalamud.Boot DLL</Filter>
</ClInclude>
<ClInclude Include="hooks.h">
<Filter>Dalamud.Boot DLL</Filter>
</ClInclude>
<ClInclude Include="DalamudStartInfo.h">
<Filter>Dalamud.Boot DLL</Filter>
</ClInclude>
<ClInclude Include="xivfixes.h">
<Filter>Dalamud.Boot DLL</Filter>
</ClInclude>
<ClInclude Include="logging.h">
<Filter>Common Boot</Filter>
</ClInclude>
<ClInclude Include="unicode.h">
<Filter>Common Boot</Filter>
</ClInclude>
<ClInclude Include="..\lib\TsudaKageyu-minhook\src\trampoline.h">
<Filter>MinHook</Filter>
</ClInclude>
<ClInclude Include="..\lib\TsudaKageyu-minhook\src\buffer.h">
<Filter>MinHook</Filter>
</ClInclude>
<ClInclude Include="..\lib\TsudaKageyu-minhook\src\HDE\hde32.h">
<Filter>MinHook</Filter>
</ClInclude>
<ClInclude Include="..\lib\TsudaKageyu-minhook\src\HDE\hde64.h">
<Filter>MinHook</Filter>
</ClInclude>
<ClInclude Include="..\lib\TsudaKageyu-minhook\include\MinHook.h">
<Filter>MinHook</Filter>
</ClInclude>
<ClInclude Include="..\lib\TsudaKageyu-minhook\src\HDE\pstdint.h">
<Filter>MinHook</Filter>
</ClInclude>
<ClInclude Include="..\lib\TsudaKageyu-minhook\src\HDE\table32.h">
<Filter>MinHook</Filter>
</ClInclude>
<ClInclude Include="..\lib\TsudaKageyu-minhook\src\HDE\table64.h">
<Filter>MinHook</Filter>
</ClInclude>
</ItemGroup>
</Project>

View file

@ -0,0 +1,117 @@
#include "pch.h"
#include "DalamudStartInfo.h"
#include "utils.h"
DalamudStartInfo g_startInfo;
void from_json(const nlohmann::json& json, DalamudStartInfo::WaitMessageboxFlags& value) {
if (json.is_number_integer()) {
value = static_cast<DalamudStartInfo::WaitMessageboxFlags>(json.get<int>());
} else if (json.is_array()) {
value = DalamudStartInfo::WaitMessageboxFlags::None;
for (const auto& item : json) {
if (item.is_number_integer()) {
value = static_cast<DalamudStartInfo::WaitMessageboxFlags>(static_cast<int>(value) | item.get<int>());
} else if (item.is_string()) {
const auto iteml = unicode::convert<std::string>(item.get<std::string>(), &unicode::lower);
if (item == "beforeinitialize")
value = static_cast<DalamudStartInfo::WaitMessageboxFlags>(static_cast<int>(value) | static_cast<int>(DalamudStartInfo::WaitMessageboxFlags::BeforeInitialize));
else if (item == "beforedalamudentrypoint")
value = static_cast<DalamudStartInfo::WaitMessageboxFlags>(static_cast<int>(value) | static_cast<int>(DalamudStartInfo::WaitMessageboxFlags::BeforeDalamudEntrypoint));
}
}
} else if (json.is_string()) {
value = DalamudStartInfo::WaitMessageboxFlags::None;
for (const auto& item : utils::split(json.get<std::string>(), ",")) {
const auto iteml = unicode::convert<std::string>(item, &unicode::lower);
if (iteml == "beforeinitialize")
value = static_cast<DalamudStartInfo::WaitMessageboxFlags>(static_cast<int>(value) | static_cast<int>(DalamudStartInfo::WaitMessageboxFlags::BeforeInitialize));
else if (iteml == "beforedalamudentrypoint")
value = static_cast<DalamudStartInfo::WaitMessageboxFlags>(static_cast<int>(value) | static_cast<int>(DalamudStartInfo::WaitMessageboxFlags::BeforeDalamudEntrypoint));
}
}
}
void from_json(const nlohmann::json& json, DalamudStartInfo::DotNetOpenProcessHookMode& value) {
if (json.is_number_integer()) {
value = static_cast<DalamudStartInfo::DotNetOpenProcessHookMode>(json.get<int>());
} else if (json.is_string()) {
const auto langstr = unicode::convert<std::string>(json.get<std::string>(), &unicode::lower);
if (langstr == "importhooks")
value = DalamudStartInfo::DotNetOpenProcessHookMode::ImportHooks;
else if (langstr == "directhook")
value = DalamudStartInfo::DotNetOpenProcessHookMode::DirectHook;
}
}
void from_json(const nlohmann::json& json, DalamudStartInfo::ClientLanguage& value) {
if (json.is_number_integer()) {
value = static_cast<DalamudStartInfo::ClientLanguage>(json.get<int>());
} else if (json.is_string()) {
const auto langstr = unicode::convert<std::string>(json.get<std::string>(), &unicode::lower);
if (langstr == "japanese")
value = DalamudStartInfo::ClientLanguage::Japanese;
else if (langstr == "english")
value = DalamudStartInfo::ClientLanguage::English;
else if (langstr == "german")
value = DalamudStartInfo::ClientLanguage::German;
else if (langstr == "french")
value = DalamudStartInfo::ClientLanguage::French;
}
}
void from_json(const nlohmann::json& json, DalamudStartInfo& config) {
if (!json.is_object())
return;
config.WorkingDirectory = json.value("WorkingDirectory", config.WorkingDirectory);
config.ConfigurationPath = json.value("ConfigurationPath", config.ConfigurationPath);
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.BootLogPath = json.value("BootLogPath", config.BootLogPath);
config.BootShowConsole = json.value("BootShowConsole", config.BootShowConsole);
config.BootDisableFallbackConsole = json.value("BootDisableFallbackConsole", config.BootDisableFallbackConsole);
config.BootWaitMessageBox = json.value("BootWaitMessageBox", config.BootWaitMessageBox);
config.BootWaitDebugger = json.value("BootWaitDebugger", config.BootWaitDebugger);
config.BootVehEnabled = json.value("BootVehEnabled", config.BootVehEnabled);
config.BootVehFull = json.value("BootVehFull", config.BootVehFull);
config.BootEnableEtw = json.value("BootEnableEtw", config.BootEnableEtw);
config.BootDotnetOpenProcessHookMode = json.value("BootDotnetOpenProcessHookMode", config.BootDotnetOpenProcessHookMode);
if (const auto it = json.find("BootEnabledGameFixes"); it != json.end() && it->is_array()) {
config.BootEnabledGameFixes.clear();
for (const auto& val : *it)
config.BootEnabledGameFixes.insert(unicode::convert<std::string>(val.get<std::string>(), &unicode::lower));
}
if (const auto it = json.find("BootUnhookDlls"); it != json.end() && it->is_array()) {
config.BootUnhookDlls.clear();
for (const auto& val : *it)
config.BootUnhookDlls.insert(unicode::convert<std::string>(val.get<std::string>(), &unicode::lower));
}
}
void DalamudStartInfo::from_envvars() {
BootLogPath = utils::get_env<std::string>(L"DALAMUD_BOOT_LOGFILE");
BootShowConsole = utils::get_env<bool>(L"DALAMUD_SHOW_CONSOLE");
BootDisableFallbackConsole = utils::get_env<bool>(L"DALAMUD_DISABLE_FALLBACK_CONSOLE");
BootWaitMessageBox = static_cast<WaitMessageboxFlags>(utils::get_env<int>(L"DALAMUD_WAIT_MESSAGEBOX"));
BootWaitDebugger = utils::get_env<bool>(L"DALAMUD_WAIT_DEBUGGER");
BootVehEnabled = utils::get_env<bool>(L"DALAMUD_IS_VEH");
BootVehFull = utils::get_env<bool>(L"DALAMUD_IS_VEH_FULL");
BootEnableEtw = utils::get_env<bool>(L"DALAMUD_ENABLE_ETW");
BootDotnetOpenProcessHookMode = static_cast<DotNetOpenProcessHookMode>(utils::get_env<int>(L"DALAMUD_DOTNET_OPENPROCESS_HOOKMODE"));
for (const auto& item : utils::get_env_list<std::string>(L"DALAMUD_GAMEFIX_LIST"))
BootEnabledGameFixes.insert(unicode::convert<std::string>(item, &unicode::lower));
for (const auto& item : utils::get_env_list<std::string>(L"DALAMUD_UNHOOK_DLLS"))
BootUnhookDlls.insert(unicode::convert<std::string>(item, &unicode::lower));
}

View file

@ -0,0 +1,50 @@
#pragma once
struct DalamudStartInfo {
enum class WaitMessageboxFlags : int {
None = 0,
BeforeInitialize = 1 << 0,
BeforeDalamudEntrypoint = 1 << 1,
};
friend void from_json(const nlohmann::json&, WaitMessageboxFlags&);
enum class DotNetOpenProcessHookMode : int {
ImportHooks = 0,
DirectHook = 1,
};
friend void from_json(const nlohmann::json&, DotNetOpenProcessHookMode&);
enum class ClientLanguage : int {
Japanese,
English,
German,
French,
};
friend void from_json(const nlohmann::json&, ClientLanguage&);
std::string WorkingDirectory;
std::string ConfigurationPath;
std::string PluginDirectory;
std::string DefaultPluginDirectory;
std::string AssetDirectory;
ClientLanguage Language = ClientLanguage::English;
std::string GameVersion;
int DelayInitializeMs = 0;
std::string BootLogPath;
bool BootShowConsole = false;
bool BootDisableFallbackConsole = false;
WaitMessageboxFlags BootWaitMessageBox = WaitMessageboxFlags::None;
bool BootWaitDebugger = false;
bool BootVehEnabled = false;
bool BootVehFull = false;
bool BootEnableEtw = false;
DotNetOpenProcessHookMode BootDotnetOpenProcessHookMode = DotNetOpenProcessHookMode::ImportHooks;
std::set<std::string> BootEnabledGameFixes{};
std::set<std::string> BootUnhookDlls{};
friend void from_json(const nlohmann::json&, DalamudStartInfo&);
void from_envvars();
};
extern DalamudStartInfo g_startInfo;

View file

@ -1,74 +1,122 @@
#include "pch.h"
#include "DalamudStartInfo.h"
#include "logging.h"
#include "utils.h"
#include "veh.h"
#include "xivfixes.h"
HMODULE g_hModule;
HINSTANCE g_hGameInstance = GetModuleHandleW(nullptr);
bool is_running_on_linux()
{
size_t required_size;
getenv_s(&required_size, nullptr, 0, "XL_WINEONLINUX");
if (required_size > 0)
{
if (char* is_wine_on_linux = static_cast<char*>(malloc(required_size * sizeof(char))))
{
getenv_s(&required_size, is_wine_on_linux, required_size, "XL_WINEONLINUX");
auto result = _stricmp(is_wine_on_linux, "true");
free(is_wine_on_linux);
if (result == 0)
return true;
DllExport DWORD WINAPI Initialize(LPVOID lpParam, HANDLE hMainThreadContinue) {
g_startInfo.from_envvars();
std::string jsonParseError;
try {
from_json(nlohmann::json::parse(std::string_view(static_cast<char*>(lpParam))), g_startInfo);
} catch (const std::exception& e) {
jsonParseError = e.what();
}
if (g_startInfo.BootShowConsole)
ConsoleSetup(L"Dalamud Boot");
logging::update_dll_load_status(true);
const auto logFilePath = unicode::convert<std::wstring>(g_startInfo.BootLogPath);
auto attemptFallbackLog = false;
if (logFilePath.empty()) {
attemptFallbackLog = true;
logging::I("No log file path given; not logging to file.");
} else {
try {
logging::start_file_logging(logFilePath, !g_startInfo.BootShowConsole);
logging::I("Logging to file: {}", logFilePath);
} catch (const std::exception& e) {
attemptFallbackLog = true;
logging::E("Couldn't open log file: {}", logFilePath);
logging::E("Error: {} / {}", errno, e.what());
}
}
HMODULE hntdll = GetModuleHandleW(L"ntdll.dll");
if (!hntdll) // not running on NT
return true;
if (!jsonParseError.empty())
logging::E("Couldn't parse input JSON: {}", jsonParseError);
FARPROC pwine_get_version = GetProcAddress(hntdll, "wine_get_version");
FARPROC pwine_get_host_version = GetProcAddress(hntdll, "wine_get_host_version");
return pwine_get_version != nullptr || pwine_get_host_version != nullptr;
}
bool is_veh_enabled()
{
size_t required_size;
getenv_s(&required_size, nullptr, 0, "DALAMUD_IS_STAGING");
if (required_size > 0)
{
if (char* is_no_veh = static_cast<char*>(malloc(required_size * sizeof(char))))
{
getenv_s(&required_size, is_no_veh, required_size, "DALAMUD_IS_STAGING");
auto result = _stricmp(is_no_veh, "true");
free(is_no_veh);
if (result == 0)
return true;
if (attemptFallbackLog) {
std::wstring logFilePath(PATHCCH_MAX_CCH + 1, L'\0');
logFilePath.resize(GetTempPathW(static_cast<DWORD>(logFilePath.size()), &logFilePath[0]));
if (logFilePath.empty()) {
logFilePath.resize(PATHCCH_MAX_CCH + 1);
logFilePath.resize(GetCurrentDirectoryW(static_cast<DWORD>(logFilePath.size()), &logFilePath[0]));
}
if (!logFilePath.empty() && logFilePath.back() != '/' && logFilePath.back() != '\\')
logFilePath += L"\\";
SYSTEMTIME st;
GetLocalTime(&st);
logFilePath += std::format(L"Dalamud.Boot.{:04}{:02}{:02}.{:02}{:02}{:02}.{:03}.{}.log", st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, st.wMilliseconds, GetCurrentProcessId());
try {
logging::start_file_logging(logFilePath, !g_startInfo.BootShowConsole);
logging::I("Logging to fallback log file: {}", logFilePath);
} catch (const std::exception& e) {
if (!g_startInfo.BootShowConsole && !g_startInfo.BootDisableFallbackConsole)
ConsoleSetup(L"Dalamud Boot - Fallback Console");
logging::E("Couldn't open fallback log file: {}", logFilePath);
logging::E("Error: {} / {}", errno, e.what());
}
}
return false;
}
auto minHookLoaded = false;
if (const auto mhStatus = MH_Initialize(); mhStatus == MH_OK) {
logging::I("MinHook initialized.");
minHookLoaded = true;
} else if (mhStatus == MH_ERROR_ALREADY_INITIALIZED) {
logging::I("MinHook already initialized.");
minHookLoaded = true;
} else {
logging::E("Failed to initialize MinHook (status={}({}))", MH_StatusToString(mhStatus), static_cast<int>(mhStatus));
}
logging::I("Dalamud.Boot Injectable, (c) 2021 XIVLauncher Contributors");
logging::I("Built at: " __DATE__ "@" __TIME__);
DllExport DWORD WINAPI Initialize(LPVOID lpParam)
{
#ifndef NDEBUG
ConsoleSetup(L"Dalamud Boot");
#endif
if (static_cast<int>(g_startInfo.BootWaitMessageBox) & static_cast<int>(DalamudStartInfo::WaitMessageboxFlags::BeforeInitialize))
MessageBoxW(nullptr, L"Press OK to continue (BeforeInitialize)", L"Dalamud Boot", MB_OK);
printf("Dalamud.Boot Injectable, (c) 2021 XIVLauncher Contributors\nBuilt at: %s@%s\n\n", __DATE__, __TIME__);
if (minHookLoaded) {
logging::I("Applying fixes...");
xivfixes::apply_all(true);
logging::I("Fixes OK");
} else {
logging::W("Skipping fixes, as MinHook has failed to load.");
}
wchar_t _module_path[MAX_PATH];
GetModuleFileNameW(g_hModule, _module_path, sizeof _module_path / 2);
std::filesystem::path fs_module_path(_module_path);
if (g_startInfo.BootWaitDebugger) {
logging::I("Waiting for debugger to attach...");
while (!IsDebuggerPresent())
Sleep(100);
logging::I("Debugger attached.");
}
std::wstring runtimeconfig_path = _wcsdup(fs_module_path.replace_filename(L"Dalamud.runtimeconfig.json").c_str());
std::wstring module_path = _wcsdup(fs_module_path.replace_filename(L"Dalamud.dll").c_str());
const auto fs_module_path = utils::get_module_path(g_hModule);
const auto runtimeconfig_path = std::filesystem::path(fs_module_path).replace_filename(L"Dalamud.runtimeconfig.json").wstring();
const auto module_path = std::filesystem::path(fs_module_path).replace_filename(L"Dalamud.dll").wstring();
// ============================== CLR ========================================= //
logging::I("Calling InitializeClrAndGetEntryPoint");
void* entrypoint_vfn;
int result = InitializeClrAndGetEntryPoint(
g_hModule,
g_startInfo.BootEnableEtw,
runtimeconfig_path,
module_path,
L"Dalamud.EntryPoint, Dalamud",
@ -79,39 +127,39 @@ DllExport DWORD WINAPI Initialize(LPVOID lpParam)
if (result != 0)
return result;
typedef void (CORECLR_DELEGATE_CALLTYPE* custom_component_entry_point_fn)(LPVOID);
custom_component_entry_point_fn entrypoint_fn = reinterpret_cast<custom_component_entry_point_fn>(entrypoint_vfn);
using custom_component_entry_point_fn = void (CORECLR_DELEGATE_CALLTYPE*)(LPVOID, HANDLE);
const auto entrypoint_fn = reinterpret_cast<custom_component_entry_point_fn>(entrypoint_vfn);
// ============================== VEH ======================================== //
printf("Initializing VEH... ");
if(is_running_on_linux())
{
printf("VEH was disabled, running on linux\n");
}
else if (is_veh_enabled())
{
if (veh::add_handler())
printf("Done!\n");
else printf("Failed!\n");
}
else
{
printf("VEH was disabled manually\n");
logging::I("Initializing VEH...");
if (utils::is_running_on_linux()) {
logging::I("=> VEH was disabled, running on linux");
} else if (g_startInfo.BootVehEnabled) {
if (veh::add_handler(g_startInfo.BootVehFull))
logging::I("=> Done!");
else
logging::I("=> Failed!");
} else {
logging::I("VEH was disabled manually");
}
// ============================== Dalamud ==================================== //
printf("Initializing Dalamud... ");
entrypoint_fn(lpParam);
printf("Done!\n");
if (static_cast<int>(g_startInfo.BootWaitMessageBox) & static_cast<int>(DalamudStartInfo::WaitMessageboxFlags::BeforeDalamudEntrypoint))
MessageBoxW(nullptr, L"Press OK to continue (BeforeDalamudEntrypoint)", L"Dalamud Boot", MB_OK);
#ifndef NDEBUG
fclose(stdin);
fclose(stdout);
fclose(stderr);
FreeConsole();
#endif
if (hMainThreadContinue) {
// Let the game initialize.
SetEvent(hMainThreadContinue);
}
// We don't need to do this anymore, Dalamud now loads without needing the window to be there. Speed!
// utils::wait_for_game_window();
logging::I("Initializing Dalamud...");
entrypoint_fn(lpParam, hMainThreadContinue);
logging::I("Done!");
return 0;
}
@ -119,13 +167,28 @@ DllExport DWORD WINAPI Initialize(LPVOID lpParam)
BOOL APIENTRY DllMain(const HMODULE hModule, const DWORD dwReason, LPVOID lpReserved) {
DisableThreadLibraryCalls(hModule);
switch (dwReason)
{
switch (dwReason) {
case DLL_PROCESS_ATTACH:
g_hModule = hModule;
break;
case DLL_PROCESS_DETACH:
// process is terminating; don't bother cleaning up
if (lpReserved)
return TRUE;
logging::update_dll_load_status(false);
xivfixes::apply_all(false);
MH_DisableHook(MH_ALL_HOOKS);
if (const auto mhStatus = MH_Uninitialize(); MH_OK != mhStatus && MH_ERROR_NOT_INITIALIZED != mhStatus) {
logging::E("Failed to uninitialize MinHook (status={})", static_cast<int>(mhStatus));
__fastfail(logging::MinHookUnload);
}
veh::remove_handler();
//logging::log_file.close();
break;
}
return TRUE;

149
Dalamud.Boot/hooks.cpp Normal file
View file

@ -0,0 +1,149 @@
#include "pch.h"
#include "hooks.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)",
[this](HMODULE hModule, LPCSTR lpProcName) { return get_proc_address_handler(hModule, lpProcName); }) {
}
hooks::getprocaddress_singleton_import_hook::~getprocaddress_singleton_import_hook() {
LdrUnregisterDllNotification(m_ldrDllNotificationCookie);
}
std::shared_ptr<void> hooks::getprocaddress_singleton_import_hook::set_handler(std::wstring dllName, std::string functionName, void* pfnDetour, std::function<void(void*)> fnOnOriginalAddressAvailable) {
const auto hModule = GetModuleHandleW(dllName.c_str());
if (!hModule)
throw std::out_of_range("Specified DLL is not found.");
const auto pfn = m_pfnGetProcAddress(hModule, functionName.c_str());
if (!pfn)
throw std::out_of_range("Could not find the specified function.");
fnOnOriginalAddressAvailable(pfn);
auto& target = m_targetFns[hModule][functionName];
if (target)
throw std::runtime_error("Specified function has already been hooked.");
target = pfnDetour;
m_dllNameMap[hModule] = unicode::convert<std::string>(dllName);
for (const auto& mod : utils::loaded_module::all_modules())
hook_module(mod);
return { pfn,[pThis = this->shared_from_this(), hModule, functionName](void*) {
auto& modFns = pThis->m_targetFns[hModule];
auto& hooks = pThis->m_hooks[hModule];
modFns.erase(functionName);
hooks.erase(functionName);
if (modFns.empty()) {
pThis->m_targetFns.erase(hModule);
pThis->m_hooks.erase(hModule);
pThis->m_dllNameMap.erase(hModule);
}
} };
}
std::shared_ptr<hooks::getprocaddress_singleton_import_hook> hooks::getprocaddress_singleton_import_hook::get_instance() {
static std::weak_ptr<hooks::getprocaddress_singleton_import_hook> s_instance;
std::shared_ptr<hooks::getprocaddress_singleton_import_hook> res;
res = s_instance.lock();
if (res)
return res;
static std::mutex m_mtx;
const auto lock = std::lock_guard(m_mtx);
res = s_instance.lock();
if (res)
return res;
s_instance = res = std::make_shared<getprocaddress_singleton_import_hook>();
res->initialize();
return res;
}
void hooks::getprocaddress_singleton_import_hook::initialize() {
m_getProcAddressHandler = set_handler(L"kernel32.dll", "GetProcAddress", m_thunk.get_thunk(), [this](void*) {});
LdrRegisterDllNotification(0, [](ULONG notiReason, const LDR_DLL_NOTIFICATION_DATA* pData, void* context) {
if (notiReason == LDR_DLL_NOTIFICATION_REASON_LOADED) {
const auto dllName = unicode::convert<std::string>(pData->Loaded.FullDllName->Buffer);
logging::I(R"({} "{}" has been loaded at 0x{:X} ~ 0x{:X} (0x{:X}); finding import table items to hook.)",
LogTag, dllName,
reinterpret_cast<size_t>(pData->Loaded.DllBase),
reinterpret_cast<size_t>(pData->Loaded.DllBase) + pData->Loaded.SizeOfImage,
pData->Loaded.SizeOfImage);
reinterpret_cast<getprocaddress_singleton_import_hook*>(context)->hook_module(utils::loaded_module(pData->Loaded.DllBase));
} else if (notiReason == LDR_DLL_NOTIFICATION_REASON_UNLOADED) {
const auto dllName = unicode::convert<std::string>(pData->Unloaded.FullDllName->Buffer);
logging::I(R"({} "{}" has been unloaded.)", LogTag, dllName);
}
}, this, &m_ldrDllNotificationCookie);
}
FARPROC hooks::getprocaddress_singleton_import_hook::get_proc_address_handler(HMODULE hModule, LPCSTR lpProcName) {
if (const auto it1 = m_targetFns.find(hModule); it1 != m_targetFns.end()) {
if (const auto it2 = it1->second.find(lpProcName); it2 != it1->second.end()) {
logging::I(R"({} Redirecting GetProcAddress("{}", "{}"))", LogTag, m_dllNameMap[hModule], lpProcName);
return reinterpret_cast<FARPROC>(it2->second);
}
}
return this->m_pfnGetProcAddress(hModule, lpProcName);
}
void hooks::getprocaddress_singleton_import_hook::hook_module(const utils::loaded_module& mod) {
if (mod.is_current_process())
return;
const auto path = unicode::convert<std::string>(mod.path().wstring());
for (const auto& [hModule, targetFns] : m_targetFns) {
for (const auto& [targetFn, pfnThunk] : targetFns) {
const auto& dllName = m_dllNameMap[hModule];
if (void* pGetProcAddressImport; mod.find_imported_function_pointer(dllName.c_str(), targetFn.c_str(), 0, pGetProcAddressImport)) {
auto& hook = m_hooks[hModule][targetFn][mod];
if (!hook) {
logging::I("{} Hooking {}!{} imported by {}", LogTag, dllName, targetFn, unicode::convert<std::string>(mod.path().wstring()));
hook.emplace(std::format("getprocaddress_singleton_import_hook::hook_module({}!{})", dllName, targetFn), static_cast<void**>(pGetProcAddressImport), pfnThunk);
}
}
}
}
}

252
Dalamud.Boot/hooks.h Normal file
View file

@ -0,0 +1,252 @@
#pragma once
#include <limits>
#include <map>
#include "utils.h"
namespace hooks {
class base_untyped_hook {
std::string m_name;
public:
base_untyped_hook(std::string name) : m_name(name) {}
virtual ~base_untyped_hook() = default;
virtual bool check_consistencies() const {
return true;
}
virtual void assert_dominance() const {
}
const std::string& name() const {
return m_name;
}
};
template<typename>
class base_hook;
template<typename TReturn, typename ... TArgs>
class base_hook<TReturn(TArgs...)> : public base_untyped_hook {
using TFn = TReturn(TArgs...);
private:
TFn* const m_pfnOriginal;
utils::thunk<TReturn(TArgs...)> m_thunk;
public:
base_hook(std::string name, TFn* pfnOriginal)
: base_untyped_hook(name)
, m_pfnOriginal(pfnOriginal)
, m_thunk(std::move(name), m_pfnOriginal) {
}
virtual void set_detour(std::function<TFn> fn) {
if (!fn)
m_thunk.set_target(m_pfnOriginal);
else
m_thunk.set_target(std::move(fn));
}
virtual TReturn call_original(TArgs... args) {
return m_pfnOriginal(std::forward<TArgs>(args)...);
}
protected:
TFn* get_original() const {
return m_pfnOriginal;
}
TFn* get_thunk() const {
return m_thunk.get_thunk();
}
};
template<typename TFn>
class import_hook : public base_hook<TFn> {
using Base = base_hook<TFn>;
TFn** const m_ppfnImportTableItem;
public:
import_hook(std::string name, TFn** ppfnImportTableItem)
: Base(std::move(name), *ppfnImportTableItem)
, m_ppfnImportTableItem(ppfnImportTableItem) {
const utils::memory_tenderizer tenderizer(ppfnImportTableItem, sizeof * ppfnImportTableItem, PAGE_READWRITE);
*ppfnImportTableItem = Base::get_thunk();
}
import_hook(std::string name, const char* pcszDllName, const char* pcszFunctionName, int hintOrOrdinal)
: import_hook(std::move(name), utils::loaded_module::current_process().get_imported_function_pointer<TFn>(pcszDllName, pcszFunctionName, hintOrOrdinal)) {
}
~import_hook() override {
const utils::memory_tenderizer tenderizer(m_ppfnImportTableItem, sizeof * m_ppfnImportTableItem, PAGE_READWRITE);
*m_ppfnImportTableItem = Base::get_original();
}
bool check_consistencies() const override {
return *m_ppfnImportTableItem == Base::get_thunk();
}
void assert_dominance() const override {
if (check_consistencies())
return;
const utils::memory_tenderizer tenderizer(m_ppfnImportTableItem, sizeof * m_ppfnImportTableItem, PAGE_READWRITE);
*m_ppfnImportTableItem = Base::get_thunk();
}
};
template<typename>
class direct_hook;
template<typename TReturn, typename ... TArgs>
class direct_hook<TReturn(TArgs...)> : public base_hook<TReturn(TArgs...)> {
using TFn = TReturn(TArgs...);
using Base = base_hook<TFn>;
TFn* m_pfnMinHookBridge;
public:
direct_hook(std::string name, TFn* pfnFunction)
: Base(std::move(name), pfnFunction) {
if (const auto mhStatus = MH_CreateHook(pfnFunction, Base::get_thunk(), reinterpret_cast<void**>(&m_pfnMinHookBridge)); mhStatus != MH_OK)
throw std::runtime_error(std::format("MH_CreateHook(0x{:X}, ...) failure: {}", reinterpret_cast<size_t>(pfnFunction), static_cast<int>(mhStatus)));
MH_EnableHook(Base::get_original());
}
~direct_hook() override {
MH_DisableHook(Base::get_original());
}
TReturn call_original(TArgs... args) override {
return m_pfnMinHookBridge(std::forward<TArgs>(args)...);
}
};
class wndproc_hook : public base_hook<std::remove_pointer_t<WNDPROC>> {
using Base = base_hook<std::remove_pointer_t<WNDPROC>>;
const HWND m_hwnd;
public:
wndproc_hook(std::string name, HWND hwnd)
: Base(std::move(name), reinterpret_cast<WNDPROC>(GetWindowLongPtrW(hwnd, GWLP_WNDPROC)))
, m_hwnd(hwnd) {
SetWindowLongPtrW(hwnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(Base::get_thunk()));
}
~wndproc_hook() override {
SetWindowLongPtrW(m_hwnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(Base::get_original()));
}
LRESULT call_original(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) override {
return CallWindowProcW(Base::get_original(), hwnd, msg, wParam, lParam);
}
bool check_consistencies() const override {
return GetWindowLongPtrW(m_hwnd, GWLP_WNDPROC) == reinterpret_cast<LONG_PTR>(Base::get_thunk());
}
void assert_dominance() const override {
if (check_consistencies())
return;
SetWindowLongPtrW(m_hwnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(Base::get_thunk()));
}
};
class untyped_import_hook : public base_untyped_hook {
void** const m_ppfnImportTableItem;
void* const m_pfnOriginalImport;
public:
untyped_import_hook(std::string name, void** ppfnImportTableItem, void* pThunk)
: base_untyped_hook(std::move(name))
, m_pfnOriginalImport(*ppfnImportTableItem)
, m_ppfnImportTableItem(ppfnImportTableItem) {
const utils::memory_tenderizer tenderizer(ppfnImportTableItem, sizeof * ppfnImportTableItem, PAGE_READWRITE);
*ppfnImportTableItem = pThunk;
}
~untyped_import_hook() override {
MEMORY_BASIC_INFORMATION mbi{};
VirtualQuery(m_ppfnImportTableItem, &mbi, sizeof mbi);
if (mbi.State != MEM_COMMIT)
return;
const utils::memory_tenderizer tenderizer(m_ppfnImportTableItem, sizeof * m_ppfnImportTableItem, PAGE_READWRITE);
*m_ppfnImportTableItem = m_pfnOriginalImport;
}
};
class getprocaddress_singleton_import_hook : public std::enable_shared_from_this<getprocaddress_singleton_import_hook> {
static inline const char* LogTag = "[global_import_hook]";
decltype(GetProcAddress)* const m_pfnGetProcAddress;
utils::thunk<decltype(GetProcAddress)> m_thunk;
std::shared_ptr<void> m_getProcAddressHandler;
void* m_ldrDllNotificationCookie{};
std::map<HMODULE, std::string> m_dllNameMap;
std::map<HMODULE, std::map<std::string, void*>> m_targetFns;
std::map<HMODULE, std::map<std::string, std::map<HMODULE, std::optional<untyped_import_hook>>>> m_hooks;
public:
getprocaddress_singleton_import_hook();
~getprocaddress_singleton_import_hook();
std::shared_ptr<void> set_handler(std::wstring dllName, std::string functionName, void* pfnDetour, std::function<void(void*)> fnOnOriginalAddressAvailable);
static std::shared_ptr<getprocaddress_singleton_import_hook> get_instance();
private:
void initialize();
FARPROC get_proc_address_handler(HMODULE hModule, LPCSTR lpProcName);
void hook_module(const utils::loaded_module& mod);
};
template<typename>
class global_import_hook;
template<typename TReturn, typename ... TArgs>
class global_import_hook<TReturn(TArgs...)> : public base_untyped_hook {
using TFn = TReturn(TArgs...);
utils::thunk<TFn> m_thunk;
std::shared_ptr<void> m_singleImportHook;
public:
global_import_hook(std::string name, std::wstring dllName, std::string functionName)
: base_untyped_hook(name)
, m_thunk(std::move(name), nullptr) {
m_singleImportHook = getprocaddress_singleton_import_hook::get_instance()->set_handler(
dllName,
functionName,
m_thunk.get_thunk(),
[this](void* p) { m_thunk.set_target(reinterpret_cast<TFn*>(p)); }
);
}
virtual void set_detour(std::function<TFn> fn) {
if (!fn)
m_thunk.set_target(reinterpret_cast<TFn*>(m_singleImportHook.get()));
else
m_thunk.set_target(std::move(fn));
}
virtual TReturn call_original(TArgs... args) {
return reinterpret_cast<TFn*>(m_singleImportHook.get())(std::forward<TArgs>(args)...);
}
};
}

91
Dalamud.Boot/logging.cpp Normal file
View file

@ -0,0 +1,91 @@
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include <fstream>
#include <memory>
#include "logging.h"
static bool s_bLoaded = false;
static bool s_bSkipLogFileWrite = false;
static std::shared_ptr<void> s_hLogFile;
void logging::start_file_logging(const std::filesystem::path& path, bool redirect_stderrout) {
if (s_hLogFile)
return;
try {
if (exists(path) && file_size(path) > 1048576) {
auto oldPath = std::filesystem::path(path);
oldPath.replace_extension(".log.old");
if (exists(oldPath))
remove(oldPath);
rename(path, oldPath);
}
} catch (...) {
// whatever
}
const auto h = CreateFile(path.wstring().c_str(),
GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
nullptr, OPEN_ALWAYS, 0, nullptr);
if (h == INVALID_HANDLE_VALUE)
throw std::runtime_error(std::format("Win32 error {}(0x{:x})", GetLastError(), GetLastError()));
SetFilePointer(h, 0, 0, FILE_END);
s_hLogFile = {h, &CloseHandle};
if (redirect_stderrout) {
SetStdHandle(STD_ERROR_HANDLE, h);
SetStdHandle(STD_OUTPUT_HANDLE, h);
s_bSkipLogFileWrite = true;
}
}
void logging::update_dll_load_status(bool loaded) {
s_bLoaded = loaded;
}
template<>
void logging::print<char>(Level level, const char* s) {
SYSTEMTIME st;
GetLocalTime(&st);
std::string estr;
switch (level) {
case Level::Verbose:
estr = std::format("[{:02}:{:02}:{:02} CPP/VRB] {}\n", st.wHour, st.wMinute, st.wSecond, s);
break;
case Level::Debug:
estr = std::format("[{:02}:{:02}:{:02} CPP/DBG] {}\n", st.wHour, st.wMinute, st.wSecond, s);
break;
case Level::Info:
estr = std::format("[{:02}:{:02}:{:02} CPP/INF] {}\n", st.wHour, st.wMinute, st.wSecond, s);
break;
case Level::Warning:
estr = std::format("[{:02}:{:02}:{:02} CPP/WRN] {}\n", st.wHour, st.wMinute, st.wSecond, s);
break;
case Level::Error:
estr = std::format("[{:02}:{:02}:{:02} CPP/ERR] {}\n", st.wHour, st.wMinute, st.wSecond, s);
break;
case Level::Fatal:
estr = std::format("[{:02}:{:02}:{:02} CPP/FTL] {}\n", st.wHour, st.wMinute, st.wSecond, s);
break;
default:
estr = std::format("[{:02}:{:02}:{:02} CPP/???] {}\n", st.wHour, st.wMinute, st.wSecond, s);
break;
}
OutputDebugStringW(unicode::convert<std::wstring>(estr).c_str());
// Handle accesses should not be done during DllMain process attach/detach calls
if (s_bLoaded) {
DWORD wr{};
WriteFile(GetStdHandle(STD_ERROR_HANDLE), &estr[0], static_cast<DWORD>(estr.size()), &wr, nullptr);
if (s_hLogFile && !s_bSkipLogFileWrite) {
WriteFile(s_hLogFile.get(), &estr[0], static_cast<DWORD>(estr.size()), &wr, nullptr);
}
}
}

125
Dalamud.Boot/logging.h Normal file
View file

@ -0,0 +1,125 @@
#pragma once
#include <filesystem>
#include <format>
#include <string>
#include "unicode.h"
namespace logging {
enum class Level : int {
Verbose = 0,
Debug = 1,
Info = 2,
Warning = 3,
Error = 4,
Fatal = 5,
};
enum FastFailErrorCode : int {
Unspecified = 12345,
MinHookUnload,
};
/**
* @brief Starts writing log to specified file.
*/
void start_file_logging(const std::filesystem::path& path, bool redirect_stderrout = false);
/**
* @brief Marks this DLL either as loaded or unloaded, top prevent accessing handles when the DLL is not loaded.
*/
void update_dll_load_status(bool loaded);
/**
* @brief Prints log, unformatted.
* @param level Log level.
* @param s Log to print, as a C-string.
*/
template<typename TElem>
void print(Level level, const TElem* s) { print(level, unicode::convert<std::string>(s).c_str()); }
/**
* @brief Prints log, unformatted.
* @param level Log level.
* @param s Log to print, as a basic_string.
*/
template<typename TElem, typename TTraits = std::char_traits<TElem>, typename TAlloc = std::allocator<TElem>>
void print(Level level, const std::basic_string<TElem, TTraits, TAlloc>& s) { print(level, s.c_str()); }
/**
* @brief Prints log, unformatted.
* @param level Log level.
* @param s Log to print, as a basic_string_view.
*/
template<typename TElem, typename TTraits = std::char_traits<TElem>>
void print(Level level, const std::basic_string_view<TElem, TTraits>& s) { print(level, unicode::convert<std::string>(s).c_str()); }
template<>
void print<char>(Level level, const char* s);
template<typename>
struct is_basic_string : std::false_type {};
template<typename TElem, typename TTraits, typename TAlloc>
struct is_basic_string<std::basic_string<TElem, TTraits, TAlloc>> : std::true_type {};
template<typename T>
inline constexpr auto is_basic_string_v = is_basic_string<T>::value;
template<typename>
struct is_basic_string_view : std::false_type {};
template<typename TElem, typename TTraits, typename TAlloc>
struct is_basic_string_view<std::basic_string<TElem, TTraits, TAlloc>> : std::true_type {};
template<typename T>
inline constexpr auto is_basic_string_view_v = is_basic_string_view<T>::value;
template<typename T>
auto to_format_arg(T&& x) {
using Td = std::remove_cvref_t<T>;
if constexpr (std::is_pointer_v<Td>) {
using Tdd = std::remove_cvref_t<std::remove_pointer_t<Td>>;
if constexpr (std::is_same_v<Tdd, wchar_t> || std::is_same_v<Tdd, char8_t> || std::is_same_v<Tdd, char16_t> || std::is_same_v<Tdd, char32_t>)
return unicode::convert<std::string>(x);
else
return std::forward<T>(x);
} else {
if constexpr (is_basic_string_v<Td> || is_basic_string_view_v<Td>) {
using Tdd = Td::value_type;
if constexpr (std::is_same_v<Tdd, wchar_t> || std::is_same_v<Tdd, char8_t> || std::is_same_v<Tdd, char16_t> || std::is_same_v<Tdd, char32_t>)
return unicode::convert<std::string>(x);
else
return std::forward<T>(x);
} else if constexpr (std::is_same_v<Td, std::filesystem::path>) {
auto u8s = x.u8string();
return std::move(*reinterpret_cast<std::string*>(&u8s));
} else {
return std::forward<T>(x);
}
}
}
/**
* @brief Prints log, formatted.
* @param level Log level.
* @param fmt C-string.
* @param arg1 First format parameter.
* @param args Second and further format parameters, if any.
*/
template<typename Arg, typename...Args>
void print(Level level, const char* fmt, Arg&& arg1, Args&&...args) {
print(level, std::vformat(fmt, std::make_format_args(to_format_arg(std::forward<Arg>(arg1)), to_format_arg(std::forward<Args>(args))...)));
}
template<typename...Args> void V(Args&&...args) { print(Level::Verbose, std::forward<Args>(args)...); }
template<typename...Args> void D(Args&&...args) { print(Level::Debug, std::forward<Args>(args)...); }
template<typename...Args> void I(Args&&...args) { print(Level::Info, std::forward<Args>(args)...); }
template<typename...Args> void W(Args&&...args) { print(Level::Warning, std::forward<Args>(args)...); }
template<typename...Args> void E(Args&&...args) { print(Level::Error, std::forward<Args>(args)...); }
template<typename...Args> void F(Args&&...args) { print(Level::Fatal, std::forward<Args>(args)...); }
}

View file

@ -8,7 +8,8 @@
#define PCH_H
// Exclude rarely-used stuff from Windows headers
#define WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#define NOMINMAX
// Windows Header Files
#include <windows.h>
@ -17,25 +18,52 @@
#include <Psapi.h>
#include <Shlobj.h>
#include <TlHelp32.h>
#include <Dbt.h>
#include <SubAuth.h>
// MSVC Compiler Intrinsic
#include <intrin.h>
// C++ Standard Libraries
#include <cassert>
#include <cstdio>
#include <deque>
#include <filesystem>
#include <format>
#include <fstream>
#include <functional>
#include <iostream>
#include <ranges>
#include <set>
#include <span>
#include <string>
#include <mutex>
#include <type_traits>
// https://www.akenotsuki.com/misc/srell/en/
#include "../lib/srell3_009/single-header/srell.hpp"
// https://github.com/TsudaKageyu/minhook
#include "../lib/TsudaKageyu-minhook/include/MinHook.h"
// https://github.com/Nomade040/nmd
#include "../lib/Nomade040-nmd/nmd_assembly.h"
// https://github.com/dotnet/coreclr
#include "..\lib\CoreCLR\CoreCLR.h"
#include "..\lib\CoreCLR\boot.h"
#include "../lib/CoreCLR/CoreCLR.h"
#include "../lib/CoreCLR/boot.h"
// https://github.com/nlohmann/json
#include "../lib/nlohmann-json/json.hpp"
#include "unicode.h"
// Commonly used macros
#define DllExport extern "C" __declspec(dllexport)
// Global variables
extern HMODULE g_hModule;
extern HINSTANCE g_hGameInstance;
extern std::optional<CoreCLR> g_clr;
#endif //PCH_H

View file

@ -0,0 +1,2 @@
#define NMD_ASSEMBLY_IMPLEMENTATION
#include "../lib/Nomade040-nmd/nmd_assembly.h"

View file

@ -1,6 +1,8 @@
#include "pch.h"
DllExport DWORD WINAPI Initialize(LPVOID lpParam);
#include "logging.h"
DllExport DWORD WINAPI Initialize(LPVOID lpParam, HANDLE hMainThreadContinue);
struct RewrittenEntryPointParameters {
void* pAllocation;
@ -231,28 +233,14 @@ void* get_mapped_image_base_address(HANDLE hProcess, const std::filesystem::path
return mbi.AllocationBase;
} catch (const std::filesystem::filesystem_error& e) {
printf("%s", e.what());
} catch (const std::exception& e) {
logging::W("Failed to check memory block 0x{:X}(len=0x{:X}): {}", mbi.BaseAddress, mbi.RegionSize, e.what());
continue;
}
}
throw std::runtime_error("corresponding base address not found");
}
/// @brief Find the game main window.
/// @return Handle to the game main window, or nullptr if it doesn't exist (yet).
HWND try_find_game_window() {
HWND hwnd = nullptr;
while ((hwnd = FindWindowExW(nullptr, hwnd, L"FFXIVGAME", nullptr))) {
DWORD pid;
GetWindowThreadProcessId(hwnd, &pid);
if (pid == GetCurrentProcessId() && IsWindowVisible(hwnd))
break;
}
return hwnd;
}
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);
@ -358,15 +346,6 @@ DllExport DWORD WINAPI RewriteRemoteEntryPoint(HANDLE hProcess, const wchar_t* p
return RewriteRemoteEntryPointW(hProcess, pcwzPath, to_utf16(pcszLoadInfo).c_str());
}
void wait_for_game_window() {
HWND game_window;
while (!(game_window = try_find_game_window())) {
WaitForInputIdle(GetCurrentProcess(), INFINITE);
Sleep(100);
};
SendMessageW(game_window, WM_NULL, 0, 0);
}
/// @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) {
@ -379,23 +358,17 @@ DllExport void WINAPI RewrittenEntryPoint(RewrittenEntryPointParameters& params)
params.hMainThread = CreateThread(nullptr, 0, [](void* p) -> DWORD {
try {
std::string loadInfo;
auto& params = *reinterpret_cast<RewrittenEntryPointParameters*>(p);
{
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);
// Make a copy of load info, as the whole params will be freed after this code block.
loadInfo = params.pLoadInfo;
// Let the game initialize.
SetEvent(params.hMainThreadContinue);
}
wait_for_game_window();
Initialize(&loadInfo[0]);
Initialize(&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);

245
Dalamud.Boot/unicode.cpp Normal file
View file

@ -0,0 +1,245 @@
#include "unicode.h"
size_t unicode::decode(EncodingTag<char8_t>, char32_t& out, const char8_t* in, size_t nRemainingBytes, bool strict) {
if (nRemainingBytes == 0) {
out = 0;
return 0;
}
if (0 == (*in & 0x80)) {
out = *in;
return 1;
}
if (0xC0 == (*in & 0xE0)) {
if (nRemainingBytes < 2) goto invalid;
if (0x80 != (in[1] & 0xC0)) goto invalid;
out = (
((static_cast<char32_t>(in[0]) & 0x1F) << 6) |
((static_cast<char32_t>(in[1]) & 0x3F) << 0));
return 2;
}
if (0xE0 == (*in & 0xF0)) {
if (nRemainingBytes < 3) goto invalid;
if (0x80 != (in[1] & 0xC0)) goto invalid;
if (0x80 != (in[2] & 0xC0)) goto invalid;
out = static_cast<char32_t>(
((static_cast<char32_t>(in[0]) & 0x0F) << 12) |
((static_cast<char32_t>(in[1]) & 0x3F) << 6) |
((static_cast<char32_t>(in[2]) & 0x3F) << 0));
return 3;
}
if (0xF0 == (*in & 0xF8)) {
if (nRemainingBytes < 4) goto invalid;
if (0x80 != (in[1] & 0xC0)) goto invalid;
if (0x80 != (in[2] & 0xC0)) goto invalid;
if (0x80 != (in[3] & 0xC0)) goto invalid;
out = (
((static_cast<char32_t>(in[0]) & 0x07) << 18) |
((static_cast<char32_t>(in[1]) & 0x3F) << 12) |
((static_cast<char32_t>(in[2]) & 0x3F) << 6) |
((static_cast<char32_t>(in[3]) & 0x3F) << 0));
return 4;
}
if (!strict) {
if (0xF8 == (*in & 0xFC)) {
if (nRemainingBytes < 5) goto invalid;
if (0x80 != (in[1] & 0xC0)) goto invalid;
if (0x80 != (in[2] & 0xC0)) goto invalid;
if (0x80 != (in[3] & 0xC0)) goto invalid;
if (0x80 != (in[4] & 0xC0)) goto invalid;
out = (
((static_cast<char32_t>(in[0]) & 0x07) << 24) |
((static_cast<char32_t>(in[1]) & 0x3F) << 18) |
((static_cast<char32_t>(in[2]) & 0x3F) << 12) |
((static_cast<char32_t>(in[3]) & 0x3F) << 6) |
((static_cast<char32_t>(in[4]) & 0x3F) << 0));
return 4;
}
if (0xFC == (*in & 0xFE)) {
if (nRemainingBytes < 6) goto invalid;
if (0x80 != (in[1] & 0xC0)) goto invalid;
if (0x80 != (in[2] & 0xC0)) goto invalid;
if (0x80 != (in[3] & 0xC0)) goto invalid;
if (0x80 != (in[4] & 0xC0)) goto invalid;
if (0x80 != (in[5] & 0xC0)) goto invalid;
out = (
((static_cast<char32_t>(in[0]) & 0x07) << 30) |
((static_cast<char32_t>(in[1]) & 0x3F) << 24) |
((static_cast<char32_t>(in[2]) & 0x3F) << 18) |
((static_cast<char32_t>(in[3]) & 0x3F) << 12) |
((static_cast<char32_t>(in[4]) & 0x3F) << 6) |
((static_cast<char32_t>(in[5]) & 0x3F) << 0));
return 5;
}
}
invalid:
out = UReplacement;
return 1;
}
size_t unicode::decode(EncodingTag<char16_t>, char32_t& out, const char16_t* in, size_t nRemainingBytes, bool strict) {
if (nRemainingBytes == 0) {
out = 0;
return 0;
}
if ((*in & 0xFC00) == 0xD800) {
if (nRemainingBytes < 2 || (in[1] & 0xFC00) != 0xDC00)
goto invalid;
out = 0x10000 + (
((static_cast<char32_t>(in[0]) & 0x03FF) << 10) |
((static_cast<char32_t>(in[1]) & 0x03FF) << 0)
);
return 2;
}
if (0xD800 <= *in && *in <= 0xDFFF && strict)
out = UReplacement;
else
out = *in;
return 1;
invalid:
out = UReplacement;
return 1;
}
size_t unicode::decode(EncodingTag<char32_t>, char32_t& out, const char32_t* in, size_t nRemainingBytes, bool strict) {
if (nRemainingBytes == 0) {
out = 0;
return 0;
}
out = *in;
return 1;
}
size_t unicode::decode(EncodingTag<char>, char32_t& out, const char* in, size_t nRemainingBytes, bool strict) {
return decode(EncodingTag<char8_t>(), out, reinterpret_cast<const char8_t*>(in), nRemainingBytes, strict);
}
size_t unicode::decode(EncodingTag<wchar_t>, char32_t& out, const wchar_t* in, size_t nRemainingBytes, bool strict) {
return decode(EncodingTag<char16_t>(), out, reinterpret_cast<const char16_t*>(in), nRemainingBytes, strict);
}
size_t unicode::encode(EncodingTag<char8_t>, char8_t* ptr, char32_t c, bool strict) {
if (c < (1 << 7)) {
if (ptr)
*(ptr++) = static_cast<char8_t>(c);
return 1;
}
if (c < (1 << (5 + 6))) {
if (ptr) {
*(ptr++) = 0xC0 | static_cast<char8_t>(c >> 6);
*(ptr++) = 0x80 | static_cast<char8_t>((c >> 0) & 0x3F);
}
return 2;
}
if (c < (1 << (4 + 6 + 6))) {
if (ptr) {
*(ptr++) = 0xE0 | static_cast<char8_t>(c >> 12);
*(ptr++) = 0x80 | static_cast<char8_t>((c >> 6) & 0x3F);
*(ptr++) = 0x80 | static_cast<char8_t>((c >> 0) & 0x3F);
}
return 3;
}
if (c < (1 << (3 + 6 + 6 + 6))) {
if (ptr) {
*(ptr++) = 0xF0 | static_cast<char8_t>(c >> 18);
*(ptr++) = 0x80 | static_cast<char8_t>((c >> 12) & 0x3F);
*(ptr++) = 0x80 | static_cast<char8_t>((c >> 6) & 0x3F);
*(ptr++) = 0x80 | static_cast<char8_t>((c >> 0) & 0x3F);
}
return 4;
}
if (strict) {
if (ptr) { // Replacement character U+FFFD
*(ptr++) = 0xEF;
*(ptr++) = 0xBF;
*(ptr++) = 0xBD;
}
return 3;
}
if (c < (1 << (3 + 6 + 6 + 6 + 6))) {
if (ptr) {
*(ptr++) = 0xF8 | static_cast<char8_t>(c >> 24);
*(ptr++) = 0x80 | static_cast<char8_t>((c >> 18) & 0x3F);
*(ptr++) = 0x80 | static_cast<char8_t>((c >> 12) & 0x3F);
*(ptr++) = 0x80 | static_cast<char8_t>((c >> 6) & 0x3F);
*(ptr++) = 0x80 | static_cast<char8_t>((c >> 0) & 0x3F);
}
return 5;
}
if (ptr) {
*(ptr++) = 0xFC | static_cast<char8_t>(c >> 30);
*(ptr++) = 0x80 | static_cast<char8_t>((c >> 24) & 0x3F);
*(ptr++) = 0x80 | static_cast<char8_t>((c >> 18) & 0x3F);
*(ptr++) = 0x80 | static_cast<char8_t>((c >> 12) & 0x3F);
*(ptr++) = 0x80 | static_cast<char8_t>((c >> 6) & 0x3F);
*(ptr++) = 0x80 | static_cast<char8_t>((c >> 0) & 0x3F);
}
return 6;
}
size_t unicode::encode(EncodingTag<char16_t>, char16_t* ptr, char32_t c, bool strict) {
if (c < 0x10000) {
if (ptr) {
if (0xD800 <= c && c <= 0xDFFF && strict)
*(ptr++) = 0xFFFD;
else
*(ptr++) = static_cast<char16_t>(c);
}
return 1;
}
c -= 0x10000;
if (c < (1 << 20)) {
if (ptr) {
*(ptr++) = 0xD800 | static_cast<char16_t>((c >> 10) & 0x3FF);
*(ptr++) = 0xDC00 | static_cast<char16_t>((c >> 0) & 0x3FF);
}
return 2;
}
if (ptr)
*(ptr++) = 0xFFFD;
return 1;
}
size_t unicode::encode(EncodingTag<char32_t>, char32_t* ptr, char32_t c, bool strict) {
if (ptr)
*ptr = c;
return 1;
}
size_t unicode::encode(EncodingTag<char>, char* ptr, char32_t c, bool strict) {
return encode(EncodingTag<char8_t>(), reinterpret_cast<char8_t*>(ptr), c, strict);
}
size_t unicode::encode(EncodingTag<wchar_t>, wchar_t* ptr, char32_t c, bool strict) {
return encode(EncodingTag<char16_t>(), reinterpret_cast<char16_t*>(ptr), c, strict);
}
char32_t unicode::lower(char32_t in) {
if ('A' <= in && in <= 'Z')
return in - 'A' + 'a';
return in;
}
char32_t unicode::upper(char32_t in) {
if ('a' <= in && in <= 'z')
return in - 'a' + 'A';
return in;
}

108
Dalamud.Boot/unicode.h Normal file
View file

@ -0,0 +1,108 @@
#pragma once
#include <array>
#include <cstdint>
#include <string>
#include <type_traits>
namespace unicode {
constexpr char32_t UReplacement = U'\uFFFD';
constexpr char32_t UInvalid = U'\uFFFF';
template<typename T> struct EncodingTag {};
size_t decode(EncodingTag<char8_t>, char32_t& out, const char8_t* in, size_t nRemainingBytes, bool strict);
size_t decode(EncodingTag<char16_t>, char32_t& out, const char16_t* in, size_t nRemainingBytes, bool strict);
size_t decode(EncodingTag<char32_t>, char32_t& out, const char32_t* in, size_t nRemainingBytes, bool strict);
size_t decode(EncodingTag<char>, char32_t& out, const char* in, size_t nRemainingBytes, bool strict);
size_t decode(EncodingTag<wchar_t>, char32_t& out, const wchar_t* in, size_t nRemainingBytes, bool strict);
template<typename T>
inline size_t decode(char32_t& out, const T* in, size_t nRemainingBytes, bool strict = true) {
return decode(EncodingTag<T>(), out, in, nRemainingBytes, strict);
}
size_t encode(EncodingTag<char8_t>, char8_t* ptr, char32_t c, bool strict);
size_t encode(EncodingTag<char16_t>, char16_t* ptr, char32_t c, bool strict);
size_t encode(EncodingTag<char32_t>, char32_t* ptr, char32_t c, bool strict);
size_t encode(EncodingTag<char>, char* ptr, char32_t c, bool strict);
size_t encode(EncodingTag<wchar_t>, wchar_t* ptr, char32_t c, bool strict);
template<typename T>
inline size_t encode(T* ptr, char32_t c, bool strict = true) {
return encode(EncodingTag<T>(), ptr, c, strict);
}
char32_t lower(char32_t in);
char32_t upper(char32_t in);
template<class TTo, class TFromElem, class TFromTraits = std::char_traits<TFromElem>>
TTo& convert(TTo& out, const std::basic_string_view<TFromElem, TFromTraits>& in, char32_t(*pfnCharMap)(char32_t) = nullptr, bool strict = false) {
out.reserve(out.size() + in.size() * 4 / sizeof(in[0]) / sizeof(out[0]));
char32_t c{};
for (size_t decLen = 0, decIdx = 0; decIdx < in.size() && ((decLen = decode(c, &in[decIdx], in.size() - decIdx, strict))); decIdx += decLen) {
if (pfnCharMap)
c = pfnCharMap(c);
const auto encIdx = out.size();
const auto encLen = encode<typename TTo::value_type>(nullptr, c, strict);
out.resize(encIdx + encLen);
encode(&out[encIdx], c, strict);
}
return out;
}
template<class TTo, class TFromElem, class TFromTraits = std::char_traits<TFromElem>, class TFromAlloc = std::allocator<TFromElem>>
TTo& convert(TTo& out, const std::basic_string<TFromElem, TFromTraits, TFromAlloc>& in, char32_t(*pfnCharMap)(char32_t) = nullptr, bool strict = false) {
return convert(out, std::basic_string_view<TFromElem, TFromTraits>(in), pfnCharMap, strict);
}
template<class TTo, class TFromElem, typename = std::enable_if_t<std::is_integral_v<TFromElem>>>
TTo& convert(TTo& out, const TFromElem* in, size_t length = (std::numeric_limits<size_t>::max)(), char32_t(*pfnCharMap)(char32_t) = nullptr, bool strict = false) {
if (length == (std::numeric_limits<size_t>::max)())
length = std::char_traits<TFromElem>::length(in);
return convert(out, std::basic_string_view<TFromElem>(in, length), pfnCharMap, strict);
}
template<class TTo, class TFromElem, class TFromTraits = std::char_traits<TFromElem>>
TTo convert(const std::basic_string_view<TFromElem, TFromTraits>& in, char32_t(*pfnCharMap)(char32_t) = nullptr, bool strict = false) {
TTo out{};
return convert(out, in, pfnCharMap, strict);
}
template<class TTo, class TFromElem, class TFromTraits = std::char_traits<TFromElem>, class TFromAlloc = std::allocator<TFromElem>>
TTo convert(const std::basic_string<TFromElem, TFromTraits, TFromAlloc>& in, char32_t(*pfnCharMap)(char32_t) = nullptr, bool strict = false) {
TTo out{};
return convert(out, std::basic_string_view<TFromElem, TFromTraits>(in), pfnCharMap, strict);
}
template<class TTo, class TFromElem, typename = std::enable_if_t<std::is_integral_v<TFromElem>>>
TTo convert(const TFromElem* in, size_t length = (std::numeric_limits<size_t>::max)(), char32_t(*pfnCharMap)(char32_t) = nullptr, bool strict = false) {
if (length == (std::numeric_limits<size_t>::max)())
length = std::char_traits<TFromElem>::length(in);
TTo out{};
return convert(out, std::basic_string_view<TFromElem>(in, length), pfnCharMap, strict);
}
inline const std::u8string& convert(const std::u8string& in) { return in; }
inline const std::u16string& convert(const std::u16string& in) { return in; }
inline const std::u32string& convert(const std::u32string& in) { return in; }
inline const std::string& convert(const std::string& in) { return in; }
inline const std::wstring& convert(const std::wstring& in) { return in; }
}

522
Dalamud.Boot/utils.cpp Normal file
View file

@ -0,0 +1,522 @@
#include "pch.h"
#include "utils.h"
std::filesystem::path utils::loaded_module::path() const {
std::wstring buf(MAX_PATH, L'\0');
for (;;) {
if (const auto len = GetModuleFileNameExW(GetCurrentProcess(), m_hModule, &buf[0], static_cast<DWORD>(buf.size())); len != buf.size()) {
if (buf.empty())
throw std::runtime_error(std::format("Failed to resolve module path: Win32 error {}", GetLastError()));
buf.resize(len);
return buf;
}
if (buf.size() * 2 < PATHCCH_MAX_CCH)
buf.resize(buf.size() * 2);
else if (auto p = std::filesystem::path(buf); exists(p))
return p;
else
throw std::runtime_error("Failed to resolve module path: no amount of buffer size would fit the data");
}
}
bool utils::loaded_module::owns_address(const void* pAddress) const {
const auto pcAddress = reinterpret_cast<const char*>(pAddress);
const auto pcModule = reinterpret_cast<const char*>(m_hModule);
return pcModule <= pcAddress && pcAddress <= pcModule + (is_pe64() ? nt_header64().OptionalHeader.SizeOfImage : nt_header32().OptionalHeader.SizeOfImage);
}
std::span<IMAGE_SECTION_HEADER> utils::loaded_module::section_headers() const {
const auto& dosHeader = ref_as<IMAGE_DOS_HEADER>(0);
const auto& ntHeader32 = ref_as<IMAGE_NT_HEADERS32>(dosHeader.e_lfanew);
// Since this does not refer to OptionalHeader32/64 else than its offset, we can use either.
return { IMAGE_FIRST_SECTION(&ntHeader32), ntHeader32.FileHeader.NumberOfSections };
}
IMAGE_SECTION_HEADER& utils::loaded_module::section_header(const char* pcszSectionName) const {
for (auto& section : section_headers()) {
if (strncmp(reinterpret_cast<const char*>(section.Name), pcszSectionName, IMAGE_SIZEOF_SHORT_NAME) == 0)
return section;
}
throw std::out_of_range(std::format("Section [{}] not found", pcszSectionName));
}
std::span<char> utils::loaded_module::section(size_t index) const {
auto& sectionHeader = section_headers()[index];
return { address(sectionHeader.VirtualAddress), sectionHeader.Misc.VirtualSize };
}
std::span<char> utils::loaded_module::section(const char* pcszSectionName) const {
auto& sectionHeader = section_header(pcszSectionName);
return { address(sectionHeader.VirtualAddress), sectionHeader.Misc.VirtualSize };
}
template<typename TEntryType>
static bool find_imported_function_pointer_helper(const char* pcBaseAddress, const IMAGE_IMPORT_DESCRIPTOR& desc, const IMAGE_DATA_DIRECTORY& dir, std::string_view reqFunc, uint32_t hintOrOrdinal, void*& ppFunctionAddress) {
const auto importLookupsOversizedSpan = std::span(reinterpret_cast<const TEntryType*>(&pcBaseAddress[desc.OriginalFirstThunk]), (dir.Size - desc.OriginalFirstThunk) / sizeof(TEntryType));
const auto importAddressesOversizedSpan = std::span(reinterpret_cast<const TEntryType*>(&pcBaseAddress[desc.FirstThunk]), (dir.Size - desc.FirstThunk) / sizeof(TEntryType));
for (size_t i = 0, i_ = (std::min)(importLookupsOversizedSpan.size(), importAddressesOversizedSpan.size()); i < i_ && importLookupsOversizedSpan[i] && importAddressesOversizedSpan[i]; i++) {
const auto& importLookup = importLookupsOversizedSpan[i];
const auto& importAddress = importAddressesOversizedSpan[i];
const auto& importByName = *reinterpret_cast<const IMAGE_IMPORT_BY_NAME*>(&pcBaseAddress[importLookup]);
// Is this entry importing by ordinals? A lot of socket functions are the case.
if (IMAGE_SNAP_BY_ORDINAL32(importLookup)) {
// Is this the entry?
if (!hintOrOrdinal || IMAGE_ORDINAL32(importLookup) != hintOrOrdinal)
continue;
// Is this entry not importing by ordinals, and are we using hint exclusively to find the entry?
} else if (reqFunc.empty()) {
// Is this the entry?
if (importByName.Hint != hintOrOrdinal)
continue;
} else {
// Name must be contained in this directory.
auto currFunc = std::string_view(importByName.Name, (std::min<size_t>)(&pcBaseAddress[dir.Size] - importByName.Name, reqFunc.size()));
currFunc = currFunc.substr(0, strnlen(currFunc.data(), currFunc.size()));
// Is this the entry? (Case sensitive)
if (reqFunc != currFunc)
continue;
}
// Found the entry; return the address of the pointer to the target function.
ppFunctionAddress = const_cast<void*>(reinterpret_cast<const void*>(&importAddress));
return true;
}
return false;
}
bool utils::loaded_module::find_imported_function_pointer(const char* pcszDllName, const char* pcszFunctionName, uint32_t hintOrOrdinal, void*& ppFunctionAddress) const {
const auto requestedDllName = std::string_view(pcszDllName, strlen(pcszDllName));
const auto requestedFunctionName = pcszFunctionName ? std::string_view(pcszFunctionName, strlen(pcszFunctionName)) : std::string_view();
const auto& directory = data_directory(IMAGE_DIRECTORY_ENTRY_IMPORT);
ppFunctionAddress = nullptr;
// This span might be too long in terms of meaningful data; it only serves to prevent accessing memory outsides boundaries.
for (const auto& importDescriptor : span_as<IMAGE_IMPORT_DESCRIPTOR>(directory.VirtualAddress, directory.Size / sizeof IMAGE_IMPORT_DESCRIPTOR)) {
// Having all zero values signals the end of the table. We didn't find anything.
if (!importDescriptor.OriginalFirstThunk && !importDescriptor.TimeDateStamp && !importDescriptor.ForwarderChain && !importDescriptor.FirstThunk)
return false;
// Skip invalid entries, just in case.
if (!importDescriptor.Name || !importDescriptor.OriginalFirstThunk)
continue;
// Name must be contained in this directory.
if (importDescriptor.Name < directory.VirtualAddress)
continue;
auto currentDllName = std::string_view(address_as<char>(importDescriptor.Name), (std::min<size_t>)(directory.Size - importDescriptor.Name, requestedDllName.size()));
currentDllName = currentDllName.substr(0, strnlen(currentDllName.data(), currentDllName.size()));
// Is this entry about the DLL that we're looking for? (Case insensitive)
if (requestedDllName.size() != currentDllName.size() || _strcmpi(requestedDllName.data(), currentDllName.data()))
continue;
if (is_pe64()) {
if (find_imported_function_pointer_helper<uint64_t>(address(), importDescriptor, directory, requestedFunctionName, hintOrOrdinal, ppFunctionAddress))
return true;
} else {
if (find_imported_function_pointer_helper<uint32_t>(address(), importDescriptor, directory, requestedFunctionName, hintOrOrdinal, ppFunctionAddress))
return true;
}
}
// Found nothing.
return false;
}
void* utils::loaded_module::get_imported_function_pointer(const char* pcszDllName, const char* pcszFunctionName, uint32_t hintOrOrdinal) const {
if (void* ppImportTableItem{}; find_imported_function_pointer(pcszDllName, pcszFunctionName, hintOrOrdinal, ppImportTableItem))
return ppImportTableItem;
throw std::runtime_error("Failed to find import for kernel32!OpenProcess.");
}
utils::loaded_module utils::loaded_module::current_process() {
return { GetModuleHandleW(nullptr) };
}
std::vector<utils::loaded_module> utils::loaded_module::all_modules() {
std::vector<HMODULE> hModules(128);
for (DWORD dwNeeded{}; EnumProcessModules(GetCurrentProcess(), &hModules[0], static_cast<DWORD>(std::span(hModules).size_bytes()), &dwNeeded) && hModules.size() < dwNeeded;)
hModules.resize(hModules.size() + 128);
std::vector<loaded_module> modules;
modules.reserve(hModules.size());
for (const auto hModule : hModules) {
if (!hModule)
break;
modules.emplace_back(hModule);
}
return modules;
}
utils::signature_finder& utils::signature_finder::look_in(const void* pFirst, size_t length) {
if (length)
m_ranges.emplace_back(std::span(reinterpret_cast<const char*>(pFirst), length));
return *this;
}
utils::signature_finder& utils::signature_finder::look_in(const loaded_module& m, const char* sectionName) {
return look_in(m.section(sectionName));
}
utils::signature_finder& utils::signature_finder::look_for(std::string_view pattern, std::string_view mask, char cExactMatch, char cWildcard) {
if (pattern.size() != mask.size())
throw std::runtime_error("Length of pattern does not match the length of mask.");
std::string buf;
buf.reserve(pattern.size() * 4);
for (size_t i = 0; i < pattern.size(); i++) {
const auto c = pattern[i];
if (mask[i] == cWildcard) {
buf.push_back('.');
} else if (mask[i] == cExactMatch) {
buf.push_back('\\');
buf.push_back('x');
buf.push_back((c >> 4) < 10 ? (c >> 4) - 10 : 'A' + (c >> 4) - 10);
buf.push_back((c & 15) < 10 ? (c & 15) - 10 : 'A' + (c & 15) - 10);
}
}
m_patterns.emplace_back(buf);
return *this;
}
utils::signature_finder& utils::signature_finder::look_for(std::string_view pattern, char wildcardMask) {
std::string buf;
buf.reserve(pattern.size() * 4);
for (const auto& c : pattern) {
if (c == wildcardMask) {
buf.push_back('.');
} else {
buf.push_back('\\');
buf.push_back('x');
buf.push_back((c >> 4) < 10 ? '0' + (c >> 4) : 'A' + (c >> 4) - 10);
buf.push_back((c & 15) < 10 ? '0' + (c & 15) : 'A' + (c & 15) - 10);
}
}
m_patterns.emplace_back(buf);
return *this;
}
utils::signature_finder& utils::signature_finder::look_for(std::string_view pattern) {
std::string buf;
buf.reserve(pattern.size() * 4);
for (const auto& c : pattern) {
buf.push_back('\\');
buf.push_back('x');
buf.push_back((c >> 4) < 10 ? '0' + (c >> 4) : 'A' + (c >> 4) - 10);
buf.push_back((c & 15) < 10 ? '0' + (c & 15) : 'A' + (c & 15) - 10);
}
m_patterns.emplace_back(buf);
return *this;
}
utils::signature_finder& utils::signature_finder::look_for_hex(std::string_view pattern) {
std::string buf;
buf.reserve(pattern.size());
bool bHighByte = true;
for (size_t i = 0; i < pattern.size(); i++) {
int n = -1;
if ('0' <= pattern[i] && pattern[i] <= '9')
n = pattern[i] - '0';
else if ('a' <= pattern[i] && pattern[i] <= 'f')
n = 10 + pattern[i] - 'A';
else if ('A' <= pattern[i] && pattern[i] <= 'F')
n = 10 + pattern[i] - 'A';
else if (pattern[i] == '?' && i + 1 < pattern.size() && pattern[i + 1] == '?') {
i++;
n = -2;
} else if (pattern[i] == '?')
n = -2;
if (n == -1)
continue;
else if (n == -2) {
if (!bHighByte) {
buf.insert(buf.begin() + buf.size() - 1, '0');
bHighByte = true;
}
buf.push_back('.');
continue;
}
if (bHighByte) {
buf.push_back('\\');
buf.push_back('x');
}
buf.push_back(pattern[i]);
bHighByte = !bHighByte;
}
m_patterns.emplace_back(buf);
return *this;
}
std::vector<utils::signature_finder::result> utils::signature_finder::find(size_t minCount, size_t maxCount, bool bErrorOnMoreThanMaximum) const {
std::vector<result> res;
for (const auto& rangeSpan : m_ranges) {
for (size_t patternIndex = 0; patternIndex < m_patterns.size(); patternIndex++) {
srell::match_results<std::span<const char>::iterator> matches;
auto ptr = rangeSpan.begin();
for (size_t matchIndex = 0;; ptr = matches[0].first + 1, matchIndex++) {
if (!m_patterns[patternIndex].search(ptr, rangeSpan.end(), rangeSpan.begin(), matches, srell::regex_constants::match_flag_type::match_default))
break;
for (size_t captureIndex = 0; captureIndex < matches.size(); captureIndex++) {
const auto& capture = matches[captureIndex];
res.emplace_back(
std::span(capture.first, capture.second),
patternIndex,
matchIndex,
captureIndex);
if (bErrorOnMoreThanMaximum) {
if (res.size() > maxCount)
throw std::runtime_error(std::format("Found {} result(s), wanted at most {} results", res.size(), maxCount));
} else if (res.size() == maxCount)
return res;
}
}
}
}
if (res.size() < minCount)
throw std::runtime_error(std::format("Found {} result(s), wanted at least {} results", res.size(), minCount));
return res;
}
std::span<const char> utils::signature_finder::find_one() const {
return find(1, 1, false).front().Match;
}
utils::memory_tenderizer::memory_tenderizer(const void* pAddress, size_t length, DWORD dwNewProtect) : m_data(reinterpret_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) {
MEMORY_BASIC_INFORMATION region{};
if (!VirtualQuery(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),
sizeof region,
GetLastError()));
}
if (!VirtualProtect(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),
region.RegionSize,
GetLastError()));
}
m_regions.emplace_back(region);
}
} catch (...) {
for (auto& region : std::ranges::reverse_view(m_regions)) {
if (!VirtualProtect(region.BaseAddress, region.RegionSize, region.Protect, &region.Protect)) {
// Could not restore; fast fail
__fastfail(GetLastError());
}
}
throw;
}
}
utils::memory_tenderizer::~memory_tenderizer() {
for (auto& region : std::ranges::reverse_view(m_regions)) {
if (!VirtualProtect(region.BaseAddress, region.RegionSize, region.Protect, &region.Protect)) {
// Could not restore; fast fail
__fastfail(GetLastError());
}
}
}
std::shared_ptr<void> utils::allocate_executable_heap(size_t len) {
static std::weak_ptr<void> s_hHeap;
std::shared_ptr<void> hHeap;
if (hHeap = s_hHeap.lock(); !hHeap) {
static std::mutex m_mtx;
const auto lock = std::lock_guard(m_mtx);
if (hHeap = s_hHeap.lock(); !hHeap) {
if (const auto hHeapRaw = HeapCreate(HEAP_CREATE_ENABLE_EXECUTE, 0, 0); hHeapRaw)
s_hHeap = hHeap = std::shared_ptr<void>(hHeapRaw, HeapDestroy);
else
throw std::runtime_error("Failed to create heap.");
}
}
const auto pAllocRaw = HeapAlloc(hHeap.get(), 0, len);
if (!pAllocRaw)
throw std::runtime_error("Failed to allocate memory.");
return {
pAllocRaw,
[hHeap = std::move(hHeap)](void* pAddress) { HeapFree(hHeap.get(), 0, pAddress); },
};
}
std::shared_ptr<void> utils::create_thunk(void* pfnFunction, void* pThis, uint64_t placeholderValue) {
const auto pcBaseFn = reinterpret_cast<const uint8_t*>(pfnFunction);
auto sourceCode = std::vector<uint8_t>(pcBaseFn, pcBaseFn + 256);
size_t i = 0;
auto placeholderFound = false;
for (nmd_x86_instruction instruction{}; ; i += instruction.length) {
if (i == sourceCode.size() || !nmd_x86_decode(&sourceCode[i], sourceCode.size() - i, &instruction, NMD_X86_MODE_64, NMD_X86_DECODER_FLAGS_ALL)) {
sourceCode.insert(sourceCode.end(), &pcBaseFn[sourceCode.size()], &pcBaseFn[sourceCode.size() + 512]);
if (!nmd_x86_decode(&sourceCode[i], sourceCode.size() - i, &instruction, NMD_X86_MODE_64, NMD_X86_DECODER_FLAGS_ALL))
throw std::runtime_error("Failed to find detour function");
}
if (instruction.opcode == 0xCC)
throw std::runtime_error("Failed to find detour function");
// msvc debugger related
if ((instruction.group & NMD_GROUP_CALL) && (instruction.imm_mask & NMD_X86_IMM_ANY))
std::fill_n(&sourceCode[i], instruction.length, 0x90);
if ((instruction.group & NMD_GROUP_JUMP) || (instruction.group & NMD_GROUP_RET)) {
sourceCode.resize(i + instruction.length);
break;
}
if (instruction.opcode == 0xB8 // mov <register>, <thunk placeholder 64bit value>
&& (instruction.imm_mask & NMD_X86_IMM64)
&& instruction.immediate == placeholderValue) {
*reinterpret_cast<void**>(&sourceCode[i + instruction.length - 8]) = pThis;
placeholderFound = true;
}
}
if (!placeholderFound)
throw std::runtime_error("Failed to find detour function");
return allocate_executable_heap(std::span(sourceCode));
}
template<>
std::wstring utils::get_env(const wchar_t* pcwzName) {
std::wstring buf(GetEnvironmentVariableW(pcwzName, nullptr, 0) + 1, L'\0');
buf.resize(GetEnvironmentVariableW(pcwzName, &buf[0], static_cast<DWORD>(buf.size())));
return buf;
}
template<>
std::string utils::get_env(const wchar_t* pcwzName) {
return unicode::convert<std::string>(get_env<std::wstring>(pcwzName));
}
template<>
int utils::get_env(const wchar_t* pcwzName) {
auto env = get_env<std::wstring>(pcwzName);
const auto trimmed = trim(std::wstring_view(env));
if (trimmed.empty())
return 0;
return std::wcstol(&trimmed[0], nullptr, 0);
}
template<>
bool utils::get_env(const wchar_t* pcwzName) {
auto env = get_env<std::wstring>(pcwzName);
const auto trimmed = trim(std::wstring_view(env));
for (auto& c : env) {
if (c < 255)
c = std::tolower(c);
}
return trimmed == L"1"
|| trimmed == L"true"
|| trimmed == L"t"
|| trimmed == L"yes"
|| trimmed == L"y";
}
template<>
std::vector<std::wstring> utils::get_env_list(const wchar_t* pcszName) {
const auto src = utils::get_env<std::wstring>(pcszName);
auto res = utils::split(src, L",");
for (auto& s : res)
s = utils::trim(s);
if (res.size() == 1 && res[0].empty())
return {};
return res;
}
template<>
std::vector<std::string> utils::get_env_list(const wchar_t* pcszName) {
const auto src = utils::get_env<std::string>(pcszName);
auto res = utils::split(src, ",");
for (auto& s : res)
s = utils::trim(s);
if (res.size() == 1 && res[0].empty())
return {};
return res;
}
bool utils::is_running_on_linux() {
if (get_env<bool>(L"XL_WINEONLINUX"))
return true;
HMODULE hntdll = GetModuleHandleW(L"ntdll.dll");
if (!hntdll)
return true;
if (GetProcAddress(hntdll, "wine_get_version"))
return true;
if (GetProcAddress(hntdll, "wine_get_host_version"))
return true;
return false;
}
std::filesystem::path utils::get_module_path(HMODULE hModule) {
std::wstring buf(MAX_PATH, L'\0');
while (true) {
if (const auto res = GetModuleFileNameW(hModule, &buf[0], static_cast<int>(buf.size())); !res)
throw std::runtime_error(std::format("GetModuleFileName failure: 0x{:X}", GetLastError()));
else if (res < buf.size()) {
buf.resize(res);
return buf;
} else
buf.resize(buf.size() * 2);
}
}
HWND utils::try_find_game_window() {
HWND hwnd = nullptr;
while ((hwnd = FindWindowExW(nullptr, hwnd, L"FFXIVGAME", nullptr))) {
DWORD pid;
GetWindowThreadProcessId(hwnd, &pid);
if (pid == GetCurrentProcessId() && IsWindowVisible(hwnd))
break;
}
return hwnd;
}
void utils::wait_for_game_window() {
HWND game_window;
while (!(game_window = try_find_game_window())) {
WaitForInputIdle(GetCurrentProcess(), INFINITE);
Sleep(100);
};
SendMessageW(game_window, WM_NULL, 0, 0);
}

263
Dalamud.Boot/utils.h Normal file
View file

@ -0,0 +1,263 @@
#pragma once
#include <filesystem>
#include <functional>
#include <span>
#include <string>
#include <memory>
#include <vector>
#include "unicode.h"
namespace utils {
class loaded_module {
HMODULE m_hModule;
public:
loaded_module() : m_hModule(nullptr) {}
loaded_module(const void* hModule) : m_hModule(reinterpret_cast<HMODULE>(const_cast<void*>(hModule))) {}
loaded_module(void* hModule) : m_hModule(reinterpret_cast<HMODULE>(hModule)) {}
loaded_module(size_t hModule) : m_hModule(reinterpret_cast<HMODULE>(hModule)) {}
std::filesystem::path path() const;
bool is_current_process() const { return m_hModule == GetModuleHandleW(nullptr); }
bool owns_address(const void* pAddress) const;
operator HMODULE() const {
return m_hModule;
}
size_t address_int() const { return reinterpret_cast<size_t>(m_hModule); }
size_t image_size() const { return is_pe64() ? nt_header64().OptionalHeader.SizeOfImage : nt_header32().OptionalHeader.SizeOfImage; }
char* address(size_t offset = 0) const { return reinterpret_cast<char*>(m_hModule) + offset; }
template<typename T> T* address_as(size_t offset) const { return reinterpret_cast<T*>(address(offset)); }
template<typename T> std::span<T> span_as(size_t offset, size_t count) const { return std::span<T>(reinterpret_cast<T*>(address(offset)), count); }
template<typename T> T& ref_as(size_t offset) const { return *reinterpret_cast<T*>(address(offset)); }
IMAGE_DOS_HEADER& dos_header() const { return ref_as<IMAGE_DOS_HEADER>(0); }
IMAGE_NT_HEADERS32& nt_header32() const { return ref_as<IMAGE_NT_HEADERS32>(dos_header().e_lfanew); }
IMAGE_NT_HEADERS64& nt_header64() const { return ref_as<IMAGE_NT_HEADERS64>(dos_header().e_lfanew); }
bool is_pe64() const { return nt_header32().OptionalHeader.Magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC; }
std::span<IMAGE_DATA_DIRECTORY> data_directories() const { return is_pe64() ? nt_header64().OptionalHeader.DataDirectory : nt_header32().OptionalHeader.DataDirectory; }
IMAGE_DATA_DIRECTORY& data_directory(size_t index) const { return data_directories()[index]; }
std::span<IMAGE_SECTION_HEADER> section_headers() const;
IMAGE_SECTION_HEADER& section_header(const char* pcszSectionName) const;
std::span<char> section(size_t index) const;
std::span<char> section(const char* pcszSectionName) const;
template<typename TFn> TFn* get_exported_function(const char* pcszFunctionName) {
const auto pAddress = GetProcAddress(m_hModule, pcszFunctionName);
if (!pAddress)
throw std::out_of_range(std::format("Exported function \"{}\" not found.", pcszFunctionName));
return reinterpret_cast<TFn*>(pAddress);
}
bool find_imported_function_pointer(const char* pcszDllName, const char* pcszFunctionName, uint32_t hintOrOrdinal, void*& ppFunctionAddress) const;
void* get_imported_function_pointer(const char* pcszDllName, const char* pcszFunctionName, uint32_t hintOrOrdinal) const;
template<typename TFn> TFn** get_imported_function_pointer(const char* pcszDllName, const char* pcszFunctionName, uint32_t hintOrOrdinal) { return reinterpret_cast<TFn**>(get_imported_function_pointer(pcszDllName, pcszFunctionName, hintOrOrdinal)); }
static loaded_module current_process();
static std::vector<loaded_module> all_modules();
};
class signature_finder {
std::vector<std::span<const char>> m_ranges;
std::vector<srell::regex> m_patterns;
public:
signature_finder& look_in(const void* pFirst, size_t length);
signature_finder& look_in(const loaded_module& m, const char* sectionName);
template<typename T>
signature_finder& look_in(std::span<T> s) {
return look_in(s.data(), s.size());
}
signature_finder& look_for(std::string_view pattern, std::string_view mask, char cExactMatch = 'x', char cWildcard = '.');
signature_finder& look_for(std::string_view pattern, char wildcardMask);
signature_finder& look_for(std::string_view pattern);
signature_finder& look_for_hex(std::string_view pattern);
template<size_t len>
signature_finder& look_for(char pattern[len]) {
static_assert(len == 5);
}
struct result {
std::span<const char> Match;
size_t PatternIndex;
size_t MatchIndex;
size_t CaptureIndex;
};
std::vector<result> find(size_t minCount, size_t maxCount, bool bErrorOnMoreThanMaximum) const;
std::span<const char> find_one() const;
};
class memory_tenderizer {
std::span<char> m_data;
std::vector<MEMORY_BASIC_INFORMATION> m_regions;
public:
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>>>
memory_tenderizer(const T& object, DWORD dwNewProtect) : memory_tenderizer(&object, sizeof T, dwNewProtect) {}
template<typename T>
memory_tenderizer(std::span<const T> s, DWORD dwNewProtect) : memory_tenderizer(&s[0], s.size(), dwNewProtect) {}
template<typename T>
memory_tenderizer(std::span<T> s, DWORD dwNewProtect) : memory_tenderizer(&s[0], s.size(), dwNewProtect) {}
~memory_tenderizer();
};
std::shared_ptr<void> allocate_executable_heap(size_t len);
template<typename T>
std::shared_ptr<void> allocate_executable_heap(std::span<T> data) {
auto res = allocate_executable_heap(data.size_bytes());
memcpy(res.get(), data.data(), data.size_bytes());
return res;
}
std::shared_ptr<void> create_thunk(void* pfnFunction, void* pThis, uint64_t placeholderValue);
template<typename>
class thunk;
template<typename TReturn, typename ... TArgs>
class thunk<TReturn(TArgs...)> {
using TFn = TReturn(TArgs...);
static constexpr uint64_t Placeholder = 0xCC90CC90CC90CC90ULL;
const std::shared_ptr<void> m_pThunk;
std::string m_name;
std::function<TFn> m_fnTarget;
public:
thunk(std::string name, std::function<TFn> target)
: m_pThunk(utils::create_thunk(&detour_static, this, Placeholder))
, m_fnTarget(std::move(target))
, m_name(name) {
}
void set_target(std::function<TFn> detour) {
m_fnTarget = std::move(detour);
}
TFn* get_thunk() const {
return reinterpret_cast<TFn*>(m_pThunk.get());
}
const std::string& name() const {
return m_name;
}
private:
// mark it as virtual to prevent compiler from inlining
virtual TReturn detour(TArgs... args) {
return m_fnTarget(std::forward<TArgs>(args)...);
}
static TReturn detour_static(TArgs... args) {
const volatile auto pThis = reinterpret_cast<thunk<TFn>*>(Placeholder);
return pThis->detour(args...);
}
};
template<class TElem, class TTraits = std::char_traits<TElem>>
std::basic_string_view<TElem, TTraits> trim(std::basic_string_view<TElem, TTraits> view, bool left = true, bool right = true) {
if (left) {
while (!view.empty() && (view.front() < 255 && std::isspace(view.front())))
view = view.substr(1);
}
if (right) {
while (!view.empty() && (view.back() < 255 && std::isspace(view.back())))
view = view.substr(0, view.size() - 1);
}
return view;
}
template<class TElem, class TTraits = std::char_traits<TElem>, class TAlloc = std::allocator<TElem>>
std::basic_string<TElem, TTraits> trim(std::basic_string<TElem, TTraits> view, bool left = true, bool right = true) {
return std::basic_string<TElem, TTraits, TAlloc>(trim(std::basic_string_view<TElem, TTraits>(view), left, right));
}
template<class TElem, class TTraits = std::char_traits<TElem>, class TAlloc = std::allocator<TElem>>
[[nodiscard]] std::vector<std::basic_string<TElem, TTraits, TAlloc>> split(const std::basic_string<TElem, TTraits, TAlloc>& str, const std::basic_string_view<TElem, TTraits>& delimiter, size_t maxSplit = SIZE_MAX) {
std::vector<std::basic_string<TElem, TTraits, TAlloc>> result;
if (delimiter.empty()) {
for (size_t i = 0; i < str.size(); ++i)
result.push_back(str.substr(i, 1));
} else {
size_t previousOffset = 0, offset;
while (maxSplit && (offset = str.find(delimiter, previousOffset)) != std::string::npos) {
result.push_back(str.substr(previousOffset, offset - previousOffset));
previousOffset = offset + delimiter.length();
--maxSplit;
}
result.push_back(str.substr(previousOffset));
}
return result;
}
template<class TElem, class TTraits = std::char_traits<TElem>, class TAlloc = std::allocator<TElem>>
[[nodiscard]] std::vector<std::basic_string<TElem, TTraits, TAlloc>> split(const std::basic_string<TElem, TTraits, TAlloc>& str, const std::basic_string<TElem, TTraits, TAlloc>& delimiter, size_t maxSplit = SIZE_MAX) {
return split(str, std::basic_string_view<TElem, TTraits>(delimiter), maxSplit);
}
template<class TElem, class TTraits = std::char_traits<TElem>, class TAlloc = std::allocator<TElem>>
[[nodiscard]] std::vector<std::basic_string<TElem, TTraits, TAlloc>> split(const std::basic_string<TElem, TTraits, TAlloc>& str, const TElem* pcszDelimiter, size_t maxSplit = SIZE_MAX) {
return split(str, std::basic_string_view<TElem, TTraits>(pcszDelimiter), maxSplit);
}
template<typename T>
T get_env(const wchar_t* pcwzName) = delete;
template<>
std::wstring get_env(const wchar_t* pcwzName);
template<>
std::string get_env(const wchar_t* pcwzName);
template<>
int get_env(const wchar_t* pcwzName);
template<>
bool get_env(const wchar_t* pcwzName);
template<typename T>
T get_env(const char* pcszName) {
return get_env<T>(unicode::convert<std::wstring>(pcszName).c_str());
}
template<typename T>
std::vector<T> get_env_list(const wchar_t* pcwzName) = delete;
template<>
std::vector<std::wstring> get_env_list(const wchar_t* pcwzName);
template<>
std::vector<std::string> get_env_list(const wchar_t* pcwzName);
template<typename T>
std::vector<T> get_env_list(const char* pcszName) {
return get_env_list<T>(unicode::convert<std::wstring>(pcszName).c_str());
}
bool is_running_on_linux();
std::filesystem::path get_module_path(HMODULE hModule);
/// @brief Find the game main window.
/// @return Handle to the game main window, or nullptr if it doesn't exist (yet).
HWND try_find_game_window();
void wait_for_game_window();
}

View file

@ -2,6 +2,9 @@
#include "veh.h"
PVOID g_veh_handle = nullptr;
bool g_veh_do_full_dump = false;
bool is_whitelist_exception(const DWORD code)
{
switch (code)
@ -251,8 +254,12 @@ LONG exception_handler(EXCEPTION_POINTERS* ex)
ex_info.ExceptionPointers = ex;
ex_info.ThreadId = GetCurrentThreadId();
auto miniDumpType = MiniDumpWithDataSegs;
if (g_veh_do_full_dump)
miniDumpType = MiniDumpWithFullMemory;
HANDLE file = CreateFileW(dmp_path.c_str(), GENERIC_WRITE, FILE_SHARE_WRITE, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), file, MiniDumpWithDataSegs, &ex_info, nullptr, nullptr);
MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), file, miniDumpType, &ex_info, nullptr, nullptr);
CloseHandle(file);
void* fn;
@ -262,16 +269,18 @@ LONG exception_handler(EXCEPTION_POINTERS* ex)
L"Dalamud.EntryPoint+VehDelegate, Dalamud",
nullptr, nullptr, &fn)))
{
const auto msg = L"An error within the game has occurred.\n\n"
const auto formatted = std::format(
L"An error within the game has occurred.\n\n"
L"This may be caused by a faulty plugin, a broken TexTools modification, any other third-party tool or simply a bug in the game.\n"
L"Please try \"Start Over\" or \"Download Index Backup\" in TexTools, an integrity check in the XIVLauncher settings, and disabling plugins you don't need.\n\n"
L"The log file is located at:\n"
L"{1}\n\n"
L"Press OK to exit the application.\n\nFailed to read stack trace: {2:08x}";
L"Press OK to exit the application.\n\nFailed to read stack trace: {2:08x}",
dmp_path, log_path, err);
// show in another thread to prevent messagebox from pumping messages of current thread
std::thread([&]() {
MessageBoxW(nullptr, std::format(msg, dmp_path, log_path, err).c_str(), L"Dalamud Error", MB_OK | MB_ICONERROR | MB_TOPMOST);
MessageBoxW(nullptr, formatted.c_str(), L"Dalamud Error", MB_OK | MB_ICONERROR | MB_TOPMOST);
}).join();
}
else
@ -282,15 +291,15 @@ LONG exception_handler(EXCEPTION_POINTERS* ex)
return EXCEPTION_CONTINUE_SEARCH;
}
PVOID g_veh_handle = nullptr;
bool veh::add_handler()
bool veh::add_handler(bool doFullDump)
{
if (g_veh_handle)
return false;
g_veh_handle = AddVectoredExceptionHandler(1, exception_handler);
SetUnhandledExceptionFilter(nullptr);
g_veh_do_full_dump = doFullDump;
return g_veh_handle != nullptr;
}

View file

@ -2,6 +2,6 @@
namespace veh
{
bool add_handler();
bool add_handler(bool doFullDump);
bool remove_handler();
}

426
Dalamud.Boot/xivfixes.cpp Normal file
View file

@ -0,0 +1,426 @@
#include "pch.h"
#include "xivfixes.h"
#include "DalamudStartInfo.h"
#include "hooks.h"
#include "logging.h"
#include "utils.h"
template<typename T>
static std::span<T> assume_nonempty_span(std::span<T> t, const char* descr) {
if (t.empty())
throw std::runtime_error(std::format("Unexpected empty span found: {}", descr));
return t;
}
void xivfixes::unhook_dll(bool bApply) {
static const auto LogTag = "[xivfixes:unhook_dll]";
static const auto LogTagW = L"[xivfixes:unhook_dll]";
if (!bApply)
return;
const auto mods = utils::loaded_module::all_modules();
const auto test_module = [&](size_t i, const utils::loaded_module & mod) {
std::filesystem::path path;
try {
path = mod.path();
logging::I("{} [{}/{}] Module 0x{:X} ~ 0x{:X} (0x{:X}): \"{}\"", LogTagW, i + 1, mods.size(), mod.address_int(), mod.address_int() + mod.image_size(), mod.image_size(), path.wstring());
} catch (const std::exception& e) {
logging::W("{} [{}/{}] Module 0x{:X}: Failed to resolve path: {}", LogTag, i + 1, mods.size(), mod.address_int(), e.what());
return;
}
const auto moduleName = unicode::convert<std::string>(path.filename().wstring());
std::vector<char> buf;
std::string formatBuf;
try {
const auto& sectionHeader = mod.section_header(".text");
const auto section = assume_nonempty_span(mod.span_as<char>(sectionHeader.VirtualAddress, sectionHeader.Misc.VirtualSize), ".text[VA:VA+VS]");
auto hFsDllRaw = CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, 0, nullptr);
if (hFsDllRaw == INVALID_HANDLE_VALUE) {
logging::W("{} Module loaded in current process but could not open file: Win32 error {}", LogTag, GetLastError());
return;
}
auto hFsDll = std::unique_ptr<void, decltype(CloseHandle)*>(hFsDllRaw, &CloseHandle);
buf.resize(section.size());
SetFilePointer(hFsDll.get(), sectionHeader.PointerToRawData, nullptr, FILE_CURRENT);
if (DWORD read{}; ReadFile(hFsDll.get(), &buf[0], static_cast<DWORD>(buf.size()), &read, nullptr)) {
if (read < section.size_bytes()) {
logging::W("{} ReadFile: read {} bytes < requested {} bytes", LogTagW, read, section.size_bytes());
return;
}
} else {
logging::I("{} ReadFile: Win32 error {}", LogTagW, GetLastError());
return;
}
const auto doRestore = g_startInfo.BootUnhookDlls.contains(unicode::convert<std::string>(path.filename().u8string()));
std::optional<utils::memory_tenderizer> tenderizer;
for (size_t i = 0, instructionLength = 1, printed = 0; i < buf.size(); i += instructionLength) {
if (section[i] == buf[i]) {
instructionLength = 1;
continue;
}
const auto rva = sectionHeader.VirtualAddress + i;
nmd_x86_instruction instruction{};
if (!nmd_x86_decode(&section[i], section.size() - i, &instruction, NMD_X86_MODE_64, NMD_X86_DECODER_FLAGS_ALL)) {
instructionLength = 1;
if (printed < 64) {
logging::W("{} {}+0x{:0X}: dd {:02X}", LogTag, moduleName, rva, static_cast<uint8_t>(section[i]));
printed++;
}
} else {
instructionLength = instruction.length;
if (printed < 64) {
formatBuf.resize(128);
nmd_x86_format(&instruction, &formatBuf[0], reinterpret_cast<size_t>(&section[i]), NMD_X86_FORMAT_FLAGS_DEFAULT | NMD_X86_FORMAT_FLAGS_BYTES);
formatBuf.resize(strnlen(&formatBuf[0], formatBuf.size()));
const auto& directory = mod.data_directory(IMAGE_DIRECTORY_ENTRY_EXPORT);
const auto& exportDirectory = mod.ref_as<IMAGE_EXPORT_DIRECTORY>(directory.VirtualAddress);
const auto names = mod.span_as<DWORD>(exportDirectory.AddressOfNames, exportDirectory.NumberOfNames);
const auto ordinals = mod.span_as<WORD>(exportDirectory.AddressOfNameOrdinals, exportDirectory.NumberOfNames);
const auto functions = mod.span_as<DWORD>(exportDirectory.AddressOfFunctions, exportDirectory.NumberOfFunctions);
std::string resolvedExportName;
for (size_t j = 0; j < names.size(); ++j) {
std::string_view name;
if (const char* pcszName = mod.address_as<char>(names[j]); pcszName < mod.address() || pcszName >= mod.address() + mod.image_size()) {
if (IsBadReadPtr(pcszName, 256)) {
logging::W("{} Name #{} points to an invalid address outside the executable. Skipping.", LogTag, j);
continue;
}
name = std::string_view(pcszName, strnlen(pcszName, 256));
logging::W("{} Name #{} points to a seemingly valid address outside the executable: {}", LogTag, j, name);
}
if (ordinals[j] >= functions.size()) {
logging::W("{} Ordinal #{} points to function index #{} >= #{}. Skipping.", LogTag, j, ordinals[j], functions.size());
continue;
}
const auto rva = functions[ordinals[j]];
if (rva == &section[i] - mod.address()) {
resolvedExportName = std::format("[export:{}]", name);
break;
}
}
logging::W("{} {}+0x{:0X}{}: {}", LogTag, moduleName, rva, resolvedExportName, formatBuf);
printed++;
}
}
if (doRestore) {
if (!tenderizer)
tenderizer.emplace(section, PAGE_EXECUTE_READWRITE);
memcpy(&section[i], &buf[i], instructionLength);
}
}
if (tenderizer)
logging::I("{} Verification and overwriting complete.", LogTag);
else if (doRestore)
logging::I("{} Verification complete. Overwriting was not required.", LogTag);
} catch (const std::exception& e) {
logging::W("{} Error: {}", LogTag, e.what());
}
};
// This is needed since try and __try cannot be used in the same function. Lambdas circumvent the limitation.
const auto windows_exception_handler = [&]() {
for (size_t i = 0; i < mods.size(); i++) {
const auto& mod = mods[i];
__try {
test_module(i, mod);
} __except (EXCEPTION_EXECUTE_HANDLER) {
logging::W("{} Error: Access Violation", LogTag);
}
}
};
windows_exception_handler();
}
using TFnGetInputDeviceManager = void* ();
static TFnGetInputDeviceManager* GetGetInputDeviceManager(HWND hwnd) {
static TFnGetInputDeviceManager* pCached = nullptr;
if (pCached)
return pCached;
char szClassName[256];
GetClassNameA(hwnd, szClassName, static_cast<int>(sizeof szClassName));
WNDCLASSEXA wcx{};
GetClassInfoExA(g_hGameInstance, szClassName, &wcx);
const auto match = utils::signature_finder()
.look_in(utils::loaded_module(g_hGameInstance), ".text")
.look_for_hex("41 81 fe 19 02 00 00 0f 87 ?? ?? 00 00 0f 84 ?? ?? 00 00")
.find_one();
auto ptr = match.data() + match.size() + *reinterpret_cast<const int*>(match.data() + match.size() - 4);
ptr += 4; // CMP RBX, 0x7
ptr += 2; // JNZ <giveup>
ptr += 7; // MOV RCX, <Framework::Instance>
ptr += 3; // TEST RCX, RCX
ptr += 2; // JZ <giveup>
ptr += 5; // CALL <GetInputDeviceManagerInstance()>
ptr += *reinterpret_cast<const int*>(ptr - 4);
return pCached = reinterpret_cast<TFnGetInputDeviceManager*>(ptr);
}
void xivfixes::prevent_devicechange_crashes(bool bApply) {
static const char* LogTag = "[xivfixes:prevent_devicechange_crashes]";
static std::optional<hooks::import_hook<decltype(CreateWindowExA)>> s_hookCreateWindowExA;
static std::optional<hooks::wndproc_hook> s_hookWndProc;
if (bApply) {
if (!g_startInfo.BootEnabledGameFixes.contains("prevent_devicechange_crashes")) {
logging::I("{} Turned off via environment variable.", LogTag);
return;
}
s_hookCreateWindowExA.emplace("user32.dll!CreateWindowExA (prevent_devicechange_crashes)", "user32.dll", "CreateWindowExA", 0);
s_hookCreateWindowExA->set_detour([](DWORD dwExStyle, LPCSTR lpClassName, LPCSTR lpWindowName, DWORD dwStyle, int X, int Y, int nWidth, int nHeight, HWND hWndParent, HMENU hMenu, HINSTANCE hInstance, LPVOID lpParam)->HWND {
const auto hWnd = s_hookCreateWindowExA->call_original(dwExStyle, lpClassName, lpWindowName, dwStyle, X, Y, nWidth, nHeight, hWndParent, hMenu, hInstance, lpParam);
if (!hWnd
|| hInstance != g_hGameInstance
|| 0 != strcmp(lpClassName, "FFXIVGAME"))
return hWnd;
logging::I(R"({} CreateWindow(0x{:08X}, "{}", "{}", 0x{:08X}, {}, {}, {}, {}, 0x{:X}, 0x{:X}, 0x{:X}, 0x{:X}) called; unhooking CreateWindowExA and hooking WndProc.)",
LogTag, dwExStyle, lpClassName, lpWindowName, dwStyle, X, Y, nWidth, nHeight, reinterpret_cast<size_t>(hWndParent), reinterpret_cast<size_t>(hMenu), reinterpret_cast<size_t>(hInstance), reinterpret_cast<size_t>(lpParam));
s_hookWndProc.emplace("FFXIVGAME:WndProc (prevent_devicechange_crashes)", hWnd);
s_hookWndProc->set_detour([](HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) -> LRESULT {
if (uMsg == WM_DEVICECHANGE && wParam == DBT_DEVNODES_CHANGED) {
if (!GetGetInputDeviceManager(hWnd)()) {
logging::I("{} WndProc(0x{:X}, WM_DEVICECHANGE, DBT_DEVNODES_CHANGED, {}) called but the game does not have InputDeviceManager initialized; doing nothing.", LogTag, reinterpret_cast<size_t>(hWnd), lParam);
return 0;
}
}
return s_hookWndProc->call_original(hWnd, uMsg, wParam, lParam);
});
return hWnd;
});
logging::I("{} Enable", LogTag);
} else {
if (s_hookCreateWindowExA) {
logging::I("{} Disable CreateWindowExA", LogTag);
s_hookCreateWindowExA.reset();
}
// This will effectively revert any other WndProc alterations, including Dalamud.
if (s_hookWndProc) {
logging::I("{} Disable WndProc", LogTag);
s_hookWndProc.reset();
}
}
}
static bool is_xivalex(const std::filesystem::path& dllPath) {
DWORD verHandle = 0;
std::vector<uint8_t> block;
block.resize(GetFileVersionInfoSizeW(dllPath.c_str(), &verHandle));
if (block.empty())
return false;
if (!GetFileVersionInfoW(dllPath.c_str(), 0, static_cast<DWORD>(block.size()), &block[0]))
return false;
struct LANGANDCODEPAGE {
WORD wLanguage;
WORD wCodePage;
} * lpTranslate;
UINT cbTranslate;
if (!VerQueryValueW(&block[0],
TEXT("\\VarFileInfo\\Translation"),
reinterpret_cast<LPVOID*>(&lpTranslate),
&cbTranslate)) {
return false;
}
for (size_t i = 0; i < (cbTranslate / sizeof(struct LANGANDCODEPAGE)); i++) {
wchar_t* buf = nullptr;
UINT size = 0;
if (!VerQueryValueW(&block[0],
std::format(L"\\StringFileInfo\\{:04x}{:04x}\\FileDescription",
lpTranslate[i].wLanguage,
lpTranslate[i].wCodePage).c_str(),
reinterpret_cast<LPVOID*>(&buf),
&size)) {
continue;
}
auto currName = std::wstring_view(buf, size);
while (!currName.empty() && currName.back() == L'\0')
currName = currName.substr(0, currName.size() - 1);
if (currName.empty())
continue;
if (currName == L"XivAlexander Main DLL")
return true;
}
return false;
}
static bool is_openprocess_already_dealt_with() {
static const auto s_value = [] {
for (const auto& mod : utils::loaded_module::all_modules()) {
try {
if (is_xivalex(mod.path()))
return true;
} catch (...) {
// pass
}
}
return false;
}();
return s_value;
}
void xivfixes::disable_game_openprocess_access_check(bool bApply) {
static const char* LogTag = "[xivfixes:disable_game_openprocess_access_check]";
static std::optional<hooks::import_hook<decltype(OpenProcess)>> s_hook;
if (bApply) {
if (!g_startInfo.BootEnabledGameFixes.contains("disable_game_openprocess_access_check")) {
logging::I("{} Turned off via environment variable.", LogTag);
return;
}
if (is_openprocess_already_dealt_with()) {
logging::I("{} Someone else already did it.", LogTag);
return;
}
s_hook.emplace("kernel32.dll!OpenProcess (import, disable_game_openprocess_access_check)", "kernel32.dll", "OpenProcess", 0);
s_hook->set_detour([](DWORD dwDesiredAccess, BOOL bInheritHandle, DWORD dwProcessId)->HANDLE {
logging::I("{} OpenProcess(0x{:08X}, {}, {}) was invoked by thread {}.", LogTag, dwDesiredAccess, bInheritHandle, dwProcessId, GetCurrentThreadId());
if (dwProcessId == GetCurrentProcessId()) {
// Prevent game from feeling unsafe that it restarts
if (dwDesiredAccess & PROCESS_VM_WRITE) {
logging::I("{} Returning failure with last error code set to ERROR_ACCESS_DENIED(5).", LogTag);
SetLastError(ERROR_ACCESS_DENIED);
return {};
}
}
return s_hook->call_original(dwDesiredAccess, bInheritHandle, dwProcessId);
});
logging::I("{} Enable", LogTag);
} else {
if (s_hook) {
logging::I("{} Disable OpenProcess", LogTag);
s_hook.reset();
}
}
}
void xivfixes::redirect_openprocess(bool bApply) {
static const char* LogTag = "[xivfixes:redirect_openprocess]";
static std::shared_ptr<hooks::base_untyped_hook> s_hook;
static std::mutex s_silenceSetMtx;
static std::set<DWORD> s_silenceSet;
if (bApply) {
if (!g_startInfo.BootEnabledGameFixes.contains("redirect_openprocess")) {
logging::I("{} Turned off via environment variable.", LogTag);
return;
}
if (is_openprocess_already_dealt_with()) {
logging::I("{} Someone else already did it.", LogTag);
return;
}
if (g_startInfo.BootDotnetOpenProcessHookMode == DalamudStartInfo::DotNetOpenProcessHookMode::ImportHooks) {
auto hook = std::make_shared<hooks::global_import_hook<decltype(OpenProcess)>>("kernel32.dll!OpenProcess (global import, redirect_openprocess)", L"kernel32.dll", "OpenProcess");
hook->set_detour([hook = hook.get()](DWORD dwDesiredAccess, BOOL bInheritHandle, DWORD dwProcessId)->HANDLE {
if (dwProcessId == GetCurrentProcessId()) {
if (s_silenceSet.emplace(GetCurrentThreadId()).second)
logging::I("{} OpenProcess(0x{:08X}, {}, {}) was invoked by thread {}. Redirecting to DuplicateHandle.", LogTag, dwDesiredAccess, bInheritHandle, dwProcessId, GetCurrentThreadId());
if (HANDLE res; DuplicateHandle(GetCurrentProcess(), GetCurrentProcess(), GetCurrentProcess(), &res, dwDesiredAccess, bInheritHandle, 0))
return res;
return {};
}
return hook->call_original(dwDesiredAccess, bInheritHandle, dwProcessId);
});
s_hook = std::dynamic_pointer_cast<hooks::base_untyped_hook>(std::move(hook));
logging::I("{} Enable via import_hook", LogTag);
} else {
auto hook = std::make_shared<hooks::direct_hook<decltype(OpenProcess)>>("kernel32.dll!OpenProcess (direct, redirect_openprocess)", OpenProcess);
hook->set_detour([hook = hook.get()](DWORD dwDesiredAccess, BOOL bInheritHandle, DWORD dwProcessId)->HANDLE {
if (dwProcessId == GetCurrentProcessId()) {
if (s_silenceSet.emplace(GetCurrentThreadId()).second)
logging::I("{} OpenProcess(0x{:08X}, {}, {}) was invoked by thread {}. Redirecting to DuplicateHandle.", LogTag, dwDesiredAccess, bInheritHandle, dwProcessId, GetCurrentThreadId());
if (HANDLE res; DuplicateHandle(GetCurrentProcess(), GetCurrentProcess(), GetCurrentProcess(), &res, dwDesiredAccess, bInheritHandle, 0))
return res;
return {};
}
return hook->call_original(dwDesiredAccess, bInheritHandle, dwProcessId);
});
s_hook = std::dynamic_pointer_cast<hooks::base_untyped_hook>(std::move(hook));
logging::I("{} Enable via direct_hook", LogTag);
}
//std::thread([]() {
// SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_IDLE);
// for (const auto to = GetTickCount64() + 3000; GetTickCount64() < to;)
// s_hook->assert_dominance();
//}).detach();
} else {
if (s_hook) {
logging::I("{} Disable OpenProcess", LogTag);
s_hook.reset();
}
}
}
void xivfixes::apply_all(bool bApply) {
for (const auto& [taskName, taskFunction] : std::initializer_list<std::pair<const char*, void(*)(bool)>>
{
{ "unhook_dll", &unhook_dll },
{ "prevent_devicechange_crashes", &prevent_devicechange_crashes },
{ "disable_game_openprocess_access_check", &disable_game_openprocess_access_check },
{ "redirect_openprocess", &redirect_openprocess },
}
) {
try {
taskFunction(bApply);
} catch (const std::exception& e) {
if (bApply)
logging::W("Error trying to activate fixup [{}]: {}", taskName, e.what());
else
logging::W("Error trying to deactivate fixup [{}]: {}", taskName, e.what());
continue;
}
if (bApply)
logging::I("Fixup [{}] activated.", taskName);
else
logging::I("Fixup [{}] deactivated.", taskName);
}
}

10
Dalamud.Boot/xivfixes.h Normal file
View file

@ -0,0 +1,10 @@
#pragma once
namespace xivfixes {
void unhook_dll(bool bApply);
void prevent_devicechange_crashes(bool bApply);
void disable_game_openprocess_access_check(bool bApply);
void redirect_openprocess(bool bApply);
void apply_all(bool bApply);
}

View file

@ -27,7 +27,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Lumina" Version="3.5.2" />
<PackageReference Include="Lumina" Version="3.5.4" />
<PackageReference Include="Lumina.Excel" Version="6.1.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.333">

View file

@ -26,7 +26,7 @@
<PropertyGroup Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v142</PlatformToolset>
<PlatformToolset>v143</PlatformToolset>
<LinkIncremental>false</LinkIncremental>
<CharacterSet>Unicode</CharacterSet>
<OutDir>..\bin\$(Configuration)\</OutDir>
@ -88,16 +88,20 @@
<ResourceCompile Include="resources.rc" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="..\Dalamud.Boot\logging.cpp" />
<ClCompile Include="..\Dalamud.Boot\unicode.cpp" />
<ClCompile Include="..\lib\CoreCLR\boot.cpp" />
<ClCompile Include="..\lib\CoreCLR\CoreCLR.cpp" />
<ClCompile Include="main.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\Dalamud.Boot\logging.h" />
<ClInclude Include="..\Dalamud.Boot\unicode.h" />
<ClInclude Include="..\lib\CoreCLR\CoreCLR.h" />
<ClInclude Include="..\lib\CoreCLR\core\coreclr_delegates.h" />
<ClInclude Include="..\lib\CoreCLR\core\hostfxr.h" />
<ClInclude Include="..\lib\CoreCLR\nethost\nethost.h" />
<ClInclude Include="..\lib\CoreCLR\pch.h" />
<ClInclude Include="pch.h" />
</ItemGroup>
<Target Name="RemoveExtraFiles" AfterTargets="PostBuildEvent">
<Delete Files="$(OutDir)$(TargetName).lib" />

View file

@ -34,11 +34,14 @@
<ClCompile Include="..\lib\CoreCLR\CoreCLR.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\Dalamud.Boot\logging.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\Dalamud.Boot\unicode.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\lib\CoreCLR\pch.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="..\lib\CoreCLR\CoreCLR.h">
<Filter>Header Files</Filter>
</ClInclude>
@ -51,5 +54,14 @@
<ClInclude Include="..\lib\CoreCLR\core\coreclr_delegates.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="..\Dalamud.Boot\logging.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="pch.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="..\Dalamud.Boot\unicode.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
</Project>

View file

@ -3,12 +3,14 @@
#include <filesystem>
#include <Windows.h>
#include <shellapi.h>
#include "..\Dalamud.Boot\logging.h"
#include "..\lib\CoreCLR\CoreCLR.h"
#include "..\lib\CoreCLR\boot.h"
int wmain(int argc, wchar_t** argv)
{
printf("Dalamud.Injector, (c) 2021 XIVLauncher Contributors\nBuilt at: %s@%s\n\n", __DATE__, __TIME__);
logging::I("Dalamud Injector, (c) 2021 XIVLauncher Contributors");
logging::I("Built at : " __DATE__ "@" __TIME__);
wchar_t _module_path[MAX_PATH];
GetModuleFileNameW(NULL, _module_path, sizeof _module_path / 2);
@ -22,6 +24,7 @@ int wmain(int argc, wchar_t** argv)
void* entrypoint_vfn;
int result = InitializeClrAndGetEntryPoint(
GetModuleHandleW(nullptr),
false,
runtimeconfig_path,
module_path,
L"Dalamud.Injector.EntryPoint, Dalamud.Injector",
@ -35,9 +38,9 @@ int wmain(int argc, wchar_t** argv)
typedef void (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);
printf("Running Dalamud Injector...\n");
logging::I("Running Dalamud Injector...");
entrypoint_fn(argc, argv);
printf("Done!\n");
logging::I("Done!");
return 0;
}

View file

@ -0,0 +1 @@
#pragma once

View file

@ -64,7 +64,7 @@
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="PeNet" Version="2.6.4" />
<PackageReference Include="Reloaded.Memory" Version="4.1.1" />
<PackageReference Include="Reloaded.Memory.Buffers" Version="1.3.5" />
<PackageReference Include="Reloaded.Memory.Buffers" Version="1.4.4" />
<PackageReference Include="Serilog" Version="2.10.0" />
<PackageReference Include="Serilog.Sinks.Async" Version="1.5.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="4.0.1" />

View file

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
@ -38,7 +39,6 @@ namespace Dalamud.Injector
public static void Main(int argc, IntPtr argvPtr)
{
List<string> args = new(argc);
Init(args);
unsafe
{
@ -47,6 +47,9 @@ namespace Dalamud.Injector
args.Add(Marshal.PtrToStringUni(argv[i]));
}
Init(args);
args.Remove("-v"); // Remove "verbose" flag
if (args.Count >= 2 && args[1].ToLowerInvariant() == "launch-test")
{
Environment.Exit(ProcessLaunchTestCommand(args));
@ -80,6 +83,8 @@ namespace Dalamud.Injector
}
startInfo = ExtractAndInitializeStartInfoFromArguments(startInfo, args);
args.Remove("--console"); // Remove "console" flag, already handled
args.Remove("--etw"); // Remove "etw" flag, already handled
var mainCommand = args[1].ToLowerInvariant();
if (mainCommand.Length > 0 && mainCommand.Length <= 6 && "inject"[..mainCommand.Length] == mainCommand)
@ -100,10 +105,23 @@ namespace Dalamud.Injector
}
}
private static string GetLogPath(string filename)
{
var baseDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
#if DEBUG
var logPath = Path.Combine(baseDirectory, $"{filename}.log");
#else
var logPath = Path.Combine(baseDirectory, "..", "..", "..", $"{filename}.log");
#endif
return logPath;
}
private static void Init(List<string> args)
{
InitLogging(args.Any(x => x == "-v"));
InitUnhandledException(args);
InitLogging();
var cwd = new FileInfo(Assembly.GetExecutingAssembly().Location).Directory;
if (cwd.FullName != Directory.GetCurrentDirectory())
@ -158,23 +176,18 @@ namespace Dalamud.Injector
};
}
private static void InitLogging()
private static void InitLogging(bool verbose)
{
var baseDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
#if DEBUG
var logPath = Path.Combine(baseDirectory, "dalamud.injector.log");
#else
var logPath = Path.Combine(baseDirectory, "..", "..", "..", "dalamud.injector.log");
verbose = true;
#endif
var levelSwitch = new LoggingLevelSwitch();
var levelSwitch = new LoggingLevelSwitch
{
MinimumLevel = verbose ? LogEventLevel.Verbose : LogEventLevel.Information,
};
#if DEBUG
levelSwitch.MinimumLevel = LogEventLevel.Verbose;
#else
levelSwitch.MinimumLevel = LogEventLevel.Information;
#endif
var logPath = GetLogPath("dalamud.injector");
CullLogFile(logPath, 1 * 1024 * 1024);
@ -236,8 +249,7 @@ namespace Dalamud.Injector
int len;
string key;
if (startInfo == null)
startInfo = new();
startInfo ??= new DalamudStartInfo();
var workingDirectory = startInfo.WorkingDirectory;
var configurationPath = startInfo.ConfigurationPath;
@ -288,7 +300,7 @@ namespace Dalamud.Injector
clientLanguage = ClientLanguage.Japanese;
else if (languageStr[0..(len = Math.Min(languageStr.Length, (key = "german").Length))] == key[0..len])
clientLanguage = ClientLanguage.German;
else if (languageStr[0..(len = Math.Min(languageStr.Length, (key = "deutsche").Length))] == key[0..len])
else if (languageStr[0..(len = Math.Min(languageStr.Length, (key = "deutsch").Length))] == key[0..len])
clientLanguage = ClientLanguage.German;
else if (languageStr[0..(len = Math.Min(languageStr.Length, (key = "french").Length))] == key[0..len])
clientLanguage = ClientLanguage.French;
@ -299,17 +311,25 @@ namespace Dalamud.Injector
else
throw new CommandLineException($"\"{languageStr}\" is not a valid supported language.");
return new()
{
WorkingDirectory = workingDirectory,
ConfigurationPath = configurationPath,
PluginDirectory = pluginDirectory,
DefaultPluginDirectory = defaultPluginDirectory,
AssetDirectory = assetDirectory,
Language = clientLanguage,
GameVersion = null,
DelayInitializeMs = delayInitializeMs,
};
startInfo.WorkingDirectory = workingDirectory;
startInfo.ConfigurationPath = configurationPath;
startInfo.PluginDirectory = pluginDirectory;
startInfo.DefaultPluginDirectory = defaultPluginDirectory;
startInfo.AssetDirectory = assetDirectory;
startInfo.Language = clientLanguage;
startInfo.DelayInitializeMs = delayInitializeMs;
startInfo.GameVersion = null;
// Set boot defaults
startInfo.BootShowConsole = args.Contains("--console");
startInfo.BootEnableEtw = args.Contains("--etw");
startInfo.BootLogPath = GetLogPath("dalamud.boot");
startInfo.BootEnabledGameFixes = new List<string> { "prevent_devicechange_crashes", "disable_game_openprocess_access_check", "redirect_openprocess" };
startInfo.BootDotnetOpenProcessHookMode = 0;
// startInfo.BootWaitMessageBox = 2;
// startInfo.BootUnhookDlls = new List<string>() { "kernel32.dll", "ntdll.dll", "user32.dll" };
return startInfo;
}
private static int ProcessHelpCommand(List<string> args, string? particularCommand = default)
@ -324,7 +344,7 @@ namespace Dalamud.Injector
Console.WriteLine("{0} help [command]", exeName);
if (particularCommand is null or "inject")
Console.WriteLine("{0} inject [-h/--help] [-a/--all] [--warn] [pid1] [pid2] [pid3] ...", exeName);
Console.WriteLine("{0} inject [-h/--help] [-a/--all] [--warn] [--fix-acl] [--se-debug-privilege] [pid1] [pid2] [pid3] ...", exeName);
if (particularCommand is null or "launch")
{
@ -332,7 +352,7 @@ namespace Dalamud.Injector
Console.WriteLine("{0} [-g path/to/ffxiv_dx11.exe] [--game=path/to/ffxiv_dx11.exe]", exeSpaces);
Console.WriteLine("{0} [-m entrypoint|inject] [--mode=entrypoint|inject]", exeSpaces);
Console.WriteLine("{0} [--handle-owner=inherited-handle-value]", exeSpaces);
Console.WriteLine("{0} [--without-dalamud]", exeSpaces);
Console.WriteLine("{0} [--without-dalamud] [--no-fix-acl]", exeSpaces);
Console.WriteLine("{0} [-- game_arg1=value1 game_arg2=value2 ...]", exeSpaces);
}
@ -341,6 +361,10 @@ namespace Dalamud.Injector
Console.WriteLine(" [--dalamud-asset-directory=path] [--dalamud-delay-initialize=0(ms)]");
Console.WriteLine(" [--dalamud-client-language=0-3|j(apanese)|e(nglish)|d|g(erman)|f(rench)]");
Console.WriteLine("Verbose logging:\t[-v]");
Console.WriteLine("Show Console:\t[--console]");
Console.WriteLine("Enable ETW:\t[--etw]");
return 0;
}
@ -351,6 +375,8 @@ namespace Dalamud.Injector
var targetProcessSpecified = false;
var warnManualInjection = false;
var showHelp = args.Count <= 2;
var tryFixAcl = false;
var tryClaimSeDebugPrivilege = false;
for (var i = 2; i < args.Count; i++)
{
@ -378,6 +404,14 @@ namespace Dalamud.Injector
targetProcessSpecified = true;
processes.AddRange(Process.GetProcessesByName("ffxiv_dx11"));
}
else if (args[i] == "--fix-acl" || args[i] == "--acl-fix")
{
tryFixAcl = true;
}
else if (args[i] == "--se-debug-privilege")
{
tryClaimSeDebugPrivilege = true;
}
else if (args[i] == "--warn")
{
warnManualInjection = true;
@ -416,8 +450,21 @@ namespace Dalamud.Injector
}
}
if (tryClaimSeDebugPrivilege)
{
try
{
GameStart.ClaimSeDebug();
Log.Information("SeDebugPrivilege claimed.");
}
catch (Win32Exception e2)
{
Log.Warning(e2, "Failed to claim SeDebugPrivilege");
}
}
foreach (var process in processes)
Inject(process, AdjustStartInfo(dalamudStartInfo, process.MainModule.FileName));
Inject(process, AdjustStartInfo(dalamudStartInfo, process.MainModule.FileName), tryFixAcl);
return 0;
}
@ -431,6 +478,7 @@ namespace Dalamud.Injector
var showHelp = args.Count <= 2;
var handleOwner = IntPtr.Zero;
var withoutDalamud = false;
var noFixAcl = false;
var parsingGameArgument = false;
for (var i = 2; i < args.Count; i++)
@ -447,6 +495,8 @@ namespace Dalamud.Injector
useFakeArguments = true;
else if (args[i] == "--without-dalamud")
withoutDalamud = true;
else if (args[i] == "--no-fix-acl" || args[i] == "--no-acl-fix")
noFixAcl = true;
else if (args[i] == "-g")
gamePath = args[++i];
else if (args[i].StartsWith("--game="))
@ -547,7 +597,7 @@ namespace Dalamud.Injector
}
var gameArgumentString = string.Join(" ", gameArguments.Select(x => EncodeParameterArgument(x)));
var process = NativeAclFix.LaunchGame(Path.GetDirectoryName(gamePath), gamePath, gameArgumentString, (Process p) =>
var process = GameStart.LaunchGame(Path.GetDirectoryName(gamePath), gamePath, gameArgumentString, noFixAcl, (Process p) =>
{
if (!withoutDalamud && mode == "entrypoint")
{
@ -558,14 +608,18 @@ namespace Dalamud.Injector
Log.Error("[HOOKS] RewriteRemoteEntryPointW failed");
throw new Exception("RewriteRemoteEntryPointW failed");
}
Log.Verbose("RewriteRemoteEntryPointW called!");
}
});
Log.Verbose("Game process started with PID {0}", process.Id);
if (!withoutDalamud && mode == "inject")
{
var startInfo = AdjustStartInfo(dalamudStartInfo, gamePath);
Log.Information("Using start info: {0}", JsonConvert.SerializeObject(startInfo));
Inject(process, startInfo);
Inject(process, startInfo, false);
}
var processHandleForOwner = IntPtr.Zero;
@ -634,21 +688,26 @@ namespace Dalamud.Injector
var gameVerStr = File.ReadAllText(Path.Combine(ffxivDir, "ffxivgame.ver"));
var gameVer = GameVersion.Parse(gameVerStr);
return new()
return new DalamudStartInfo(startInfo)
{
WorkingDirectory = startInfo.WorkingDirectory,
ConfigurationPath = startInfo.ConfigurationPath,
PluginDirectory = startInfo.PluginDirectory,
DefaultPluginDirectory = startInfo.DefaultPluginDirectory,
AssetDirectory = startInfo.AssetDirectory,
Language = startInfo.Language,
GameVersion = gameVer,
DelayInitializeMs = startInfo.DelayInitializeMs,
};
}
private static void Inject(Process process, DalamudStartInfo startInfo)
private static void Inject(Process process, DalamudStartInfo startInfo, bool tryFixAcl = false)
{
if (tryFixAcl)
{
try
{
GameStart.CopyAclFromSelfToTargetProcess(process.SafeHandle.DangerousGetHandle());
}
catch (Win32Exception e1)
{
Log.Warning(e1, "Failed to copy ACL");
}
}
var bootName = "Dalamud.Boot.dll";
var bootPath = Path.GetFullPath(bootName);

View file

@ -12,9 +12,9 @@ using Serilog;
namespace Dalamud.Injector
{
/// <summary>
/// Class responsible for stripping ACL protections from processes.
/// Class responsible for starting the game and stripping ACL protections from processes.
/// </summary>
public static class NativeAclFix
public static class GameStart
{
/// <summary>
/// Start a process without ACL protections.
@ -22,41 +22,47 @@ namespace Dalamud.Injector
/// <param name="workingDir">The working directory.</param>
/// <param name="exePath">The path to the executable file.</param>
/// <param name="arguments">Arguments to pass to the executable file.</param>
/// <param name="dontFixAcl">Don't actually fix the ACL.</param>
/// <param name="beforeResume">Action to execute before the process is started.</param>
/// <param name="waitForGameWindow">Wait for the game window to be ready before proceeding.</param>
/// <returns>The started process.</returns>
/// <exception cref="Win32Exception">Thrown when a win32 error occurs.</exception>
/// <exception cref="GameExitedException">Thrown when the process did not start correctly.</exception>
public static Process LaunchGame(string workingDir, string exePath, string arguments, Action<Process> beforeResume)
/// <exception cref="GameStartException">Thrown when the process did not start correctly.</exception>
public static Process LaunchGame(string workingDir, string exePath, string arguments, bool dontFixAcl, Action<Process> beforeResume, bool waitForGameWindow = true)
{
Process process = null;
var userName = Environment.UserName;
var pExplicitAccess = default(PInvoke.EXPLICIT_ACCESS);
PInvoke.BuildExplicitAccessWithName(
ref pExplicitAccess,
userName,
PInvoke.STANDARD_RIGHTS_ALL | PInvoke.SPECIFIC_RIGHTS_ALL & ~PInvoke.PROCESS_VM_WRITE,
PInvoke.GRANT_ACCESS,
0);
if (PInvoke.SetEntriesInAcl(1, ref pExplicitAccess, IntPtr.Zero, out var newAcl) != 0)
var psecDesc = IntPtr.Zero;
if (!dontFixAcl)
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
var userName = Environment.UserName;
if (!PInvoke.InitializeSecurityDescriptor(out var secDesc, PInvoke.SECURITY_DESCRIPTOR_REVISION))
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
var pExplicitAccess = default(PInvoke.EXPLICIT_ACCESS);
PInvoke.BuildExplicitAccessWithName(
ref pExplicitAccess,
userName,
PInvoke.STANDARD_RIGHTS_ALL | PInvoke.SPECIFIC_RIGHTS_ALL & ~PInvoke.PROCESS_VM_WRITE,
PInvoke.GRANT_ACCESS,
0);
if (!PInvoke.SetSecurityDescriptorDacl(ref secDesc, true, newAcl, false))
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
if (PInvoke.SetEntriesInAcl(1, ref pExplicitAccess, IntPtr.Zero, out var newAcl) != 0)
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
var psecDesc = Marshal.AllocHGlobal(Marshal.SizeOf<PInvoke.SECURITY_DESCRIPTOR>());
Marshal.StructureToPtr(secDesc, psecDesc, true);
if (!PInvoke.InitializeSecurityDescriptor(out var secDesc, PInvoke.SECURITY_DESCRIPTOR_REVISION))
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
if (!PInvoke.SetSecurityDescriptorDacl(ref secDesc, true, newAcl, false))
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
psecDesc = Marshal.AllocHGlobal(Marshal.SizeOf<PInvoke.SECURITY_DESCRIPTOR>());
Marshal.StructureToPtr(secDesc, psecDesc, true);
}
var lpProcessInformation = default(PInvoke.PROCESS_INFORMATION);
try
@ -75,7 +81,15 @@ namespace Dalamud.Injector
var compatLayerPrev = Environment.GetEnvironmentVariable("__COMPAT_LAYER");
Environment.SetEnvironmentVariable("__COMPAT_LAYER", "RunAsInvoker");
if (!string.IsNullOrEmpty(compatLayerPrev) && !compatLayerPrev.Contains("RunAsInvoker"))
{
Environment.SetEnvironmentVariable("__COMPAT_LAYER", $"RunAsInvoker {compatLayerPrev}");
}
else if (string.IsNullOrEmpty(compatLayerPrev))
{
Environment.SetEnvironmentVariable("__COMPAT_LAYER", "RunAsInvoker");
}
try
{
if (!PInvoke.CreateProcess(
@ -98,7 +112,8 @@ namespace Dalamud.Injector
Environment.SetEnvironmentVariable("__COMPAT_LAYER", compatLayerPrev);
}
DisableSeDebug(lpProcessInformation.hProcess);
if (!dontFixAcl)
DisableSeDebug(lpProcessInformation.hProcess);
process = new ExistingProcess(lpProcessInformation.hProcess);
@ -107,49 +122,40 @@ namespace Dalamud.Injector
PInvoke.ResumeThread(lpProcessInformation.hThread);
// Ensure that the game main window is prepared
try
if (waitForGameWindow)
{
do
try
{
process.WaitForInputIdle();
var tries = 0;
const int maxTries = 420;
const int timeout = 50;
Thread.Sleep(100);
do
{
Thread.Sleep(timeout);
if (process.HasExited)
throw new GameStartException();
if (tries > maxTries)
throw new GameStartException($"Couldn't find game window after {maxTries * timeout}ms");
tries++;
}
while (TryFindGameWindow(process) == IntPtr.Zero);
}
catch (InvalidOperationException)
{
throw new GameStartException("Could not read process information.");
}
while (TryFindGameWindow(process) == IntPtr.Zero);
}
catch (InvalidOperationException)
{
throw new GameExitedException();
}
if (PInvoke.GetSecurityInfo(
PInvoke.GetCurrentProcess(),
PInvoke.SE_OBJECT_TYPE.SE_KERNEL_OBJECT,
PInvoke.SECURITY_INFORMATION.DACL_SECURITY_INFORMATION,
IntPtr.Zero,
IntPtr.Zero,
out var pACL,
IntPtr.Zero,
IntPtr.Zero) != 0)
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
if (PInvoke.SetSecurityInfo(
lpProcessInformation.hProcess,
PInvoke.SE_OBJECT_TYPE.SE_KERNEL_OBJECT,
PInvoke.SECURITY_INFORMATION.DACL_SECURITY_INFORMATION | PInvoke.SECURITY_INFORMATION.UNPROTECTED_DACL_SECURITY_INFORMATION,
IntPtr.Zero,
IntPtr.Zero,
pACL,
IntPtr.Zero) != 0)
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
if (!dontFixAcl)
CopyAclFromSelfToTargetProcess(lpProcessInformation.hProcess);
}
catch (Exception ex)
{
Log.Error(ex, "[NativeAclFix] Uncaught error during initialization, trying to kill process");
Log.Error(ex, "[GameStart] Uncaught error during initialization, trying to kill process");
try
{
@ -157,20 +163,98 @@ namespace Dalamud.Injector
}
catch (Exception killEx)
{
Log.Error(killEx, "[NativeAclFix] Could not kill process");
Log.Error(killEx, "[GameStart] Could not kill process");
}
throw;
}
finally
{
Marshal.FreeHGlobal(psecDesc);
if (psecDesc != IntPtr.Zero)
Marshal.FreeHGlobal(psecDesc);
PInvoke.CloseHandle(lpProcessInformation.hThread);
}
return process;
}
/// <summary>
/// Copies ACL of current process to the target process.
/// </summary>
/// <param name="hProcess">Native handle to the target process.</param>
/// <exception cref="Win32Exception">Thrown when a win32 error occurs.</exception>
public static void CopyAclFromSelfToTargetProcess(IntPtr hProcess)
{
if (PInvoke.GetSecurityInfo(
PInvoke.GetCurrentProcess(),
PInvoke.SE_OBJECT_TYPE.SE_KERNEL_OBJECT,
PInvoke.SECURITY_INFORMATION.DACL_SECURITY_INFORMATION,
IntPtr.Zero,
IntPtr.Zero,
out var pACL,
IntPtr.Zero,
IntPtr.Zero) != 0)
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
if (PInvoke.SetSecurityInfo(
hProcess,
PInvoke.SE_OBJECT_TYPE.SE_KERNEL_OBJECT,
PInvoke.SECURITY_INFORMATION.DACL_SECURITY_INFORMATION | PInvoke.SECURITY_INFORMATION.UNPROTECTED_DACL_SECURITY_INFORMATION,
IntPtr.Zero,
IntPtr.Zero,
pACL,
IntPtr.Zero) != 0)
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
}
public static void ClaimSeDebug()
{
var hToken = PInvoke.INVALID_HANDLE_VALUE;
try
{
if (!PInvoke.OpenThreadToken(PInvoke.GetCurrentThread(), PInvoke.TOKEN_QUERY | PInvoke.TOKEN_ADJUST_PRIVILEGES, false, out hToken))
{
if (Marshal.GetLastWin32Error() != PInvoke.ERROR_NO_TOKEN)
throw new Exception("ClaimSeDebug.OpenProcessToken#1", new Win32Exception(Marshal.GetLastWin32Error()));
if (!PInvoke.ImpersonateSelf(PInvoke.SECURITY_IMPERSONATION_LEVEL.SecurityImpersonation))
throw new Exception("ClaimSeDebug.ImpersonateSelf", new Win32Exception(Marshal.GetLastWin32Error()));
if (!PInvoke.OpenThreadToken(PInvoke.GetCurrentThread(), PInvoke.TOKEN_QUERY | PInvoke.TOKEN_ADJUST_PRIVILEGES, false, out hToken))
throw new Exception("ClaimSeDebug.OpenProcessToken#2", new Win32Exception(Marshal.GetLastWin32Error()));
}
var luidDebugPrivilege = default(PInvoke.LUID);
if (!PInvoke.LookupPrivilegeValue(null, PInvoke.SE_DEBUG_NAME, ref luidDebugPrivilege))
throw new Exception("ClaimSeDebug.LookupPrivilegeValue", new Win32Exception(Marshal.GetLastWin32Error()));
var tpLookup = new PInvoke.TOKEN_PRIVILEGES()
{
PrivilegeCount = 1,
Privileges = new PInvoke.LUID_AND_ATTRIBUTES[1]
{
new PInvoke.LUID_AND_ATTRIBUTES()
{
Luid = luidDebugPrivilege,
Attributes = PInvoke.SE_PRIVILEGE_ENABLED,
},
},
};
if (!PInvoke.AdjustTokenPrivileges(hToken, false, ref tpLookup, 0, IntPtr.Zero, IntPtr.Zero))
throw new Exception("ClaimSeDebug.AdjustTokenPrivileges", new Win32Exception(Marshal.GetLastWin32Error()));
}
finally
{
if (hToken != PInvoke.INVALID_HANDLE_VALUE && hToken != IntPtr.Zero)
PInvoke.CloseHandle(hToken);
}
}
private static void DisableSeDebug(IntPtr processHandle)
{
if (!PInvoke.OpenProcessToken(processHandle, PInvoke.TOKEN_QUERY | PInvoke.TOKEN_ADJUST_PRIVILEGES, out var tokenHandle))
@ -179,7 +263,7 @@ namespace Dalamud.Injector
}
var luidDebugPrivilege = default(PInvoke.LUID);
if (!PInvoke.LookupPrivilegeValue(null, "SeDebugPrivilege", ref luidDebugPrivilege))
if (!PInvoke.LookupPrivilegeValue(null, PInvoke.SE_DEBUG_NAME, ref luidDebugPrivilege))
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
@ -211,7 +295,7 @@ namespace Dalamud.Injector
tokenPrivileges.Privileges[0].Luid = luidDebugPrivilege;
tokenPrivileges.Privileges[0].Attributes = PInvoke.SE_PRIVILEGE_REMOVED;
if (!PInvoke.AdjustTokenPrivileges(tokenHandle, false, ref tokenPrivileges, 0, IntPtr.Zero, 0))
if (!PInvoke.AdjustTokenPrivileges(tokenHandle, false, ref tokenPrivileges, 0, IntPtr.Zero, IntPtr.Zero))
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
@ -239,13 +323,14 @@ namespace Dalamud.Injector
/// <summary>
/// Exception thrown when the process has exited before a window could be found.
/// </summary>
public class GameExitedException : Exception
public class GameStartException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="GameExitedException"/> class.
/// Initializes a new instance of the <see cref="GameStartException"/> class.
/// </summary>
public GameExitedException()
: base("Game exited prematurely.")
/// <param name="message">The message to pass on.</param>
public GameStartException(string? message = null)
: base(message ?? "Game exited prematurely.")
{
}
}
@ -260,6 +345,10 @@ namespace Dalamud.Injector
private static class PInvoke
{
#region Constants
public static readonly IntPtr INVALID_HANDLE_VALUE = new(-1);
public const string SE_DEBUG_NAME = "SeDebugPrivilege";
public const UInt32 STANDARD_RIGHTS_ALL = 0x001F0000;
public const UInt32 SPECIFIC_RIGHTS_ALL = 0x0000FFFF;
public const UInt32 PROCESS_VM_WRITE = 0x0020;
@ -278,6 +367,8 @@ namespace Dalamud.Injector
public const UInt32 SE_PRIVILEGE_ENABLED = 0x00000002;
public const UInt32 SE_PRIVILEGE_REMOVED = 0x00000004;
public const UInt32 ERROR_NO_TOKEN = 0x000003F0;
public enum MULTIPLE_TRUSTEE_OPERATION
{
NO_MULTIPLE_TRUSTEE,
@ -334,6 +425,14 @@ namespace Dalamud.Injector
UNPROTECTED_DACL_SECURITY_INFORMATION = 0x20000000,
PROTECTED_SACL_SECURITY_INFORMATION = 0x40000000,
}
public enum SECURITY_IMPERSONATION_LEVEL
{
SecurityAnonymous,
SecurityIdentification,
SecurityImpersonation,
SecurityDelegation
}
#endregion
#region Methods
@ -384,12 +483,24 @@ namespace Dalamud.Injector
[DllImport("kernel32.dll", SetLastError = true)]
public static extern uint ResumeThread(IntPtr hThread);
[DllImport("advapi32.dll", SetLastError = true)]
public static extern bool ImpersonateSelf(
SECURITY_IMPERSONATION_LEVEL impersonationLevel
);
[DllImport("advapi32.dll", SetLastError = true)]
public static extern bool OpenProcessToken(
IntPtr processHandle,
UInt32 desiredAccess,
out IntPtr tokenHandle);
[DllImport("advapi32.dll", SetLastError = true)]
public static extern bool OpenThreadToken(
IntPtr ThreadHandle,
uint DesiredAccess,
bool OpenAsSelf,
out IntPtr TokenHandle);
[DllImport("advapi32.dll", SetLastError = true)]
public static extern bool LookupPrivilegeValue(string lpSystemName, string lpName, ref LUID lpLuid);
@ -404,9 +515,9 @@ namespace Dalamud.Injector
IntPtr tokenHandle,
bool disableAllPrivileges,
ref TOKEN_PRIVILEGES newState,
UInt32 bufferLengthInBytes,
int cbPreviousState,
IntPtr previousState,
UInt32 returnLengthInBytes);
IntPtr cbOutPreviousState);
[DllImport("advapi32.dll", SetLastError = true)]
public static extern uint GetSecurityInfo(
@ -432,6 +543,9 @@ namespace Dalamud.Injector
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr GetCurrentProcess();
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr GetCurrentThread();
[DllImport("user32.dll", SetLastError = true)]
public static extern IntPtr FindWindowEx(IntPtr parentHandle, IntPtr hWndChildAfter, string className, IntPtr windowTitle);

View file

@ -1,6 +1,6 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.31424.327
# Visual Studio Version 17
VisualStudioVersion = 17.1.32319.34
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{CEF7D22B-CB85-400E-BD64-349A30E3C097}"
ProjectSection(SolutionItems) = preProject
@ -38,106 +38,154 @@ 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
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View file

@ -27,11 +27,6 @@ namespace Dalamud.Configuration.Internal
/// </summary>
public static bool DalamudForceMinHook { get; } = GetEnvironmentVariable("DALAMUD_FORCE_MINHOOK");
/// <summary>
/// Gets a value indicating whether or not Dalamud should wait for a debugger to be attached when initializing.
/// </summary>
public static bool DalamudWaitForDebugger { get; } = GetEnvironmentVariable("DALAMUD_WAIT_DEBUGGER");
/// <summary>
/// Gets a value indicating whether or not Dalamud context menus should be disabled.
/// </summary>

View file

@ -26,6 +26,7 @@ using Dalamud.Plugin.Internal;
using Dalamud.Plugin.Ipc.Internal;
using Dalamud.Support;
using Dalamud.Utility;
using Dalamud.Utility.Timing;
using Serilog;
using Serilog.Core;
using Serilog.Events;
@ -48,6 +49,7 @@ namespace Dalamud
private readonly ManualResetEvent unloadSignal;
private readonly ManualResetEvent finishUnloadSignal;
private readonly IntPtr mainThreadContinueEvent;
private MonoMod.RuntimeDetour.Hook processMonoHook;
private bool hasDisposedPlugins = false;
@ -60,10 +62,9 @@ namespace Dalamud
/// <param name="loggingLevelSwitch">LoggingLevelSwitch to control Serilog level.</param>
/// <param name="finishSignal">Signal signalling shutdown.</param>
/// <param name="configuration">The Dalamud configuration.</param>
public Dalamud(DalamudStartInfo info, LoggingLevelSwitch loggingLevelSwitch, ManualResetEvent finishSignal, DalamudConfiguration configuration)
/// <param name="mainThreadContinueEvent">Event used to signal the main thread to continue.</param>
public Dalamud(DalamudStartInfo info, LoggingLevelSwitch loggingLevelSwitch, ManualResetEvent finishSignal, DalamudConfiguration configuration, IntPtr mainThreadContinueEvent)
{
this.ApplyProcessPatch();
Service<Dalamud>.Set(this);
Service<DalamudStartInfo>.Set(info);
Service<DalamudConfiguration>.Set(configuration);
@ -75,6 +76,8 @@ namespace Dalamud
this.finishUnloadSignal = finishSignal;
this.finishUnloadSignal.Reset();
this.mainThreadContinueEvent = mainThreadContinueEvent;
}
/// <summary>
@ -92,6 +95,8 @@ namespace Dalamud
/// </summary>
public void LoadTier1()
{
using var tier1Timing = Timings.Start("Tier 1 Init");
try
{
SerilogEventSink.Instance.LogLine += SerilogOnLogLine;
@ -99,12 +104,21 @@ namespace Dalamud
Service<ServiceContainer>.Set();
// Initialize the process information.
Service<SigScanner>.Set(new SigScanner(true));
var info = Service<DalamudStartInfo>.Get();
var cacheDir = new DirectoryInfo(Path.Combine(info.WorkingDirectory!, "cachedSigs"));
if (!cacheDir.Exists)
cacheDir.Create();
Service<SigScanner>.Set(
new SigScanner(true, new FileInfo(Path.Combine(cacheDir.FullName, $"{info.GameVersion}.json"))));
Service<HookManager>.Set();
// Initialize FFXIVClientStructs function resolver
FFXIVClientStructs.Resolver.Initialize();
Log.Information("[T1] FFXIVClientStructs initialized!");
using (Timings.Start("CS Resolver Init"))
{
FFXIVClientStructs.Resolver.InitializeParallel(new FileInfo(Path.Combine(cacheDir.FullName, $"{info.GameVersion}_cs.json")));
Log.Information("[T1] FFXIVClientStructs initialized!");
}
// Initialize game subsystem
var framework = Service<Framework>.Set();
@ -123,9 +137,18 @@ namespace Dalamud
}
catch (Exception ex)
{
Log.Error(ex, "Tier 1 load failed.");
Log.Error(ex, "Tier 1 load failed");
this.Unload();
}
finally
{
// Signal the main game thread to continue
// TODO: This is done in rewrite_entrypoint.cpp again to avoid a race condition. Should be fixed!
// NativeFunctions.SetEvent(this.mainThreadContinueEvent);
// Timings.Event("Game kickoff");
// Log.Information("[T1] Game thread continued!");
}
}
/// <summary>
@ -134,6 +157,11 @@ namespace Dalamud
/// <returns>Whether or not the load succeeded.</returns>
public bool LoadTier2()
{
// This marks the first time we are actually on the game's main thread
ThreadSafety.MarkMainThread();
using var tier2Timing = Timings.Start("Tier 2 Init");
try
{
var configuration = Service<DalamudConfiguration>.Get();
@ -157,21 +185,27 @@ namespace Dalamud
Service<NetworkHandlers>.Set();
Log.Information("[T2] NH OK!");
try
using (Timings.Start("DM Init"))
{
Service<DataManager>.Set().Initialize(this.AssetDirectory.FullName);
}
catch (Exception e)
{
Log.Error(e, "Could not initialize DataManager.");
this.Unload();
return false;
try
{
Service<DataManager>.Set().Initialize(this.AssetDirectory.FullName);
}
catch (Exception e)
{
Log.Error(e, "Could not initialize DataManager");
this.Unload();
return false;
}
}
Log.Information("[T2] Data OK!");
var clientState = Service<ClientState>.Set();
Log.Information("[T2] CS OK!");
using (Timings.Start("CS Init"))
{
Service<ClientState>.Set();
Log.Information("[T2] CS OK!");
}
var localization = Service<Localization>.Set(new Localization(Path.Combine(this.AssetDirectory.FullName, "UIRes", "loc", "dalamud"), "dalamud_"));
if (!string.IsNullOrEmpty(configuration.LanguageOverride))
@ -186,14 +220,23 @@ namespace Dalamud
Log.Information("[T2] LOC OK!");
// This is enabled in ImGuiScene setup
Service<DalamudIME>.Set();
Log.Information("[T2] IME OK!");
using (Timings.Start("IME Init"))
{
Service<DalamudIME>.Set();
Log.Information("[T2] IME OK!");
}
Service<InterfaceManager>.Set().Enable();
Log.Information("[T2] IM OK!");
using (Timings.Start("IM Enable"))
{
Service<InterfaceManager>.Set().Enable();
Log.Information("[T2] IM OK!");
}
Service<GameFontManager>.Set();
Log.Information("[T2] GFM OK!");
using (Timings.Start("GFM Init"))
{
Service<GameFontManager>.Set();
Log.Information("[T2] GFM OK!");
}
#pragma warning disable CS0618 // Type or member is obsolete
Service<SeStringManager>.Set();
@ -201,27 +244,30 @@ namespace Dalamud
Log.Information("[T2] SeString OK!");
// Initialize managers. Basically handlers for the logic
Service<CommandManager>.Set();
Service<DalamudCommands>.Set().SetupCommands();
Log.Information("[T2] CM OK!");
using (Timings.Start("CM Init"))
{
Service<CommandManager>.Set();
Service<DalamudCommands>.Set().SetupCommands();
Log.Information("[T2] CM OK!");
}
Service<ChatHandlers>.Set();
Log.Information("[T2] CH OK!");
clientState.Enable();
Log.Information("[T2] CS ENABLE!");
using (Timings.Start("CS Enable"))
{
Service<ClientState>.Get().Enable();
Log.Information("[T2] CS ENABLE!");
}
Service<DalamudAtkTweaks>.Set().Enable();
Log.Information("[T2] ATKTWEAKS ENABLE!");
Log.Information("[T2] Load complete!");
}
catch (Exception ex)
{
Log.Error(ex, "Tier 2 load failed.");
Log.Error(ex, "Tier 2 load failed");
this.Unload();
return false;
}
@ -235,46 +281,62 @@ namespace Dalamud
/// <returns>Whether or not the load succeeded.</returns>
public bool LoadTier3()
{
using var tier3Timing = Timings.Start("Tier 3 Init");
ThreadSafety.AssertMainThread();
try
{
Log.Information("[T3] START!");
Service<TitleScreenMenu>.Set();
var pluginManager = Service<PluginManager>.Set();
Service<CallGate>.Set();
Log.Information("[T3] PM OK!");
PluginManager pluginManager;
using (Timings.Start("PM Init"))
{
pluginManager = Service<PluginManager>.Set();
Service<CallGate>.Set();
Log.Information("[T3] PM OK!");
}
Service<DalamudInterface>.Set();
Log.Information("[T3] DUI OK!");
try
{
_ = pluginManager.SetPluginReposFromConfigAsync(false);
using (Timings.Start("PM Load Plugin Repos"))
{
_ = pluginManager.SetPluginReposFromConfigAsync(false);
pluginManager.OnInstalledPluginsChanged += Troubleshooting.LogTroubleshooting;
pluginManager.OnInstalledPluginsChanged += Troubleshooting.LogTroubleshooting;
Log.Information("[T3] PM repos OK!");
}
Log.Information("[T3] Sync plugins OK!");
using (Timings.Start("PM Cleanup Plugins"))
{
pluginManager.CleanupPlugins();
Log.Information("[T3] PMC OK!");
}
pluginManager.CleanupPlugins();
Log.Information("[T3] PMC OK!");
pluginManager.LoadAllPlugins();
Log.Information("[T3] PML OK!");
using (Timings.Start("PM Load Sync Plugins"))
{
pluginManager.LoadAllPlugins();
Log.Information("[T3] PML OK!");
}
}
catch (Exception ex)
{
Log.Error(ex, "Plugin load failed.");
Log.Error(ex, "Plugin load failed");
}
Troubleshooting.LogTroubleshooting();
Log.Information("Dalamud is ready.");
Log.Information("Dalamud is ready");
Timings.Event("Dalamud ready");
}
catch (Exception ex)
{
Log.Error(ex, "Tier 3 load failed.");
Log.Error(ex, "Tier 3 load failed");
this.Unload();
return false;
@ -354,7 +416,10 @@ namespace Dalamud
Service<AntiDebug>.GetNullable()?.Dispose();
Service<DalamudAtkTweaks>.GetNullable()?.Dispose();
Service<HookManager>.GetNullable()?.Dispose();
Service<SigScanner>.GetNullable()?.Dispose();
var sigScanner = Service<SigScanner>.Get();
sigScanner.Save();
sigScanner.Dispose();
SerilogEventSink.Instance.LogLine -= SerilogOnLogLine;
@ -388,35 +453,5 @@ namespace Dalamud
Troubleshooting.LogException(e.Exception, e.Line);
}
/// <summary>
/// Patch method for the class Process.Handle. This patch facilitates fixing Reloaded so that it
/// uses pseudo-handles to access memory, to prevent permission errors.
/// It should never be called manually.
/// </summary>
/// <param name="orig">A delegate that acts as the original method.</param>
/// <param name="self">The equivalent of `this`.</param>
/// <returns>A pseudo-handle for the current process, or the result from the original method.</returns>
private static IntPtr ProcessHandlePatch(Func<Process, IntPtr> orig, Process self)
{
var result = orig(self);
if (self.Id == Environment.ProcessId)
{
result = (IntPtr)0xFFFFFFFF;
}
// Log.Verbose($"Process.Handle // {self.ProcessName} // {result:X}");
return result;
}
private void ApplyProcessPatch()
{
var targetType = typeof(Process);
var handleTarget = targetType.GetProperty(nameof(Process.Handle)).GetGetMethod();
var handlePatch = typeof(Dalamud).GetMethod(nameof(Dalamud.ProcessHandlePatch), BindingFlags.NonPublic | BindingFlags.Static);
this.processMonoHook = new MonoMod.RuntimeDetour.Hook(handleTarget, handlePatch);
}
}
}

View file

@ -8,7 +8,7 @@
</PropertyGroup>
<PropertyGroup Label="Feature">
<DalamudVersion>6.4.0.9</DalamudVersion>
<DalamudVersion>6.4.0.31</DalamudVersion>
<Description>XIV Launcher addon framework</Description>
<AssemblyVersion>$(DalamudVersion)</AssemblyVersion>
<Version>$(DalamudVersion)</Version>
@ -65,13 +65,14 @@
<ItemGroup>
<PackageReference Include="CheapLoc" Version="1.1.6" />
<PackageReference Include="JetBrains.Annotations" Version="2021.2.0" />
<PackageReference Include="Lumina" Version="3.5.2" />
<PackageReference Include="Lumina" Version="3.5.4" />
<PackageReference Include="Lumina.Excel" Version="6.1.1" />
<PackageReference Include="MinSharp" Version="1.0.4" />
<PackageReference Include="MonoMod.RuntimeDetour" Version="21.10.10.01" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="Serilog" Version="2.10.0" />
<PackageReference Include="Serilog.Sinks.Async" Version="1.5.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="4.0.1" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageReference Include="Reloaded.Hooks" Version="3.5.3" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.333">
@ -94,6 +95,13 @@
<AdditionalFiles Include="..\stylecop.json" />
</ItemGroup>
<ItemGroup>
<None Remove="licenses.txt" />
<Content Include="licenses.txt">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<Target Name="AddRuntimeDependenciesToContent" BeforeTargets="GetCopyToOutputDirectoryItems" DependsOnTargets="GenerateBuildDependencyFile;GenerateBuildRuntimeConfigurationFiles">
<ItemGroup>
<ContentWithTargetPath Include="$(ProjectDepsFilePath)" CopyToOutputDirectory="PreserveNewest" TargetPath="$(ProjectDepsFileName)" />

View file

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using Dalamud.Game;
using Newtonsoft.Json;
@ -11,45 +12,135 @@ namespace Dalamud
[Serializable]
public record DalamudStartInfo
{
/// <summary>
/// Initializes a new instance of the <see cref="DalamudStartInfo"/> class.
/// </summary>
public DalamudStartInfo()
{
// ignored
}
/// <summary>
/// Initializes a new instance of the <see cref="DalamudStartInfo"/> class.
/// </summary>
/// <param name="other">Object to copy values from.</param>
public DalamudStartInfo(DalamudStartInfo other)
{
this.WorkingDirectory = other.WorkingDirectory;
this.ConfigurationPath = other.ConfigurationPath;
this.PluginDirectory = other.PluginDirectory;
this.DefaultPluginDirectory = other.DefaultPluginDirectory;
this.AssetDirectory = other.AssetDirectory;
this.Language = other.Language;
this.GameVersion = other.GameVersion;
this.DelayInitializeMs = other.DelayInitializeMs;
this.BootLogPath = other.BootLogPath;
this.BootShowConsole = other.BootShowConsole;
this.BootDisableFallbackConsole = other.BootDisableFallbackConsole;
this.BootWaitMessageBox = other.BootWaitMessageBox;
this.BootWaitDebugger = other.BootWaitDebugger;
this.BootVehEnabled = other.BootVehEnabled;
this.BootVehFull = other.BootVehFull;
this.BootEnableEtw = other.BootEnableEtw;
this.BootDotnetOpenProcessHookMode = other.BootDotnetOpenProcessHookMode;
this.BootEnabledGameFixes = other.BootEnabledGameFixes;
this.BootUnhookDlls = other.BootUnhookDlls;
}
/// <summary>
/// Gets or sets the working directory of the XIVLauncher installations.
/// </summary>
public string WorkingDirectory { get; set; }
public string? WorkingDirectory { get; set; }
/// <summary>
/// Gets the path to the configuration file.
/// Gets or sets the path to the configuration file.
/// </summary>
public string ConfigurationPath { get; init; }
public string? ConfigurationPath { get; set; }
/// <summary>
/// Gets the path to the directory for installed plugins.
/// Gets or sets the path to the directory for installed plugins.
/// </summary>
public string PluginDirectory { get; init; }
public string? PluginDirectory { get; set; }
/// <summary>
/// Gets the path to the directory for developer plugins.
/// Gets or sets the path to the directory for developer plugins.
/// </summary>
public string DefaultPluginDirectory { get; init; }
public string? DefaultPluginDirectory { get; set; }
/// <summary>
/// Gets the path to core Dalamud assets.
/// Gets or sets the path to core Dalamud assets.
/// </summary>
public string AssetDirectory { get; init; }
public string? AssetDirectory { get; set; }
/// <summary>
/// Gets the language of the game client.
/// Gets or sets the language of the game client.
/// </summary>
public ClientLanguage Language { get; init; }
public ClientLanguage Language { get; set; } = ClientLanguage.English;
/// <summary>
/// Gets the current game version code.
/// Gets or sets the current game version code.
/// </summary>
[JsonConverter(typeof(GameVersionConverter))]
public GameVersion GameVersion { get; init; }
public GameVersion? GameVersion { get; set; }
/// <summary>
/// Gets a value that specifies how much to wait before a new Dalamud session.
/// Gets or sets a value that specifies how much to wait before a new Dalamud session.
/// </summary>
public int DelayInitializeMs { get; init; } = 0;
public int DelayInitializeMs { get; set; } = 0;
/// <summary>
/// Gets or sets the path the boot log file is supposed to be written to.
/// </summary>
public string BootLogPath { get; set; }
/// <summary>
/// Gets or sets a value indicating whether a Boot console should be shown.
/// </summary>
public bool BootShowConsole { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the fallback console should be shown, if needed.
/// </summary>
public bool BootDisableFallbackConsole { get; set; }
/// <summary>
/// Gets or sets a flag indicating where Dalamud should wait with a message box.
/// </summary>
public int BootWaitMessageBox { get; set; }
/// <summary>
/// Gets or sets a value indicating whether Dalamud should wait for a debugger to be attached before initializing.
/// </summary>
public bool BootWaitDebugger { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the VEH should be enabled.
/// </summary>
public bool BootVehEnabled { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the VEH should be doing full crash dumps.
/// </summary>
public bool BootVehFull { get; set; }
/// <summary>
/// Gets or sets a value indicating whether or not ETW should be enabled.
/// </summary>
public bool BootEnableEtw { get; set; }
/// <summary>
/// Gets or sets a value choosing the OpenProcess hookmode.
/// </summary>
public int BootDotnetOpenProcessHookMode { get; set; }
/// <summary>
/// Gets or sets a list of enabled game fixes.
/// </summary>
public List<string>? BootEnabledGameFixes { get; set; }
/// <summary>
/// Gets or sets a list of DLLs that should be unhooked.
/// </summary>
public List<string>? BootUnhookDlls { get; set; }
}
}

View file

@ -9,6 +9,7 @@ using Dalamud.Interface.Internal;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Utility;
using Dalamud.Utility.Timing;
using ImGuiScene;
using JetBrains.Annotations;
using Lumina;
@ -298,25 +299,32 @@ namespace Dalamud.Data
Log.Verbose("Loaded {0} ClientOpCodes.", clientOpCodeDict.Count);
var luminaOptions = new LuminaOptions
using (Timings.Start("Lumina Init"))
{
CacheFileResources = true,
var luminaOptions = new LuminaOptions
{
CacheFileResources = true,
#if DEBUG
PanicOnSheetChecksumMismatch = true,
PanicOnSheetChecksumMismatch = true,
#else
PanicOnSheetChecksumMismatch = false,
PanicOnSheetChecksumMismatch = false,
#endif
DefaultExcelLanguage = this.Language.ToLumina(),
};
DefaultExcelLanguage = this.Language.ToLumina(),
};
var processModule = Process.GetCurrentProcess().MainModule;
if (processModule != null)
{
this.GameData = new GameData(Path.Combine(Path.GetDirectoryName(processModule.FileName), "sqpack"), luminaOptions);
var processModule = Process.GetCurrentProcess().MainModule;
if (processModule != null)
{
this.GameData = new GameData(Path.Combine(Path.GetDirectoryName(processModule.FileName), "sqpack"), luminaOptions);
}
else
{
throw new Exception("Could not main module.");
}
Log.Information("Lumina is ready: {0}", this.GameData.DataPath);
}
Log.Information("Lumina is ready: {0}", this.GameData.DataPath);
this.IsDataReady = true;
this.luminaCancellationTokenSource = new();

View file

@ -29,7 +29,8 @@ namespace Dalamud
/// A delegate used during initialization of the CLR from Dalamud.Boot.
/// </summary>
/// <param name="infoPtr">Pointer to a serialized <see cref="DalamudStartInfo"/> data.</param>
public delegate void InitDelegate(IntPtr infoPtr);
/// <param name="mainThreadContinueEvent">Event used to signal the main thread to continue.</param>
public delegate void InitDelegate(IntPtr infoPtr, IntPtr mainThreadContinueEvent);
/// <summary>
/// A delegate used from VEH handler on exception which CoreCLR will fast fail by default.
@ -43,12 +44,13 @@ namespace Dalamud
/// Initialize Dalamud.
/// </summary>
/// <param name="infoPtr">Pointer to a serialized <see cref="DalamudStartInfo"/> data.</param>
public static void Initialize(IntPtr infoPtr)
/// <param name="mainThreadContinueEvent">Event used to signal the main thread to continue.</param>
public static void Initialize(IntPtr infoPtr, IntPtr mainThreadContinueEvent)
{
var infoStr = Marshal.PtrToStringUTF8(infoPtr);
var info = JsonConvert.DeserializeObject<DalamudStartInfo>(infoStr);
new Thread(() => RunThread(info)).Start();
new Thread(() => RunThread(info, mainThreadContinueEvent)).Start();
}
/// <summary>
@ -106,18 +108,11 @@ namespace Dalamud
/// Initialize all Dalamud subsystems and start running on the main thread.
/// </summary>
/// <param name="info">The <see cref="DalamudStartInfo"/> containing information needed to initialize Dalamud.</param>
private static void RunThread(DalamudStartInfo info)
/// <param name="mainThreadContinueEvent">Event used to signal the main thread to continue.</param>
private static void RunThread(DalamudStartInfo info, IntPtr mainThreadContinueEvent)
{
if (EnvironmentConfiguration.DalamudWaitForDebugger)
{
while (!Debugger.IsAttached)
{
Thread.Sleep(100);
}
}
// Setup logger
var levelSwitch = InitLogging(info.WorkingDirectory);
var levelSwitch = InitLogging(info.WorkingDirectory, info.BootShowConsole);
// Load configuration first to get some early persistent state, like log level
var configuration = DalamudConfiguration.Load(info.ConfigurationPath);
@ -150,11 +145,12 @@ namespace Dalamud
if (!Util.IsLinux())
InitSymbolHandler(info);
var dalamud = new Dalamud(info, levelSwitch, finishSignal, configuration);
Log.Information("Starting a session..");
var dalamud = new Dalamud(info, levelSwitch, finishSignal, configuration, mainThreadContinueEvent);
Log.Information("This is Dalamud - Core: {GitHash}, CS: {CsGitHash}", Util.GetGitHash(), Util.GetGitHashClientStructs());
// Run session
dalamud.LoadTier1();
dalamud.WaitForUnload();
dalamud.Dispose();
@ -197,7 +193,7 @@ namespace Dalamud
}
}
private static LoggingLevelSwitch InitLogging(string baseDirectory)
private static LoggingLevelSwitch InitLogging(string baseDirectory, bool logConsole)
{
#if DEBUG
var logPath = Path.Combine(baseDirectory, "dalamud.log");
@ -211,11 +207,15 @@ namespace Dalamud
CullLogFile(oldPath, null, 10 * 1024 * 1024);
var levelSwitch = new LoggingLevelSwitch(LogEventLevel.Verbose);
Log.Logger = new LoggerConfiguration()
.WriteTo.Async(a => a.File(logPath))
.WriteTo.Sink(SerilogEventSink.Instance)
.MinimumLevel.ControlledBy(levelSwitch)
.CreateLogger();
var config = new LoggerConfiguration()
.WriteTo.Async(a => a.File(logPath, fileSizeLimitBytes: null, buffered: false, flushToDiskInterval: TimeSpan.FromSeconds(1)))
.WriteTo.Sink(SerilogEventSink.Instance)
.MinimumLevel.ControlledBy(levelSwitch);
if (logConsole)
config = config.WriteTo.Console();
Log.Logger = config.CreateLogger();
return levelSwitch;
}

View file

@ -65,8 +65,8 @@ namespace Dalamud.Game
// };
private readonly Regex rmtRegex = new(
@"4KGOLD|We have sufficient stock|VPK\.OM|Gil for free|www\.so9\.com|Fast & Convenient|Cheap & Safety Guarantee|【Code|A O A U E|igfans|4KGOLD\.COM|Cheapest Gil with|pvp and bank on google|Selling Cheap GIL|ff14mogstation\.com|Cheap Gil 1000k|gilsforyou|server 1000K =|gils_selling|E A S Y\.C O M|bonus code|mins delivery guarantee|Sell cheap|Salegm\.com|cheap Mog|Off Code:|FF14Mog.com|使用する5オ|Off Code( *):|offers Fantasia",
RegexOptions.Compiled);
@"4KGOLD|We have sufficient stock|VPK\.OM|[Gg]il for free|[Gg]il [Cc]heap|5GOLD|www\.so9\.com|Fast & Convenient|Cheap & Safety Guarantee|【Code|A O A U E|igfans|4KGOLD\.COM|Cheapest Gil with|pvp and bank on google|Selling Cheap GIL|ff14mogstation\.com|Cheap Gil 1000k|gilsforyou|server 1000K =|gils_selling|E A S Y\.C O M|bonus code|mins delivery guarantee|Sell cheap|Salegm\.com|cheap Mog|Off Code:|FF14Mog.com|使用する5オ|[Oo][Ff][Ff] [Cc]ode( *)[:;]|offers Fantasia",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
private readonly Dictionary<ClientLanguage, Regex[]> retainerSaleRegexes = new()
{

View file

@ -17,7 +17,7 @@ using Dalamud.Hooking;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Utility;
using Lumina.Excel.GeneratedSheets;
using FFXIVClientStructs.FFXIV.Client.Game;
using Serilog;
namespace Dalamud.Game.ClientState
@ -100,12 +100,12 @@ namespace Dalamud.Game.ClientState
/// <summary>
/// Event that fires when a character is entering PvP.
/// </summary>
public event System.Action EnterPvP;
public event Action EnterPvP;
/// <summary>
/// Event that fires when a character is leaving PvP.
/// </summary>
public event System.Action LeavePvP;
public event Action LeavePvP;
/// <summary>
/// Event that gets fired when a duty is ready.
@ -142,6 +142,11 @@ namespace Dalamud.Game.ClientState
/// </summary>
public bool IsPvP { get; private set; }
/// <summary>
/// Gets a value indicating whether or not the user is playing PvP, excluding the Wolves' Den.
/// </summary>
public bool IsPvPExcludingDen { get; private set; }
/// <summary>
/// Enable this module.
/// </summary>
@ -203,11 +208,8 @@ namespace Dalamud.Game.ClientState
gameGui.ResetUiHideState();
}
if (this.TerritoryType != 0)
{
var terriRow = data.GetExcelSheet<TerritoryType>()!.GetRow(this.TerritoryType);
this.IsPvP = terriRow?.Bg.RawString.StartsWith("ffxiv/pvp") ?? false;
}
this.IsPvP = GameMain.IsInPvPArea();
this.IsPvPExcludingDen = this.IsPvP && this.TerritoryType != 250;
if (this.IsPvP != this.lastFramePvP)
{

View file

@ -36,8 +36,8 @@ namespace Dalamud.Game
private bool tierInitError = false;
private Hook<OnUpdateDetour> updateHook;
private Hook<OnDestroyDetour> destroyHook;
private Hook<OnRealDestroyDelegate> realDestroyHook;
private Hook<OnDestroyDetour> freeHook;
private Hook<OnRealDestroyDelegate> destroyHook;
private Thread? frameworkUpdateThread;
@ -49,14 +49,9 @@ namespace Dalamud.Game
this.Address = new FrameworkAddressResolver();
this.Address.Setup();
Log.Verbose($"Framework address 0x{this.Address.BaseAddress.ToInt64():X}");
if (this.Address.BaseAddress == IntPtr.Zero)
{
throw new InvalidOperationException("Framework is not initalized yet.");
}
// Hook virtual functions
this.HookVTable();
this.updateHook = new Hook<OnUpdateDetour>(this.Address.TickAddress, this.HandleFrameworkUpdate);
this.freeHook = new Hook<OnDestroyDetour>(this.Address.FreeAddress, this.HandleFrameworkFree);
this.destroyHook = new Hook<OnRealDestroyDelegate>(this.Address.DestroyAddress, this.HandleFrameworkDestroy);
}
/// <summary>
@ -138,8 +133,8 @@ namespace Dalamud.Game
Service<GameNetwork>.Get().Enable();
this.updateHook.Enable();
this.freeHook.Enable();
this.destroyHook.Enable();
this.realDestroyHook.Enable();
}
/// <summary>
@ -229,38 +224,18 @@ namespace Dalamud.Game
Service<GameNetwork>.GetNullable()?.ExplicitDispose();
this.updateHook?.Disable();
this.freeHook?.Disable();
this.destroyHook?.Disable();
this.realDestroyHook?.Disable();
Thread.Sleep(500);
this.updateHook?.Dispose();
this.freeHook?.Dispose();
this.destroyHook?.Dispose();
this.realDestroyHook?.Dispose();
this.updateStopwatch.Reset();
statsStopwatch.Reset();
}
private void HookVTable()
{
var vtable = Marshal.ReadIntPtr(this.Address.BaseAddress);
// Virtual function layout:
// .rdata:00000001411F1FE0 dq offset Xiv__Framework___dtor
// .rdata:00000001411F1FE8 dq offset Xiv__Framework__init
// .rdata:00000001411F1FF0 dq offset Xiv__Framework__destroy
// .rdata:00000001411F1FF8 dq offset Xiv__Framework__free
// .rdata:00000001411F2000 dq offset Xiv__Framework__update
var pUpdate = Marshal.ReadIntPtr(vtable, IntPtr.Size * 4);
this.updateHook = new Hook<OnUpdateDetour>(pUpdate, this.HandleFrameworkUpdate);
var pDestroy = Marshal.ReadIntPtr(vtable, IntPtr.Size * 3);
this.destroyHook = new Hook<OnDestroyDetour>(pDestroy, this.HandleFrameworkDestroy);
var pRealDestroy = Marshal.ReadIntPtr(vtable, IntPtr.Size * 2);
this.realDestroyHook = new Hook<OnRealDestroyDelegate>(pRealDestroy, this.HandleRealDestroy);
}
private bool HandleFrameworkUpdate(IntPtr framework)
{
// If any of the tier loads failed, just go to the original code.
@ -371,7 +346,7 @@ namespace Dalamud.Game
return this.updateHook.Original(framework);
}
private bool HandleRealDestroy(IntPtr framework)
private bool HandleFrameworkDestroy(IntPtr framework)
{
if (this.DispatchUpdateEvents)
{
@ -385,15 +360,15 @@ namespace Dalamud.Game
this.DispatchUpdateEvents = false;
return this.realDestroyHook.Original(framework);
return this.destroyHook.Original(framework);
}
private IntPtr HandleFrameworkDestroy()
private IntPtr HandleFrameworkFree()
{
Log.Information("Framework::Free!");
// Store the pointer to the original trampoline location
var originalPtr = Marshal.GetFunctionPointerForDelegate(this.destroyHook.Original);
var originalPtr = Marshal.GetFunctionPointerForDelegate(this.freeHook.Original);
var dalamud = Service<Dalamud>.Get();
dalamud.Unload();

View file

@ -1,55 +1,49 @@
using System;
using System.Runtime.InteropServices;
namespace Dalamud.Game
{
/// <summary>
/// The address resolver for the <see cref="Framework"/> class.
/// </summary>
public sealed class FrameworkAddressResolver : BaseAddressResolver
public sealed unsafe class FrameworkAddressResolver : BaseAddressResolver
{
/// <summary>
/// Gets the base address native Framework class.
/// Gets the base address of the Framework object.
/// </summary>
public IntPtr BaseAddress { get; private set; }
[Obsolete("Please use FFXIVClientStructs.FFXIV.Client.System.Framework.Framework.Instance() instead.")]
public IntPtr BaseAddress => new(FFXIVClientStructs.FFXIV.Client.System.Framework.Framework.Instance());
/// <summary>
/// Gets the address for the native GuiManager class.
/// Gets the address for the function that is called once the Framework is destroyed.
/// </summary>
public IntPtr GuiManager { get; private set; }
public IntPtr DestroyAddress { get; private set; }
/// <summary>
/// Gets the address for the native ScriptManager class.
/// Gets the address for the function that is called once the Framework is free'd.
/// </summary>
public IntPtr ScriptManager { get; private set; }
public IntPtr FreeAddress { get; private set; }
/// <summary>
/// Gets the function that is called every tick.
/// </summary>
public IntPtr TickAddress { get; private set; }
/// <inheritdoc/>
protected override void Setup64Bit(SigScanner sig)
{
this.SetupFramework(sig);
// Xiv__Framework__GetGuiManager+8 000 mov rax, [rcx+2C00h]
// Xiv__Framework__GetGuiManager+F 000 retn
this.GuiManager = Marshal.ReadIntPtr(this.BaseAddress, 0x2C08);
// Called from Framework::Init
this.ScriptManager = this.BaseAddress + 0x2C68; // note that no deref here
}
private void SetupFramework(SigScanner scanner)
{
// Dissasembly of part of the .dtor
// 00007FF701AD665A | 48 C7 05 ?? ?? ?? ?? 00 00 00 00 | MOV QWORD PTR DS:[g_mainFramework],0
// 00007FF701AD6665 | E8 ?? ?? ?? ?? | CALL ffxiv_dx11.7FF701E27130
// 00007FF701AD666A | 48 8D ?? ?? ?? 00 00 | LEA RCX,QWORD PTR DS:[RBX + 2C38]
// 00007FF701AD6671 | E8 ?? ?? ?? ?? | CALL ffxiv_dx11.7FF701E2A7D0
// 00007FF701AD6676 | 48 8D ?? ?? ?? ?? ?? | LEA RAX,QWORD PTR DS:[7FF702C31F80
var fwDtor = scanner.ScanText("48 C7 05 ?? ?? ?? ?? 00 00 00 00 E8 ?? ?? ?? ?? 48 8D ?? ?? ?? 00 00 E8 ?? ?? ?? ?? 48 8D");
var fwOffset = Marshal.ReadInt32(fwDtor + 3);
var pFramework = scanner.ResolveRelativeAddress(fwDtor + 11, fwOffset);
this.DestroyAddress =
scanner.ScanText("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 48 8B 3D ?? ?? ?? ?? 48 8B D9 48 85 FF");
// Framework does not change once initialized in startup so don't bother to deref again and again.
this.BaseAddress = Marshal.ReadIntPtr(pFramework);
this.FreeAddress =
scanner.ScanText("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 48 8B D9 48 8B 0D ?? ?? ?? ??");
this.TickAddress =
scanner.ScanText("40 53 48 83 EC 20 FF 81 ?? ?? ?? ?? 48 8B D9 48 8D 4C 24 ??");
}
}
}

View file

@ -38,13 +38,11 @@ namespace Dalamud.Game.Gui
/// Initializes a new instance of the <see cref="ChatGui"/> class.
/// </summary>
/// <param name="baseAddress">The base address of the ChatManager.</param>
internal ChatGui(IntPtr baseAddress)
internal ChatGui()
{
this.address = new ChatGuiAddressResolver(baseAddress);
this.address = new ChatGuiAddressResolver();
this.address.Setup();
Log.Verbose($"Chat manager address 0x{this.address.BaseAddress.ToInt64():X}");
this.printMessageHook = new Hook<PrintMessageDelegate>(this.address.PrintMessage, this.HandlePrintMessageDetour);
this.populateItemLinkHook = new Hook<PopulateItemLinkDelegate>(this.address.PopulateItemLinkObject, this.HandlePopulateItemLinkDetour);
this.interactableLinkClickedHook = new Hook<InteractableLinkClickedDelegate>(this.address.InteractableLinkClicked, this.InteractableLinkClickedDetour);

View file

@ -7,20 +7,6 @@ namespace Dalamud.Game.Gui
/// </summary>
public sealed class ChatGuiAddressResolver : BaseAddressResolver
{
/// <summary>
/// Initializes a new instance of the <see cref="ChatGuiAddressResolver"/> class.
/// </summary>
/// <param name="baseAddress">The base address of the native ChatManager class.</param>
public ChatGuiAddressResolver(IntPtr baseAddress)
{
this.BaseAddress = baseAddress;
}
/// <summary>
/// Gets the base address of the native ChatManager class.
/// </summary>
public IntPtr BaseAddress { get; }
/// <summary>
/// Gets the address of the native PrintMessage method.
/// </summary>

View file

@ -62,7 +62,7 @@ namespace Dalamud.Game.Gui
Log.Verbose($"HandleItemOut address 0x{this.address.HandleItemOut.ToInt64():X}");
Log.Verbose($"HandleImm address 0x{this.address.HandleImm.ToInt64():X}");
Service<ChatGui>.Set(new ChatGui(this.address.ChatManager));
Service<ChatGui>.Set();
Service<PartyFinderGui>.Set();
Service<ToastGui>.Set();
Service<FlyTextGui>.Set();

View file

@ -7,24 +7,11 @@ namespace Dalamud.Game.Gui
/// </summary>
internal sealed class GameGuiAddressResolver : BaseAddressResolver
{
/// <summary>
/// Initializes a new instance of the <see cref="GameGuiAddressResolver"/> class.
/// </summary>
public GameGuiAddressResolver()
{
this.BaseAddress = Service<Framework>.Get().Address.BaseAddress;
}
/// <summary>
/// Gets the base address of the native GuiManager class.
/// </summary>
public IntPtr BaseAddress { get; private set; }
/// <summary>
/// Gets the address of the native ChatManager class.
/// </summary>
public IntPtr ChatManager { get; private set; }
/// <summary>
/// Gets the address of the native SetGlobalBgm method.
/// </summary>
@ -89,13 +76,5 @@ namespace Dalamud.Game.Gui
this.ToggleUiHide = sig.ScanText("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 0F B6 B9 ?? ?? ?? ?? B8 ?? ?? ?? ??");
this.Utf8StringFromSequence = sig.ScanText("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 48 8D 41 22 66 C7 41 ?? ?? ?? 48 89 01 49 8B D8");
}
/// <inheritdoc/>
protected override void SetupInternal(SigScanner scanner)
{
// Xiv__UiManager__GetChatManager 000 lea rax, [rcx+13E0h]
// Xiv__UiManager__GetChatManager+7 000 retn
this.ChatManager = this.BaseAddress + 0x13E0;
}
}
}

View file

@ -1,8 +1,12 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Numerics;
using System.Runtime.InteropServices;
using System.Text;
using Dalamud.Hooking;
using Dalamud.Interface.Internal;
using Dalamud.Logging.Internal;
using ImGuiNET;
@ -14,7 +18,7 @@ namespace Dalamud.Game.Gui.Internal
/// <summary>
/// This class handles IME for non-English users.
/// </summary>
internal class DalamudIME : IDisposable
internal unsafe class DalamudIME : IDisposable
{
private static readonly ModuleLog Log = new("IME");
@ -22,6 +26,8 @@ namespace Dalamud.Game.Gui.Internal
private IntPtr wndProcPtr;
private IntPtr oldWndProcPtr;
private WndProcDelegate wndProcDelegate;
private AsmHook imguiTextInputCursorHook;
private Vector2* cursorPos;
/// <summary>
/// Initializes a new instance of the <see cref="DalamudIME"/> class.
@ -60,6 +66,18 @@ namespace Dalamud.Game.Gui.Internal
SetWindowLongPtrW(this.interfaceHandle, WindowLongType.WndProc, this.oldWndProcPtr);
this.oldWndProcPtr = IntPtr.Zero;
}
this.imguiTextInputCursorHook?.Dispose();
Marshal.FreeHGlobal((IntPtr)this.cursorPos);
}
/// <summary>
/// Get the position of the cursor.
/// </summary>
/// <returns>The position of the cursor.</returns>
internal Vector2 GetCursorPos()
{
return new Vector2(this.cursorPos->X, this.cursorPos->Y);
}
/// <summary>
@ -73,6 +91,31 @@ namespace Dalamud.Game.Gui.Internal
this.interfaceHandle = Service<InterfaceManager>.Get().WindowHandlePtr;
this.wndProcPtr = Marshal.GetFunctionPointerForDelegate(this.wndProcDelegate);
this.oldWndProcPtr = SetWindowLongPtrW(this.interfaceHandle, WindowLongType.WndProc, this.wndProcPtr);
var module = Process.GetCurrentProcess().Modules.Cast<ProcessModule>().First(m => m.ModuleName == "cimgui.dll");
var scanner = new SigScanner(module);
var cursorDrawingPtr = scanner.ScanModule("F3 0F 11 75 ?? 0F 28 CF");
Log.Debug($"Found cursorDrawingPtr at {cursorDrawingPtr:X}");
this.cursorPos = (Vector2*)Marshal.AllocHGlobal(sizeof(Vector2));
this.cursorPos->X = 0f;
this.cursorPos->Y = 0f;
var asm = new[]
{
"use64",
$"push rax",
$"mov rax, {(IntPtr)this.cursorPos + sizeof(float)}",
$"movss [rax],xmm7",
$"mov rax, {(IntPtr)this.cursorPos}",
$"movss [rax],xmm6",
$"pop rax",
};
Log.Debug($"Asm Code:\n{string.Join("\n", asm)}");
this.imguiTextInputCursorHook = new AsmHook(cursorDrawingPtr, asm, "ImguiTextInputCursorHook");
this.imguiTextInputCursorHook?.Enable();
this.IsEnabled = true;
Log.Information("Enabled!");
}

View file

@ -9,6 +9,7 @@ using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Hooking;
using Dalamud.Interface;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Windowing;
using FFXIVClientStructs.FFXIV.Component.GUI;
@ -90,8 +91,9 @@ namespace Dalamud.Game.Internal
{
var systemText = Service<DataManager>.Get().GetExcelSheet<Addon>()!.GetRow(1059)!.Text.RawString; // "System"
var configuration = Service<DalamudConfiguration>.Get();
var interfaceManager = Service<InterfaceManager>.Get();
if (args.Title == systemText && configuration.DoButtonsSystemMenu)
if (args.Title == systemText && configuration.DoButtonsSystemMenu && interfaceManager.IsDispatchingEvents)
{
var dalamudInterface = Service<DalamudInterface>.Get();
@ -131,8 +133,9 @@ namespace Dalamud.Game.Internal
}
var configuration = Service<DalamudConfiguration>.Get();
var interfaceManager = Service<InterfaceManager>.Get();
if (!configuration.DoButtonsSystemMenu)
if (!configuration.DoButtonsSystemMenu || !interfaceManager.IsDispatchingEvents)
{
this.hookAgentHudOpenSystemMenu.Original(thisPtr, atkValueArgs, menuSize);
return;

View file

@ -2,11 +2,13 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Newtonsoft.Json;
using Serilog;
namespace Dalamud.Game
@ -16,17 +18,22 @@ namespace Dalamud.Game
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
public sealed class SigScanner : IDisposable
public class SigScanner : IDisposable
{
private readonly FileInfo? cacheFile;
private IntPtr moduleCopyPtr;
private long moduleCopyOffset;
private Dictionary<string, long>? textCache;
/// <summary>
/// Initializes a new instance of the <see cref="SigScanner"/> class using the main module of the current process.
/// </summary>
/// <param name="doCopy">Whether or not to copy the module upon initialization for search operations to use, as to not get disturbed by possible hooks.</param>
public SigScanner(bool doCopy = false)
: this(Process.GetCurrentProcess().MainModule!, doCopy)
/// <param name="cacheFile">File used to cached signatures.</param>
public SigScanner(bool doCopy = false, FileInfo? cacheFile = null)
: this(Process.GetCurrentProcess().MainModule!, doCopy, cacheFile)
{
}
@ -35,8 +42,10 @@ namespace Dalamud.Game
/// </summary>
/// <param name="module">The ProcessModule to be used for scanning.</param>
/// <param name="doCopy">Whether or not to copy the module upon initialization for search operations to use, as to not get disturbed by possible hooks.</param>
public SigScanner(ProcessModule module, bool doCopy = false)
/// <param name="cacheFile">File used to cached signatures.</param>
public SigScanner(ProcessModule module, bool doCopy = false, FileInfo? cacheFile = null)
{
this.cacheFile = cacheFile;
this.Module = module;
this.Is32BitProcess = !Environment.Is64BitProcess;
this.IsCopy = doCopy;
@ -49,6 +58,9 @@ namespace Dalamud.Game
Log.Verbose($"Module base: 0x{this.TextSectionBase.ToInt64():X}");
Log.Verbose($"Module size: 0x{this.TextSectionSize:X}");
if (cacheFile != null)
this.Load();
}
/// <summary>
@ -294,8 +306,12 @@ namespace Dalamud.Game
/// <returns>The real offset of the found signature.</returns>
public IntPtr ScanText(string signature)
{
var mBase = this.IsCopy ? this.moduleCopyPtr : this.TextSectionBase;
if (this.textCache != null && this.textCache.TryGetValue(signature, out var address))
{
return new IntPtr(address + this.Module.BaseAddress.ToInt64());
}
var mBase = this.IsCopy ? this.moduleCopyPtr : this.TextSectionBase;
var scanRet = Scan(mBase, this.TextSectionSize, signature);
if (this.IsCopy)
@ -304,7 +320,9 @@ namespace Dalamud.Game
var insnByte = Marshal.ReadByte(scanRet);
if (insnByte == 0xE8 || insnByte == 0xE9)
return ReadJmpCallSig(scanRet);
scanRet = ReadJmpCallSig(scanRet);
this.textCache?.Add(signature, scanRet.ToInt64() - this.Module.BaseAddress.ToInt64());
return scanRet;
}
@ -337,6 +355,17 @@ namespace Dalamud.Game
Marshal.FreeHGlobal(this.moduleCopyPtr);
}
/// <summary>
/// Save the current state of the cache.
/// </summary>
internal void Save()
{
if (this.cacheFile == null)
return;
File.WriteAllText(this.cacheFile.FullName, JsonConvert.SerializeObject(this.textCache));
}
/// <summary>
/// Helper for ScanText to get the correct address for IDA sigs that mark the first JMP or CALL location.
/// </summary>
@ -479,5 +508,16 @@ namespace Dalamud.Game
this.moduleCopyOffset = this.moduleCopyPtr.ToInt64() - this.Module.BaseAddress.ToInt64();
}
private void Load()
{
if (this.cacheFile is not { Exists: true })
{
this.textCache = new();
return;
}
this.textCache = JsonConvert.DeserializeObject<Dictionary<string, long>>(File.ReadAllText(this.cacheFile.FullName)) ?? new Dictionary<string, long>();
}
}
}

View file

@ -9,29 +9,31 @@ namespace Dalamud.Interface.GameFonts
/// </summary>
public class FdtReader
{
private static unsafe T StructureFromByteArray<T> (byte[] data, int offset)
{
var len = Marshal.SizeOf<T>();
if (offset + len > data.Length)
throw new Exception("Data too short");
fixed (byte* ptr = data)
return Marshal.PtrToStructure<T>(new(ptr + offset));
}
/// <summary>
/// Initializes a new instance of the <see cref="FdtReader"/> class.
/// </summary>
/// <param name="data">Content of a FDT file.</param>
public FdtReader(byte[] data)
{
unsafe
{
fixed (byte* ptr = data)
{
this.FileHeader = *(FdtHeader*)ptr;
this.FontHeader = *(FontTableHeader*)(ptr + this.FileHeader.FontTableHeaderOffset);
this.KerningHeader = *(KerningTableHeader*)(ptr + this.FileHeader.KerningTableHeaderOffset);
this.FileHeader = StructureFromByteArray<FdtHeader>(data, 0);
this.FontHeader = StructureFromByteArray<FontTableHeader>(data, this.FileHeader.FontTableHeaderOffset);
this.KerningHeader = StructureFromByteArray<KerningTableHeader>(data, this.FileHeader.KerningTableHeaderOffset);
var glyphs = (FontTableEntry*)(ptr + this.FileHeader.FontTableHeaderOffset + Marshal.SizeOf(this.FontHeader));
for (var i = 0; i < this.FontHeader.FontTableEntryCount; i++)
this.Glyphs.Add(glyphs[i]);
for (var i = 0; i < this.FontHeader.FontTableEntryCount; i++)
this.Glyphs.Add(StructureFromByteArray<FontTableEntry>(data, this.FileHeader.FontTableHeaderOffset + Marshal.SizeOf<FontTableHeader>() + (Marshal.SizeOf<FontTableEntry>() * i)));
var kerns = (KerningTableEntry*)(ptr + this.FileHeader.KerningTableHeaderOffset + Marshal.SizeOf(this.KerningHeader));
for (var i = 0; i < this.FontHeader.KerningTableEntryCount; i++)
this.Distances.Add(kerns[i]);
}
}
for (int i = 0, i_ = Math.Min(this.FontHeader.KerningTableEntryCount, this.KerningHeader.Count); i < i_; i++)
this.Distances.Add(StructureFromByteArray<KerningTableEntry>(data, this.FileHeader.KerningTableHeaderOffset+ Marshal.SizeOf<KerningTableHeader>() + (Marshal.SizeOf<KerningTableEntry>() * i)));
}
/// <summary>

View file

@ -7,7 +7,7 @@ using System.Text;
using Dalamud.Data;
using Dalamud.Interface.Internal;
using Dalamud.Utility;
using Dalamud.Utility.Timing;
using ImGuiNET;
using Lumina.Data.Files;
using Serilog;
@ -39,7 +39,7 @@ namespace Dalamud.Interface.GameFonts
private readonly Dictionary<GameFontStyle, int> fontUseCounter = new();
private readonly Dictionary<GameFontStyle, Dictionary<char, Tuple<int, FdtReader.FontTableEntry>>> glyphRectIds = new();
private bool isBetweenBuildFontsAndAfterBuildFonts = false;
private bool isBetweenBuildFontsAndRightAfterImGuiIoFontsBuild = false;
private bool isBuildingAsFallbackFontMode = false;
/// <summary>
@ -49,12 +49,31 @@ namespace Dalamud.Interface.GameFonts
{
var dataManager = Service<DataManager>.Get();
this.fdts = FontNames.Select(fontName =>
using (Timings.Start("Load FDTs"))
{
var file = fontName == null ? null : dataManager.GetFile($"common/font/{fontName}.fdt");
return file == null ? null : new FdtReader(file!.Data);
}).ToArray();
this.texturePixels = Enumerable.Range(1, 1 + this.fdts.Where(x => x != null).Select(x => x.Glyphs.Select(x => x.TextureFileIndex).Max()).Max()).Select(x => dataManager.GameData.GetFile<TexFile>($"common/font/font{x}.tex").ImageData).ToList();
this.fdts = FontNames.Select(fontName =>
{
var fileName = $"common/font/{fontName}.fdt";
using (Timings.Start($"Loading FDT: {fileName}"))
{
var file = fontName == null ? null : dataManager.GetFile(fileName);
return file == null ? null : new FdtReader(file!.Data);
}
}).ToArray();
}
using (Timings.Start("Getting texture data"))
{
this.texturePixels = Enumerable.Range(1, 1 + this.fdts.Where(x => x != null).Select(x => x.Glyphs.Select(x => x.TextureFileIndex).Max()).Max()).Select(
x =>
{
var fileName = $"common/font/font{x}.tex";
using (Timings.Start($"Get tex: {fileName}"))
{
return dataManager.GameData.GetFile<TexFile>(fileName)!.ImageData;
}
}).ToList();
}
this.interfaceManager = Service<InterfaceManager>.Get();
}
@ -174,7 +193,7 @@ namespace Dalamud.Interface.GameFonts
needRebuild = !this.fonts.ContainsKey(style);
if (needRebuild)
{
if (Service<InterfaceManager>.Get().IsBuildingFontsBeforeAtlasBuild && this.isBetweenBuildFontsAndAfterBuildFonts)
if (Service<InterfaceManager>.Get().IsBuildingFontsBeforeAtlasBuild && this.isBetweenBuildFontsAndRightAfterImGuiIoFontsBuild)
{
Log.Information("[GameFontManager] NewFontRef: Building {0} right now, as it is called while BuildFonts is already in progress yet atlas build has not been called yet.", style.ToString());
this.EnsureFont(style);
@ -246,7 +265,7 @@ namespace Dalamud.Interface.GameFonts
public void BuildFonts(bool forceMinSize)
{
this.isBuildingAsFallbackFontMode = forceMinSize;
this.isBetweenBuildFontsAndAfterBuildFonts = true;
this.isBetweenBuildFontsAndRightAfterImGuiIoFontsBuild = true;
this.glyphRectIds.Clear();
this.fonts.Clear();
@ -255,6 +274,21 @@ namespace Dalamud.Interface.GameFonts
this.EnsureFont(style);
}
/// <summary>
/// Record that ImGui.GetIO().Fonts.Build() has been called.
/// </summary>
public void AfterIoFontsBuild()
{
this.isBetweenBuildFontsAndRightAfterImGuiIoFontsBuild = false;
}
/// <summary>
/// Checks whether GameFontMamager owns an ImFont.
/// </summary>
/// <param name="fontPtr">ImFontPtr to check.</param>
/// <returns>Whether it owns.</returns>
public bool OwnsFont(ImFontPtr fontPtr) => this.fonts.ContainsValue(fontPtr);
/// <summary>
/// Post-build fonts before plugins do something more. To be called from InterfaceManager.
/// </summary>
@ -270,6 +304,9 @@ namespace Dalamud.Interface.GameFonts
var fdt = this.fdts[(int)(this.isBuildingAsFallbackFontMode ? style.FamilyWithMinimumSize : style.FamilyAndSize)];
var scale = style.SizePt / fdt.FontHeader.Size;
var fontPtr = font.NativePtr;
Log.Verbose("[GameFontManager] AfterBuildFonts: Scaling {0} from {1}pt to {2}pt (scale: {3})", style.ToString(), fdt.FontHeader.Size, style.SizePt, scale);
fontPtr->FontSize = fdt.FontHeader.Size * 4 / 3;
if (fontPtr->ConfigData != null)
fontPtr->ConfigData->SizePixels = fontPtr->FontSize;
@ -364,12 +401,8 @@ namespace Dalamud.Interface.GameFonts
}
}
ImGuiHelpers.CopyGlyphsAcrossFonts(InterfaceManager.DefaultFont, font, true, false);
UnscaleFont(font, 1 / scale, false);
font.BuildLookupTable();
}
this.isBetweenBuildFontsAndAfterBuildFonts = false;
}
/// <summary>

View file

@ -0,0 +1,66 @@
using System;
using System.Numerics;
using System.Text;
using ImGuiNET;
namespace Dalamud.Interface;
public static class ImGuiExtensions
{
public static void AddTextClippedEx(
this ImDrawListPtr drawListPtr, Vector2 posMin, Vector2 posMax, string text, Vector2? textSizeIfKnown,
Vector2 align, Vector4? clipRect)
{
var pos = posMin;
var textSize = textSizeIfKnown ?? ImGui.CalcTextSize(text, false, 0);
var clipMin = clipRect.HasValue ? new Vector2(clipRect.Value.X, clipRect.Value.Y) : posMin;
var clipMax = clipRect.HasValue ? new Vector2(clipRect.Value.Z, clipRect.Value.W) : posMax;
var needClipping = (pos.X + textSize.X >= clipMax.X) || (pos.Y + textSize.Y >= clipMax.Y);
if (clipRect.HasValue)
needClipping |= (pos.X < clipMin.X) || (pos.Y < clipMin.Y);
if (align.X > 0)
{
pos.X = Math.Max(pos.X, pos.X + ((posMax.X - pos.X - textSize.X) * align.X));
}
if (align.Y > 0)
{
pos.Y = Math.Max(pos.Y, pos.Y + ((posMax.Y - pos.Y - textSize.Y) * align.Y));
}
if (needClipping)
{
var fineClipRect = new Vector4(clipMin.X, clipMin.Y, clipMax.X, clipMax.Y);
drawListPtr.AddText(ImGui.GetFont(), ImGui.GetFontSize(), pos, ImGui.GetColorU32(ImGuiCol.Text), text, ref fineClipRect);
}
else
{
drawListPtr.AddText(ImGui.GetFont(), ImGui.GetFontSize(), pos, ImGui.GetColorU32(ImGuiCol.Text), text);
}
}
// TODO: This should go into ImDrawList.Manual.cs in ImGui.NET...
public static unsafe void AddText(this ImDrawListPtr drawListPtr, ImFontPtr font, float fontSize, Vector2 pos, uint col, string textBegin, ref Vector4 cpuFineClipRect)
{
var nativeFont = font.NativePtr;
var textBeginByteCount = Encoding.UTF8.GetByteCount(textBegin);
var nativeTextBegin = stackalloc byte[textBeginByteCount + 1];
fixed (char* textBeginPtr = textBegin)
{
var nativeTextBeginOffset = Encoding.UTF8.GetBytes(textBeginPtr, textBegin.Length, nativeTextBegin, textBeginByteCount);
nativeTextBegin[nativeTextBeginOffset] = 0;
}
byte* nativeTextEnd = null;
var wrapWidth = 0.0f;
fixed (Vector4* nativeCpuFineClipRect = &cpuFineClipRect)
{
ImGuiNative.ImDrawList_AddText_FontPtr(drawListPtr.NativePtr, nativeFont, fontSize, pos, col, nativeTextBegin, nativeTextEnd, wrapWidth, nativeCpuFineClipRect);
}
}
}

View file

@ -298,70 +298,25 @@ namespace Dalamud.Interface.ImGuiFileDialog
private void SetupSideBar()
{
var drives = DriveInfo.GetDrives();
foreach (var drive in drives)
foreach (var drive in DriveInfo.GetDrives())
{
this.drives.Add(new SideBarItem
{
Icon = (char)FontAwesomeIcon.Server,
Location = drive.Name,
Text = drive.Name,
});
this.drives.Add(new SideBarItem(drive.Name, drive.Name, FontAwesomeIcon.Server));
}
var personal = Path.GetDirectoryName(Environment.GetFolderPath(Environment.SpecialFolder.Personal));
this.quickAccess.Add(new SideBarItem
{
Icon = (char)FontAwesomeIcon.Desktop,
Location = Environment.GetFolderPath(Environment.SpecialFolder.Desktop),
Text = "Desktop",
});
this.quickAccess.Add(new SideBarItem
{
Icon = (char)FontAwesomeIcon.File,
Location = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),
Text = "Documents",
});
this.quickAccess.Add(new SideBarItem("Desktop", Environment.GetFolderPath(Environment.SpecialFolder.Desktop), FontAwesomeIcon.Desktop));
this.quickAccess.Add(new SideBarItem("Documents", Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), FontAwesomeIcon.File));
if (!string.IsNullOrEmpty(personal))
{
this.quickAccess.Add(new SideBarItem
{
Icon = (char)FontAwesomeIcon.Download,
Location = Path.Combine(personal, "Downloads"),
Text = "Downloads",
});
this.quickAccess.Add(new SideBarItem("Downloads", Path.Combine(personal, "Downloads"), FontAwesomeIcon.Download));
}
this.quickAccess.Add(new SideBarItem
{
Icon = (char)FontAwesomeIcon.Star,
Location = Environment.GetFolderPath(Environment.SpecialFolder.Favorites),
Text = "Favorites",
});
this.quickAccess.Add(new SideBarItem
{
Icon = (char)FontAwesomeIcon.Music,
Location = Environment.GetFolderPath(Environment.SpecialFolder.MyMusic),
Text = "Music",
});
this.quickAccess.Add(new SideBarItem
{
Icon = (char)FontAwesomeIcon.Image,
Location = Environment.GetFolderPath(Environment.SpecialFolder.MyPictures),
Text = "Pictures",
});
this.quickAccess.Add(new SideBarItem
{
Icon = (char)FontAwesomeIcon.Video,
Location = Environment.GetFolderPath(Environment.SpecialFolder.MyVideos),
Text = "Videos",
});
this.quickAccess.Add(new SideBarItem("Favorites", Environment.GetFolderPath(Environment.SpecialFolder.Favorites), FontAwesomeIcon.Star));
this.quickAccess.Add(new SideBarItem("Music", Environment.GetFolderPath(Environment.SpecialFolder.MyMusic), FontAwesomeIcon.Music));
this.quickAccess.Add(new SideBarItem("Pictures", Environment.GetFolderPath(Environment.SpecialFolder.MyPictures), FontAwesomeIcon.Image));
this.quickAccess.Add(new SideBarItem("Videos", Environment.GetFolderPath(Environment.SpecialFolder.MyVideos), FontAwesomeIcon.Video));
}
private void SortFields(SortingField sortingField, bool canChangeOrder = false)

View file

@ -1,5 +1,7 @@
using System.Collections.Generic;
using System.IO;
using System.Numerics;
using Dalamud.Utility;
namespace Dalamud.Interface.ImGuiFileDialog
{
@ -19,11 +21,26 @@ namespace Dalamud.Interface.ImGuiFileDialog
public string FileModifiedDate;
}
private struct SideBarItem
private readonly struct SideBarItem
{
public char Icon;
public string Text;
public string Location;
public SideBarItem(string text, string location, FontAwesomeIcon icon)
{
this.Text = text;
this.Location = location;
this.Icon = icon;
this.Exists = !this.Location.IsNullOrEmpty() && Directory.Exists(this.Location);
}
public string Text { get; init; }
public string Location { get; init; }
public FontAwesomeIcon Icon { get; init; }
public bool Exists { get; init; }
public bool CheckExistence()
=> !this.Location.IsNullOrEmpty() && Directory.Exists(this.Location);
}
private struct FilterStruct
@ -50,7 +67,7 @@ namespace Dalamud.Interface.ImGuiFileDialog
private struct IconColorItem
{
public char Icon;
public FontAwesomeIcon Icon;
public Vector4 Color;
}
}

View file

@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Numerics;
using ImGuiNET;
@ -43,11 +44,11 @@ namespace Dalamud.Interface.ImGuiFileDialog
if (this.isModal && !this.okResultToConfirm)
{
ImGui.OpenPopup(name);
windowVisible = ImGui.BeginPopupModal(name, ref this.visible, ImGuiWindowFlags.NoScrollbar);
windowVisible = ImGui.BeginPopupModal(name, ref this.visible, this.WindowFlags);
}
else
{
windowVisible = ImGui.Begin(name, ref this.visible, ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoNav);
windowVisible = ImGui.Begin(name, ref this.visible, this.WindowFlags);
}
bool wasClosed = false;
@ -99,7 +100,7 @@ namespace Dalamud.Interface.ImGuiFileDialog
return wasClosed || this.ConfirmOrOpenOverWriteFileDialogIfNeeded(res);
}
private static void AddToIconMap(string[] extensions, char icon, Vector4 color)
private static void AddToIconMap(string[] extensions, FontAwesomeIcon icon, Vector4 color)
{
foreach (var ext in extensions)
{
@ -116,19 +117,19 @@ namespace Dalamud.Interface.ImGuiFileDialog
if (iconMap == null)
{
iconMap = new();
AddToIconMap(new[] { "mp4", "gif", "mov", "avi" }, (char)FontAwesomeIcon.FileVideo, miscTextColor);
AddToIconMap(new[] { "pdf" }, (char)FontAwesomeIcon.FilePdf, miscTextColor);
AddToIconMap(new[] { "png", "jpg", "jpeg", "tiff" }, (char)FontAwesomeIcon.FileImage, imageTextColor);
AddToIconMap(new[] { "cs", "json", "cpp", "h", "py", "xml", "yaml", "js", "html", "css", "ts", "java" }, (char)FontAwesomeIcon.FileCode, codeTextColor);
AddToIconMap(new[] { "txt", "md" }, (char)FontAwesomeIcon.FileAlt, standardTextColor);
AddToIconMap(new[] { "zip", "7z", "gz", "tar" }, (char)FontAwesomeIcon.FileArchive, miscTextColor);
AddToIconMap(new[] { "mp3", "m4a", "ogg", "wav" }, (char)FontAwesomeIcon.FileAudio, miscTextColor);
AddToIconMap(new[] { "csv" }, (char)FontAwesomeIcon.FileCsv, miscTextColor);
AddToIconMap(new[] { "mp4", "gif", "mov", "avi" }, FontAwesomeIcon.FileVideo, miscTextColor);
AddToIconMap(new[] { "pdf" }, FontAwesomeIcon.FilePdf, miscTextColor);
AddToIconMap(new[] { "png", "jpg", "jpeg", "tiff" }, FontAwesomeIcon.FileImage, imageTextColor);
AddToIconMap(new[] { "cs", "json", "cpp", "h", "py", "xml", "yaml", "js", "html", "css", "ts", "java" }, FontAwesomeIcon.FileCode, codeTextColor);
AddToIconMap(new[] { "txt", "md" }, FontAwesomeIcon.FileAlt, standardTextColor);
AddToIconMap(new[] { "zip", "7z", "gz", "tar" }, FontAwesomeIcon.FileArchive, miscTextColor);
AddToIconMap(new[] { "mp3", "m4a", "ogg", "wav" }, FontAwesomeIcon.FileAudio, miscTextColor);
AddToIconMap(new[] { "csv" }, FontAwesomeIcon.FileCsv, miscTextColor);
}
return iconMap.TryGetValue(ext.ToLower(), out var icon) ? icon : new IconColorItem
{
Icon = (char)FontAwesomeIcon.File,
Icon = FontAwesomeIcon.File,
Color = standardTextColor,
};
}
@ -147,7 +148,7 @@ namespace Dalamud.Interface.ImGuiFileDialog
private void DrawPathComposer()
{
ImGui.PushFont(UiBuilder.IconFont);
if (ImGui.Button($"{(this.pathInputActivated ? (char)FontAwesomeIcon.Times : (char)FontAwesomeIcon.Edit)}"))
if (ImGui.Button(this.pathInputActivated ? FontAwesomeIcon.Times.ToIconString() : FontAwesomeIcon.Edit.ToIconString()))
{
this.pathInputActivated = !this.pathInputActivated;
}
@ -158,13 +159,10 @@ namespace Dalamud.Interface.ImGuiFileDialog
if (this.pathDecomposition.Count > 0)
{
ImGui.SameLine();
if (this.pathInputActivated)
{
ImGui.PushItemWidth(ImGui.GetContentRegionAvail().X);
ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X);
ImGui.InputText("##pathedit", ref this.pathInputBuffer, 255);
ImGui.PopItemWidth();
}
else
{
@ -203,7 +201,7 @@ namespace Dalamud.Interface.ImGuiFileDialog
private void DrawSearchBar()
{
ImGui.PushFont(UiBuilder.IconFont);
if (ImGui.Button($"{(char)FontAwesomeIcon.Home}"))
if (ImGui.Button(FontAwesomeIcon.Home.ToIconString()))
{
this.SetPath(".");
}
@ -222,12 +220,10 @@ namespace Dalamud.Interface.ImGuiFileDialog
if (!this.createDirectoryMode)
{
ImGui.SameLine();
ImGui.Text("Search :");
ImGui.TextUnformatted("Search :");
ImGui.SameLine();
ImGui.PushItemWidth(ImGui.GetContentRegionAvail().X);
var edited = ImGui.InputText("##InputImGuiFileDialogSearchField", ref this.searchBuffer, 255);
ImGui.PopItemWidth();
if (edited)
ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X);
if (ImGui.InputText("##InputImGuiFileDialogSearchField", ref this.searchBuffer, 255))
{
this.ApplyFilteringOnFileList();
}
@ -239,13 +235,10 @@ namespace Dalamud.Interface.ImGuiFileDialog
if (this.flags.HasFlag(ImGuiFileDialogFlags.DisableCreateDirectoryButton)) return;
ImGui.PushFont(UiBuilder.IconFont);
if (ImGui.Button($"{(char)FontAwesomeIcon.FolderPlus}"))
if (ImGui.Button(FontAwesomeIcon.FolderPlus.ToIconString()) && !this.createDirectoryMode)
{
if (!this.createDirectoryMode)
{
this.createDirectoryMode = true;
this.createDirectoryBuffer = string.Empty;
}
this.createDirectoryMode = true;
this.createDirectoryBuffer = string.Empty;
}
ImGui.PopFont();
@ -258,12 +251,11 @@ namespace Dalamud.Interface.ImGuiFileDialog
if (this.createDirectoryMode)
{
ImGui.SameLine();
ImGui.Text("New Directory Name");
ImGui.TextUnformatted("New Directory Name");
ImGui.SameLine();
ImGui.PushItemWidth(ImGui.GetContentRegionAvail().X - 100f);
ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X - 100f);
ImGui.InputText("##DirectoryFileName", ref this.createDirectoryBuffer, 255);
ImGui.PopItemWidth();
ImGui.SameLine();
@ -292,17 +284,20 @@ namespace Dalamud.Interface.ImGuiFileDialog
if (!this.flags.HasFlag(ImGuiFileDialogFlags.HideSideBar))
{
ImGui.BeginChild("##FileDialog_ColumnChild", size);
ImGui.Columns(2, "##FileDialog_Columns");
if (ImGui.BeginChild("##FileDialog_ColumnChild", size))
{
ImGui.Columns(2, "##FileDialog_Columns");
this.DrawSideBar(new Vector2(150, size.Y));
this.DrawSideBar(size with { X = 150 });
ImGui.SetColumnWidth(0, 150);
ImGui.NextColumn();
ImGui.SetColumnWidth(0, 150);
ImGui.NextColumn();
this.DrawFileListView(size - new Vector2(160, 0));
this.DrawFileListView(size - new Vector2(160, 0));
ImGui.Columns(1);
}
ImGui.Columns(1);
ImGui.EndChild();
}
else
@ -313,45 +308,30 @@ namespace Dalamud.Interface.ImGuiFileDialog
private void DrawSideBar(Vector2 size)
{
ImGui.BeginChild("##FileDialog_SideBar", size);
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + 5);
foreach (var drive in this.drives)
if (ImGui.BeginChild("##FileDialog_SideBar", size))
{
ImGui.PushFont(UiBuilder.IconFont);
if (ImGui.Selectable($"{drive.Icon}##{drive.Text}", drive.Text == this.selectedSideBar))
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + 5);
var idx = 0;
foreach (var qa in this.drives.Concat(this.quickAccess).Where(qa => qa.Exists))
{
this.SetPath(drive.Location);
this.selectedSideBar = drive.Text;
ImGui.PushID(idx++);
ImGui.SetCursorPosX(25);
if (ImGui.Selectable(qa.Text, qa.Text == this.selectedSideBar) && qa.CheckExistence())
{
this.SetPath(qa.Location);
this.selectedSideBar = qa.Text;
}
ImGui.PushFont(UiBuilder.IconFont);
ImGui.SameLine();
ImGui.SetCursorPosX(0);
ImGui.TextUnformatted(qa.Icon.ToIconString());
ImGui.PopFont();
ImGui.PopID();
}
ImGui.PopFont();
ImGui.SameLine(25);
ImGui.Text(drive.Text);
}
foreach (var quick in this.quickAccess)
{
if (string.IsNullOrEmpty(quick.Location))
{
continue;
}
ImGui.PushFont(UiBuilder.IconFont);
if (ImGui.Selectable($"{quick.Icon}##{quick.Text}", quick.Text == this.selectedSideBar))
{
this.SetPath(quick.Location);
this.selectedSideBar = quick.Text;
}
ImGui.PopFont();
ImGui.SameLine(25);
ImGui.Text(quick.Text);
}
ImGui.EndChild();
@ -359,9 +339,13 @@ namespace Dalamud.Interface.ImGuiFileDialog
private unsafe void DrawFileListView(Vector2 size)
{
ImGui.BeginChild("##FileDialog_FileList", size);
if (!ImGui.BeginChild("##FileDialog_FileList", size))
{
ImGui.EndChild();
return;
}
var tableFlags = ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.Hideable | ImGuiTableFlags.ScrollY | ImGuiTableFlags.NoHostExtendX;
const ImGuiTableFlags tableFlags = ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.Hideable | ImGuiTableFlags.ScrollY | ImGuiTableFlags.NoHostExtendX;
if (ImGui.BeginTable("##FileTable", 4, tableFlags, size))
{
ImGui.TableSetupScrollFreeze(0, 1);
@ -427,13 +411,12 @@ namespace Dalamud.Interface.ImGuiFileDialog
var dir = file.Type == FileStructType.Directory;
var item = !dir ? GetIcon(file.Ext) : new IconColorItem
{
Color = dirTextColor,
Icon = (char)FontAwesomeIcon.Folder,
};
{
Color = dirTextColor,
Icon = FontAwesomeIcon.Folder,
};
ImGui.PushStyleColor(ImGuiCol.Text, item.Color);
if (selected) ImGui.PushStyleColor(ImGuiCol.Text, selectedTextColor);
ImGui.PushStyleColor(ImGuiCol.Text, selected ? selectedTextColor : item.Color);
ImGui.TableNextRow();
@ -444,30 +427,28 @@ namespace Dalamud.Interface.ImGuiFileDialog
if (ImGui.TableNextColumn())
{
ImGui.Text(file.Ext);
ImGui.TextUnformatted(file.Ext);
}
if (ImGui.TableNextColumn())
{
if (file.Type == FileStructType.File)
{
ImGui.Text(file.FormattedFileSize + " ");
ImGui.TextUnformatted(file.FormattedFileSize + " ");
}
else
{
ImGui.Text(" ");
ImGui.TextUnformatted(" ");
}
}
if (ImGui.TableNextColumn())
{
var sz = ImGui.CalcTextSize(file.FileModifiedDate);
ImGui.PushItemWidth(sz.X + 5);
ImGui.Text(file.FileModifiedDate + " ");
ImGui.PopItemWidth();
ImGui.SetNextItemWidth(sz.X + 5);
ImGui.TextUnformatted(file.FileModifiedDate + " ");
}
if (selected) ImGui.PopStyleColor();
ImGui.PopStyleColor();
if (needToBreak) break;
@ -475,6 +456,7 @@ namespace Dalamud.Interface.ImGuiFileDialog
}
clipper.End();
clipper.Destroy();
}
}
@ -503,13 +485,13 @@ namespace Dalamud.Interface.ImGuiFileDialog
ImGui.EndChild();
}
private bool SelectableItem(FileStruct file, bool selected, char icon)
private bool SelectableItem(FileStruct file, bool selected, FontAwesomeIcon icon)
{
var flags = ImGuiSelectableFlags.AllowDoubleClick | ImGuiSelectableFlags.SpanAllColumns;
const ImGuiSelectableFlags flags = ImGuiSelectableFlags.AllowDoubleClick | ImGuiSelectableFlags.SpanAllColumns;
ImGui.PushFont(UiBuilder.IconFont);
ImGui.Text($"{icon}");
ImGui.TextUnformatted(icon.ToIconString());
ImGui.PopFont();
ImGui.SameLine(25f);
@ -523,7 +505,7 @@ namespace Dalamud.Interface.ImGuiFileDialog
this.pathClicked = this.SelectDirectory(file);
return true;
}
else if (this.IsDirectoryMode())
if (this.IsDirectoryMode())
{
this.SelectFileName(file);
}
@ -712,11 +694,11 @@ namespace Dalamud.Interface.ImGuiFileDialog
if (this.IsDirectoryMode())
{
ImGui.Text("Directory Path :");
ImGui.TextUnformatted("Directory Path :");
}
else
{
ImGui.Text("File Name :");
ImGui.TextUnformatted("File Name :");
}
ImGui.SameLine();
@ -729,18 +711,17 @@ namespace Dalamud.Interface.ImGuiFileDialog
var selectOnly = this.flags.HasFlag(ImGuiFileDialogFlags.SelectOnly);
ImGui.PushItemWidth(width);
ImGui.SetNextItemWidth(width);
if (selectOnly) ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0.5f);
ImGui.InputText("##FileName", ref this.fileNameBuffer, 255, selectOnly ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None);
if (selectOnly) ImGui.PopStyleVar();
ImGui.PopItemWidth();
if (this.filters.Count > 0)
{
ImGui.SameLine();
var needToApplyNewFilter = false;
ImGui.PushItemWidth(150f);
ImGui.SetNextItemWidth(150f);
if (ImGui.BeginCombo("##Filters", this.selectedFilter.Filter, ImGuiComboFlags.None))
{
var idx = 0;
@ -760,8 +741,6 @@ namespace Dalamud.Interface.ImGuiFileDialog
ImGui.EndCombo();
}
ImGui.PopItemWidth();
if (needToApplyNewFilter)
{
this.SetPath(this.currentPath);
@ -825,11 +804,10 @@ namespace Dalamud.Interface.ImGuiFileDialog
{ // quit dialog, it doesn't exist anyway
return true;
}
else
{ // already exists, open dialog to confirm overwrite
this.isOk = false;
this.okResultToConfirm = true;
}
// already exists, open dialog to confirm overwrite
this.isOk = false;
this.okResultToConfirm = true;
}
var name = $"The file Already Exists !##{this.title}{this.id}OverWriteDialog";
@ -839,7 +817,7 @@ namespace Dalamud.Interface.ImGuiFileDialog
ImGui.OpenPopup(name);
if (ImGui.BeginPopupModal(name, ref open, ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoMove))
{
ImGui.Text("Would you like to Overwrite it ?");
ImGui.TextUnformatted("Would you like to Overwrite it ?");
if (ImGui.Button("Confirm"))
{
this.okResultToConfirm = false;

View file

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using ImGuiNET;
namespace Dalamud.Interface.ImGuiFileDialog
{
@ -10,6 +11,11 @@ namespace Dalamud.Interface.ImGuiFileDialog
/// </summary>
public partial class FileDialog
{
/// <summary>
/// The flags used to draw the file picker window.
/// </summary>
public ImGuiWindowFlags WindowFlags;
private readonly string title;
private readonly int selectionCountMax;
private readonly ImGuiFileDialogFlags flags;
@ -74,6 +80,9 @@ namespace Dalamud.Interface.ImGuiFileDialog
this.flags = flags;
this.selectionCountMax = selectionCountMax;
this.isModal = isModal;
this.WindowFlags = ImGuiWindowFlags.NoNav;
if (!isModal)
this.WindowFlags |= ImGuiWindowFlags.NoScrollbar;
this.currentPath = path;
this.defaultExtension = defaultExtension;
@ -161,6 +170,42 @@ namespace Dalamud.Interface.ImGuiFileDialog
return this.currentPath;
}
/// <summary>
/// Set or remove a quick access folder for the navigation panel.
/// </summary>
/// <param name="name">The displayed name of the folder. If this name already exists, it will be overwritten.</param>
/// <param name="path">The new linked path. If this is empty, no link will be added and existing links will be removed.</param>
/// <param name="icon">The FontAwesomeIcon-ID of the icon displayed before the name.</param>
/// <param name="position">An optional position at which to insert the new link. If the link is updated, having this less than zero will keep its position.
/// Otherwise, invalid indices will insert it at the end.</param>
public void SetQuickAccess(string name, string path, FontAwesomeIcon icon, int position = -1)
{
var idx = this.quickAccess.FindIndex(q => q.Text.Equals(name, StringComparison.InvariantCultureIgnoreCase));
if (idx >= 0)
{
if (position >= 0 || path.Length == 0)
{
this.quickAccess.RemoveAt(idx);
}
else
{
this.quickAccess[idx] = new SideBarItem(name, path, icon);
return;
}
}
if (path.Length == 0) return;
if (position < 0 || position >= this.quickAccess.Count)
{
this.quickAccess.Add(new SideBarItem(name, path, icon));
}
else
{
this.quickAccess.Insert(position, new SideBarItem(name, path, icon));
}
}
private string GetFilePathName()
{
var path = this.GetCurrentPath();
@ -184,7 +229,7 @@ namespace Dalamud.Interface.ImGuiFileDialog
var result = this.fileNameBuffer;
// a collection like {.cpp, .h}, so can't decide on an extension
if (this.selectedFilter.CollectionFilters != null && this.selectedFilter.CollectionFilters.Count > 0)
if (this.selectedFilter.CollectionFilters is { Count: > 0 })
{
return result;
}
@ -195,7 +240,7 @@ namespace Dalamud.Interface.ImGuiFileDialog
var lastPoint = result.LastIndexOf('.');
if (lastPoint != -1)
{
result = result.Substring(0, lastPoint);
result = result[..lastPoint];
}
result += this.selectedFilter.Filter;
@ -230,7 +275,7 @@ namespace Dalamud.Interface.ImGuiFileDialog
this.currentPath = dir.FullName;
if (this.currentPath[^1] == Path.DirectorySeparatorChar)
{ // handle selecting a drive, like C: -> C:\
this.currentPath = this.currentPath[0..^1];
this.currentPath = this.currentPath[..^1];
}
this.pathInputBuffer = this.currentPath;

View file

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using ImGuiNET;
namespace Dalamud.Interface.ImGuiFileDialog
{
@ -8,6 +9,12 @@ namespace Dalamud.Interface.ImGuiFileDialog
/// </summary>
public class FileDialogManager
{
/// <summary> Additional quick access items for the side bar.</summary>
public readonly List<(string Name, string Path, FontAwesomeIcon Icon, int Position)> CustomSideBarItems = new();
/// <summary> Additional flags with which to draw the window. </summary>
public ImGuiWindowFlags AddedWindowFlags = ImGuiWindowFlags.None;
private FileDialog? dialog;
private Action<bool, string>? callback;
private Action<bool, List<string>>? multiCallback;
@ -181,6 +188,9 @@ namespace Dalamud.Interface.ImGuiFileDialog
}
this.dialog = new FileDialog(id, title, filters, path, defaultFileName, defaultExtension, selectionCountMax, isModal, flags);
this.dialog.WindowFlags |= this.AddedWindowFlags;
foreach (var (name, location, icon, position) in this.CustomSideBarItems)
this.dialog.SetQuickAccess(name, location, icon, position);
this.dialog.Show();
}
}

View file

@ -16,41 +16,41 @@ namespace Dalamud.Interface.ImGuiFileDialog
/// <summary>
/// Confirm the selection when choosing a file which already exists.
/// </summary>
ConfirmOverwrite = 1,
ConfirmOverwrite = 0x01,
/// <summary>
/// Only allow selection of files or folders which currently exist.
/// </summary>
SelectOnly = 2,
SelectOnly = 0x02,
/// <summary>
/// Hide files or folders which start with a period.
/// </summary>
DontShowHiddenFiles = 3,
DontShowHiddenFiles = 0x04,
/// <summary>
/// Disable the creation of new folders within the dialog.
/// </summary>
DisableCreateDirectoryButton = 4,
DisableCreateDirectoryButton = 0x08,
/// <summary>
/// Hide the type column.
/// </summary>
HideColumnType = 5,
HideColumnType = 0x10,
/// <summary>
/// Hide the file size column.
/// </summary>
HideColumnSize = 6,
HideColumnSize = 0x20,
/// <summary>
/// Hide the last modified date column.
/// </summary>
HideColumnDate = 7,
HideColumnDate = 0x40,
/// <summary>
/// Hide the quick access sidebar.
/// </summary>
HideSideBar = 8,
HideSideBar = 0x80,
}
}

View file

@ -131,6 +131,13 @@ namespace Dalamud.Interface.Internal
HelpMessage = "Dalamud version info",
});
commandManager.AddHandler("/xlui", new CommandInfo(this.OnUiCommand)
{
HelpMessage = Loc.Localize(
"DalamudUiModeHelp",
"Toggle Dalamud UI display modes. Native UI modifications may also be affected by this, but that depends on the plugin."),
});
commandManager.AddHandler("/imdebug", new CommandInfo(this.OnDebugImInfoCommand)
{
HelpMessage = "ImGui DEBUG",
@ -366,5 +373,31 @@ namespace Dalamud.Interface.Internal
{
Service<DalamudInterface>.Get().ToggleSettingsWindow();
}
private void OnUiCommand(string command, string arguments)
{
var im = Service<InterfaceManager>.Get();
im.IsDispatchingEvents = arguments switch
{
"show" => true,
"hide" => false,
_ => !im.IsDispatchingEvents,
};
var pm = Service<PluginManager>.Get();
foreach (var plugin in pm.InstalledPlugins)
{
if (im.IsDispatchingEvents)
{
plugin.DalamudInterface?.UiBuilder.NotifyShowUi();
}
else
{
plugin.DalamudInterface?.UiBuilder.NotifyHideUi();
}
}
}
}
}

View file

@ -54,6 +54,7 @@ namespace Dalamud.Interface.Internal
private readonly StyleEditorWindow styleEditorWindow;
private readonly TitleScreenMenuWindow titleScreenMenuWindow;
private readonly FallbackFontNoticeWindow fallbackFontNoticeWindow;
private readonly ProfilerWindow profilerWindow;
private readonly TextureWrap logoTexture;
private readonly TextureWrap tsmLogoTexture;
@ -66,7 +67,12 @@ namespace Dalamud.Interface.Internal
private bool isImGuiDrawDevMenu = false;
#endif
#if BOOT_AGING
private bool signaledBoot = false;
#endif
private bool isImGuiDrawDemoWindow = false;
private bool isImGuiTestWindowsInMonospace = false;
private bool isImGuiDrawMetricsWindow = false;
/// <summary>
@ -94,6 +100,7 @@ namespace Dalamud.Interface.Internal
this.styleEditorWindow = new StyleEditorWindow() { IsOpen = false };
this.titleScreenMenuWindow = new TitleScreenMenuWindow() { IsOpen = false };
this.fallbackFontNoticeWindow = new FallbackFontNoticeWindow() { IsOpen = interfaceManager.IsFallbackFontMode && !configuration.DisableFontFallbackNotice };
this.profilerWindow = new ProfilerWindow() { IsOpen = false };
this.WindowSystem.AddWindow(this.changelogWindow);
this.WindowSystem.AddWindow(this.colorDemoWindow);
@ -110,6 +117,7 @@ namespace Dalamud.Interface.Internal
this.WindowSystem.AddWindow(this.styleEditorWindow);
this.WindowSystem.AddWindow(this.titleScreenMenuWindow);
this.WindowSystem.AddWindow(this.fallbackFontNoticeWindow);
this.WindowSystem.AddWindow(this.profilerWindow);
ImGuiManagedAsserts.AssertsEnabled = configuration.AssertsEnabledAtStartup;
this.isImGuiDrawDevMenu = this.isImGuiDrawDevMenu || configuration.DevBarOpenAtStartup;
@ -255,6 +263,11 @@ namespace Dalamud.Interface.Internal
/// Opens the <see cref="StyleEditorWindow"/>.
/// </summary>
public void OpenStyleEditor() => this.styleEditorWindow.IsOpen = true;
/// <summary>
/// Opens the <see cref="ProfilerWindow"/>.
/// </summary>
public void OpenProfiler() => this.profilerWindow.IsOpen = true;
#endregion
@ -346,6 +359,11 @@ namespace Dalamud.Interface.Internal
/// Toggles the <see cref="StyleEditorWindow"/>.
/// </summary>
public void ToggleStyleEditorWindow() => this.selfTestWindow.Toggle();
/// <summary>
/// Toggles the <see cref="ProfilerWindow"/>.
/// </summary>
public void ToggleProfilerWindow() => this.profilerWindow.Toggle();
#endregion
@ -353,6 +371,19 @@ namespace Dalamud.Interface.Internal
{
this.frameCount++;
#if BOOT_AGING
if (this.frameCount > 500 && !this.signaledBoot)
{
this.signaledBoot = true;
System.Threading.Tasks.Task.Run(async () =>
{
using var client = new System.Net.Http.HttpClient();
await client.PostAsync("http://localhost:1415/aging/success", new System.Net.Http.StringContent(string.Empty));
});
}
#endif
try
{
this.DrawHiddenDevMenuOpener();
@ -363,12 +394,18 @@ namespace Dalamud.Interface.Internal
this.WindowSystem.Draw();
if (this.isImGuiTestWindowsInMonospace)
ImGui.PushFont(InterfaceManager.MonoFont);
if (this.isImGuiDrawDemoWindow)
ImGui.ShowDemoWindow(ref this.isImGuiDrawDemoWindow);
if (this.isImGuiDrawMetricsWindow)
ImGui.ShowMetricsWindow(ref this.isImGuiDrawMetricsWindow);
if (this.isImGuiTestWindowsInMonospace)
ImGui.PopFont();
// Release focus of any ImGui window if we click into the game.
var io = ImGui.GetIO();
if (!io.WantCaptureMouse && (User32.GetKeyState((int)User32.VirtualKey.VK_LBUTTON) & 0x8000) != 0)
@ -519,6 +556,11 @@ namespace Dalamud.Interface.Internal
this.OpenStyleEditor();
}
if (ImGui.MenuItem("Open Profiler"))
{
this.OpenProfiler();
}
ImGui.Separator();
if (ImGui.MenuItem("Unload Dalamud"))
@ -566,8 +608,8 @@ namespace Dalamud.Interface.Internal
if (ImGui.BeginMenu("GUI"))
{
ImGui.MenuItem("Use Monospace font for following windows", string.Empty, ref this.isImGuiTestWindowsInMonospace);
ImGui.MenuItem("Draw ImGui demo", string.Empty, ref this.isImGuiDrawDemoWindow);
ImGui.MenuItem("Draw metrics", string.Empty, ref this.isImGuiDrawMetricsWindow);
ImGui.Separator();

View file

@ -21,6 +21,7 @@ using Dalamud.Interface.Internal.Notifications;
using Dalamud.Interface.Style;
using Dalamud.Interface.Windowing;
using Dalamud.Utility;
using Dalamud.Utility.Timing;
using ImGuiNET;
using ImGuiScene;
using PInvoke;
@ -151,7 +152,7 @@ namespace Dalamud.Interface.Internal
/// <summary>
/// Gets or sets an action that is executed right after font fallback mode has been changed.
/// </summary>
public event Action<bool> OnFallbackFontModeChange;
public event Action<bool> FallbackFontModeChange;
/// <summary>
/// Gets the default ImGui font.
@ -202,6 +203,11 @@ namespace Dalamud.Interface.Internal
/// </summary>
public bool IsReady => this.scene != null;
/// <summary>
/// Gets or sets a value indicating whether or not Draw events should be dispatched.
/// </summary>
public bool IsDispatchingEvents { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether the font has been loaded in fallback mode.
/// </summary>
@ -214,7 +220,7 @@ namespace Dalamud.Interface.Internal
return;
this.isFallbackFontMode = value;
this.OnFallbackFontModeChange?.Invoke(value);
this.FallbackFontModeChange?.Invoke(value);
}
}
@ -469,118 +475,121 @@ namespace Dalamud.Interface.Internal
if (this.scene == null)
{
try
using (Timings.Start("IM Scene Init"))
{
this.scene = new RawDX11Scene(swapChain);
}
catch (DllNotFoundException ex)
{
Log.Error(ex, "Could not load ImGui dependencies.");
var res = PInvoke.User32.MessageBox(
IntPtr.Zero,
"Dalamud plugins require the Microsoft Visual C++ Redistributable to be installed.\nPlease install the runtime from the official Microsoft website or disable Dalamud.\n\nDo you want to download the redistributable now?",
"Dalamud Error",
User32.MessageBoxOptions.MB_YESNO | User32.MessageBoxOptions.MB_TOPMOST | User32.MessageBoxOptions.MB_ICONERROR);
if (res == User32.MessageBoxResult.IDYES)
try
{
var psi = new ProcessStartInfo
this.scene = new RawDX11Scene(swapChain);
}
catch (DllNotFoundException ex)
{
Log.Error(ex, "Could not load ImGui dependencies.");
var res = PInvoke.User32.MessageBox(
IntPtr.Zero,
"Dalamud plugins require the Microsoft Visual C++ Redistributable to be installed.\nPlease install the runtime from the official Microsoft website or disable Dalamud.\n\nDo you want to download the redistributable now?",
"Dalamud Error",
User32.MessageBoxOptions.MB_YESNO | User32.MessageBoxOptions.MB_TOPMOST | User32.MessageBoxOptions.MB_ICONERROR);
if (res == User32.MessageBoxResult.IDYES)
{
FileName = "https://aka.ms/vs/16/release/vc_redist.x64.exe",
UseShellExecute = true,
};
Process.Start(psi);
var psi = new ProcessStartInfo
{
FileName = "https://aka.ms/vs/16/release/vc_redist.x64.exe",
UseShellExecute = true,
};
Process.Start(psi);
}
Environment.Exit(-1);
}
Environment.Exit(-1);
}
var startInfo = Service<DalamudStartInfo>.Get();
var configuration = Service<DalamudConfiguration>.Get();
var startInfo = Service<DalamudStartInfo>.Get();
var configuration = Service<DalamudConfiguration>.Get();
var iniFileInfo = new FileInfo(Path.Combine(Path.GetDirectoryName(startInfo.ConfigurationPath), "dalamudUI.ini"));
var iniFileInfo = new FileInfo(Path.Combine(Path.GetDirectoryName(startInfo.ConfigurationPath), "dalamudUI.ini"));
try
{
if (iniFileInfo.Length > 1200000)
try
{
Log.Warning("dalamudUI.ini was over 1mb, deleting");
iniFileInfo.CopyTo(Path.Combine(iniFileInfo.DirectoryName, $"dalamudUI-{DateTimeOffset.Now.ToUnixTimeSeconds()}.ini"));
iniFileInfo.Delete();
if (iniFileInfo.Length > 1200000)
{
Log.Warning("dalamudUI.ini was over 1mb, deleting");
iniFileInfo.CopyTo(Path.Combine(iniFileInfo.DirectoryName, $"dalamudUI-{DateTimeOffset.Now.ToUnixTimeSeconds()}.ini"));
iniFileInfo.Delete();
}
}
catch (Exception ex)
{
Log.Error(ex, "Could not delete dalamudUI.ini");
}
this.scene.ImGuiIniPath = iniFileInfo.FullName;
this.scene.OnBuildUI += this.Display;
this.scene.OnNewInputFrame += this.OnNewInputFrame;
StyleModel.TransferOldModels();
if (configuration.SavedStyles == null || configuration.SavedStyles.All(x => x.Name != StyleModelV1.DalamudStandard.Name))
{
configuration.SavedStyles = new List<StyleModel> { StyleModelV1.DalamudStandard, StyleModelV1.DalamudClassic };
configuration.ChosenStyle = StyleModelV1.DalamudStandard.Name;
}
else if (configuration.SavedStyles.Count == 1)
{
configuration.SavedStyles.Add(StyleModelV1.DalamudClassic);
}
else if (configuration.SavedStyles[1].Name != StyleModelV1.DalamudClassic.Name)
{
configuration.SavedStyles.Insert(1, StyleModelV1.DalamudClassic);
}
configuration.SavedStyles[0] = StyleModelV1.DalamudStandard;
configuration.SavedStyles[1] = StyleModelV1.DalamudClassic;
var style = configuration.SavedStyles.FirstOrDefault(x => x.Name == configuration.ChosenStyle);
if (style == null)
{
style = StyleModelV1.DalamudStandard;
configuration.ChosenStyle = style.Name;
configuration.Save();
}
style.Apply();
ImGui.GetIO().FontGlobalScale = configuration.GlobalUiScale;
this.SetupFonts();
if (!configuration.IsDocking)
{
ImGui.GetIO().ConfigFlags &= ~ImGuiConfigFlags.DockingEnable;
}
else
{
ImGui.GetIO().ConfigFlags |= ImGuiConfigFlags.DockingEnable;
}
// NOTE (Chiv) Toggle gamepad navigation via setting
if (!configuration.IsGamepadNavigationEnabled)
{
ImGui.GetIO().BackendFlags &= ~ImGuiBackendFlags.HasGamepad;
ImGui.GetIO().ConfigFlags &= ~ImGuiConfigFlags.NavEnableSetMousePos;
}
else
{
ImGui.GetIO().BackendFlags |= ImGuiBackendFlags.HasGamepad;
ImGui.GetIO().ConfigFlags |= ImGuiConfigFlags.NavEnableSetMousePos;
}
// NOTE (Chiv) Explicitly deactivate on dalamud boot
ImGui.GetIO().ConfigFlags &= ~ImGuiConfigFlags.NavEnableGamepad;
ImGuiHelpers.MainViewport = ImGui.GetMainViewport();
Log.Information("[IM] Scene & ImGui setup OK!");
Service<DalamudIME>.Get().Enable();
}
catch (Exception ex)
{
Log.Error(ex, "Could not delete dalamudUI.ini");
}
this.scene.ImGuiIniPath = iniFileInfo.FullName;
this.scene.OnBuildUI += this.Display;
this.scene.OnNewInputFrame += this.OnNewInputFrame;
this.SetupFonts();
StyleModel.TransferOldModels();
if (configuration.SavedStyles == null || configuration.SavedStyles.All(x => x.Name != StyleModelV1.DalamudStandard.Name))
{
configuration.SavedStyles = new List<StyleModel> { StyleModelV1.DalamudStandard, StyleModelV1.DalamudClassic };
configuration.ChosenStyle = StyleModelV1.DalamudStandard.Name;
}
else if (configuration.SavedStyles.Count == 1)
{
configuration.SavedStyles.Add(StyleModelV1.DalamudClassic);
}
else if (configuration.SavedStyles[1].Name != StyleModelV1.DalamudClassic.Name)
{
configuration.SavedStyles.Insert(1, StyleModelV1.DalamudClassic);
}
configuration.SavedStyles[0] = StyleModelV1.DalamudStandard;
configuration.SavedStyles[1] = StyleModelV1.DalamudClassic;
var style = configuration.SavedStyles.FirstOrDefault(x => x.Name == configuration.ChosenStyle);
if (style == null)
{
style = StyleModelV1.DalamudStandard;
configuration.ChosenStyle = style.Name;
configuration.Save();
}
style.Apply();
ImGui.GetIO().FontGlobalScale = configuration.GlobalUiScale;
if (!configuration.IsDocking)
{
ImGui.GetIO().ConfigFlags &= ~ImGuiConfigFlags.DockingEnable;
}
else
{
ImGui.GetIO().ConfigFlags |= ImGuiConfigFlags.DockingEnable;
}
// NOTE (Chiv) Toggle gamepad navigation via setting
if (!configuration.IsGamepadNavigationEnabled)
{
ImGui.GetIO().BackendFlags &= ~ImGuiBackendFlags.HasGamepad;
ImGui.GetIO().ConfigFlags &= ~ImGuiConfigFlags.NavEnableSetMousePos;
}
else
{
ImGui.GetIO().BackendFlags |= ImGuiBackendFlags.HasGamepad;
ImGui.GetIO().ConfigFlags |= ImGuiConfigFlags.NavEnableSetMousePos;
}
// NOTE (Chiv) Explicitly deactivate on dalamud boot
ImGui.GetIO().ConfigFlags &= ~ImGuiConfigFlags.NavEnableGamepad;
ImGuiHelpers.MainViewport = ImGui.GetMainViewport();
Log.Information("[IM] Scene & ImGui setup OK!");
Service<DalamudIME>.Get().Enable();
}
if (this.address.IsReshade)
@ -628,6 +637,8 @@ namespace Dalamud.Interface.Internal
/// <param name="disableBigFonts">If set, then glyphs will be loaded in smaller resolution to make all glyphs fit into given constraints.</param>
private unsafe void SetupFonts(bool disableBigFonts = false)
{
using var setupFontsTimings = Timings.Start("IM SetupFonts");
var gameFontManager = Service<GameFontManager>.Get();
var dalamud = Service<Dalamud>.Get();
var io = ImGui.GetIO();
@ -828,10 +839,15 @@ namespace Dalamud.Interface.Internal
for (int i = customFontFirstConfigIndex, i_ = ioFonts.ConfigData.Size; i < i_; i++)
{
var config = ioFonts.ConfigData[i];
if (gameFontManager.OwnsFont(config.DstFont))
continue;
config.OversampleH = 1;
config.OversampleV = 1;
var name = Encoding.UTF8.GetString((byte*)config.Name.Data, config.Name.Count).TrimEnd('\0');
if (name.IsNullOrEmpty())
name = $"{config.SizePixels}px";
// ImFont information is reflected only if corresponding ImFontConfig has MergeMode not set.
if (config.MergeMode)
@ -869,6 +885,7 @@ namespace Dalamud.Interface.Internal
Log.Verbose("[FONT] ImGui.IO.Build will be called.");
ioFonts.Build();
gameFontManager.AfterIoFontsBuild();
Log.Verbose("[FONT] ImGui.IO.Build OK!");
if (ioFonts.TexHeight > maxTexDimension)
@ -886,6 +903,7 @@ namespace Dalamud.Interface.Internal
if (possibilityForScaling && !disableBigFonts)
{
Log.Information("[FONT] Atlas size is {0}x{1} which is bigger than allowed {2}x{3}. Retrying with minimized font sizes.", ioFonts.TexWidth, ioFonts.TexHeight, maxTexDimension, maxTexDimension);
this.IsFallbackFontMode = true;
this.SetupFonts(true);
return;
}
@ -895,7 +913,8 @@ namespace Dalamud.Interface.Internal
}
}
this.IsFallbackFontMode = disableBigFonts;
if (!disableBigFonts)
this.IsFallbackFontMode = false;
if (Math.Abs(fontGamma - 1.0f) >= 0.001)
{
@ -929,9 +948,10 @@ namespace Dalamud.Interface.Internal
if (mod.Axis == TargetFontModification.AxisMode.Overwrite)
{
Log.Verbose("[FONT] {0}: Overwrite from AXIS of size {1}px (was {2}px)", mod.Name, mod.SourceAxis.ImFont.FontSize, font.FontSize);
font.FontSize = mod.SourceAxis.ImFont.FontSize;
font.Ascent = mod.SourceAxis.ImFont.Ascent;
font.Descent = mod.SourceAxis.ImFont.Descent;
GameFontManager.UnscaleFont(font, font.FontSize / mod.SourceAxis.ImFont.FontSize, false);
var ascentDiff = mod.SourceAxis.ImFont.Ascent - font.Ascent;
font.Ascent += ascentDiff;
font.Descent = ascentDiff;
font.FallbackChar = mod.SourceAxis.ImFont.FallbackChar;
font.EllipsisChar = mod.SourceAxis.ImFont.EllipsisChar;
ImGuiHelpers.CopyGlyphsAcrossFonts(mod.SourceAxis.ImFont, font, false, false);
@ -964,6 +984,9 @@ namespace Dalamud.Interface.Internal
this.AfterBuildFonts?.Invoke();
Log.Verbose("[FONT] OnAfterBuildFonts OK!");
if (ioFonts.Fonts[0].NativePtr != DefaultFont.NativePtr)
Log.Warning("[FONT] First font is not DefaultFont");
Log.Verbose("[FONT] Fonts built!");
this.fontBuildSignal.Set();
@ -995,7 +1018,36 @@ namespace Dalamud.Interface.Internal
Log.Verbose("[FONT] RebuildFontsInternal() detaching");
this.scene.OnNewRenderFrame -= this.RebuildFontsInternal;
this.scene.InvalidateFonts();
Log.Verbose("[FONT] Calling InvalidateFonts");
try
{
this.scene.InvalidateFonts();
}
catch (Exception ex)
{
if (this.FontResolutionLevel > 2)
{
Log.Error(ex, "[FONT] Failed to create font textures; setting font resolution level to 2 and retrying");
this.FontResolutionLevelOverride = 2;
this.SetupFonts();
}
else
{
Log.Error(ex, "[FONT] Failed to create font textures; forcing fallback font mode");
this.SetupFonts(true);
}
Log.Verbose("[FONT] Calling InvalidateFonts again");
try
{
this.scene.InvalidateFonts();
}
catch (Exception ex2)
{
Log.Error(ex2, "[FONT] Giving up");
}
}
Log.Verbose("[FONT] Font Rebuild OK!");
@ -1109,7 +1161,10 @@ namespace Dalamud.Interface.Internal
WindowSystem.FocusedWindowSystemNamespace = string.Empty;
var snap = ImGuiManagedAsserts.GetSnapshot();
this.Draw?.Invoke();
if (this.IsDispatchingEvents)
this.Draw?.Invoke();
ImGuiManagedAsserts.ReportProblems("Dalamud Core", snap);
Service<NotificationManager>.Get().Draw();

View file

@ -28,24 +28,30 @@ Version D{0}
created by:
goat
Mino
Meli
attick
Aida-Enna
perchbird
Wintermute
fmauNeko
Caraxi
Adam
nibs/Poliwrath
karashiiro
Pohky
daemitus
Aireil
kalilistic
MgAl2O4
Soreepeong
ff-meli
attickdoor
Caraxi
ascclemens
r00telement
kalilistic
0ceal0t
lmcintyre
pohky
Aireil
fitzchivalrik
MgAl2O4
NotAdam
marimelon
karashiiro
pmgr
Ottermandias
aers
Poliwrath
Minizbot2021
MalRD
SheepGoMeh
philpax
@ -99,15 +105,31 @@ Franz
aers
We use these awesome C# libraries:
We use these awesome libraries:
Lumina by Adam
FFXIVClientStructs by aers ({2})
DotNetCorePlugins
Copyright (c) Nate McMaster
Copyright (c) Nate McMaster
Licensed under the Apache License, Version 2.0
See License.txt for license information.
json
Copyright (c) 2013-2022 Niels Lohmann
Licensed under the MIT License
nmd by Nomade040
Licensed under the Unlicense
MinHook
Copyright (C) 2009-2017 Tsuda Kageyu
Licensed under the BSD 2-Clause License
SRELL
Copyright (c) 2012-2022, Nozomu Katoo
Please see licenses.txt for more information.
Thanks to everyone in the XIVLauncher Discord server

View file

@ -452,7 +452,6 @@ namespace Dalamud.Interface.Internal.Windows
}
else
{
stateString += $"FrameworkBase: {framework.Address.BaseAddress.ToInt64():X}\n";
stateString += $"ObjectTableLen: {objectTable.Length}\n";
stateString += $"LocalPlayerName: {clientState.LocalPlayer.Name}\n";
stateString += $"CurrentWorldName: {(this.resolveGameData ? clientState.LocalPlayer.CurrentWorld.GameData.Name : clientState.LocalPlayer.CurrentWorld.Id.ToString())}\n";
@ -530,7 +529,6 @@ namespace Dalamud.Interface.Internal.Windows
}
else
{
stateString += $"FrameworkBase: {framework.Address.BaseAddress.ToInt64():X}\n";
stateString += $"FateTableLen: {fateTable.Length}\n";
ImGui.TextUnformatted(stateString);

View file

@ -29,7 +29,7 @@ namespace Dalamud.Interface.Internal.Windows
var interfaceManager = Service<InterfaceManager>.Get();
var dalamud = Service<Dalamud>.Get();
Service<InterfaceManager>.Get().OnFallbackFontModeChange += this.OnFallbackFontModeChange;
Service<InterfaceManager>.Get().FallbackFontModeChange += this.OnFallbackFontModeChange;
}
private static string Title => Loc.Localize("FallbackFontNoticeWindowTitle", "Fallback Font Mode Active");
@ -80,7 +80,7 @@ namespace Dalamud.Interface.Internal.Windows
/// </summary>
public void Dispose()
{
Service<InterfaceManager>.Get().OnFallbackFontModeChange -= this.OnFallbackFontModeChange;
Service<InterfaceManager>.Get().FallbackFontModeChange -= this.OnFallbackFontModeChange;
}
private void OnFallbackFontModeChange(bool mode)

View file

@ -1,7 +1,7 @@
using System.Numerics;
using Dalamud.Game.ClientState.Keys;
using Dalamud.Game.Gui.Internal;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Windowing;
using ImGuiNET;
@ -10,7 +10,7 @@ namespace Dalamud.Interface.Internal.Windows
/// <summary>
/// A window for displaying IME details.
/// </summary>
internal class IMEWindow : Window
internal unsafe class IMEWindow : Window
{
private const int ImePageSize = 9;
@ -18,7 +18,7 @@ namespace Dalamud.Interface.Internal.Windows
/// Initializes a new instance of the <see cref="IMEWindow"/> class.
/// </summary>
public IMEWindow()
: base("Dalamud IME", ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.AlwaysAutoResize)
: base("Dalamud IME", ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoBackground)
{
this.Size = new Vector2(100, 200);
this.SizeCondition = ImGuiCond.FirstUseEver;
@ -29,6 +29,7 @@ namespace Dalamud.Interface.Internal.Windows
/// <inheritdoc/>
public override void Draw()
{
if (this.IsOpen && Service<KeyState>.Get()[VirtualKey.SHIFT]) Service<DalamudInterface>.Get().CloseIMEWindow();
var ime = Service<DalamudIME>.GetNullable();
if (ime == null || !ime.IsEnabled)
@ -36,34 +37,70 @@ namespace Dalamud.Interface.Internal.Windows
ImGui.Text("IME is unavailable.");
return;
}
}
ImGui.Text(ime.ImmComp);
/// <inheritdoc/>
public override void PostDraw()
{
if (this.IsOpen && Service<KeyState>.Get()[VirtualKey.SHIFT]) Service<DalamudInterface>.Get().CloseIMEWindow();
var ime = Service<DalamudIME>.GetNullable();
ImGui.Separator();
if (ime == null || !ime.IsEnabled)
return;
var cursorPos = ime.GetCursorPos();
var nextDrawPosY = cursorPos.Y;
var maxTextWidth = 0f;
var textHeight = ImGui.CalcTextSize(ime.ImmComp).Y;
var drawAreaPosX = cursorPos.X + ImGui.GetStyle().WindowPadding.X;
var native = ime.ImmCandNative;
for (var i = 0; i < ime.ImmCand.Count; i++)
{
var selected = i == (native.Selection % ImePageSize);
if (selected)
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.HealerGreen);
ImGui.Text($"{i + 1}. {ime.ImmCand[i]}");
if (selected)
ImGui.PopStyleColor();
}
var totalIndex = native.Selection + 1;
var totalSize = native.Count;
var pageStart = native.PageStart;
var pageIndex = (pageStart / ImePageSize) + 1;
var pageCount = (totalSize / ImePageSize) + 1;
var pageInfo = $"{totalIndex}/{totalSize} ({pageIndex}/{pageCount})";
ImGui.Separator();
ImGui.Text($"{totalIndex}/{totalSize} ({pageIndex}/{pageCount})");
// Calc the window size
for (var i = 0; i < ime.ImmCand.Count; i++)
{
var textSize = ImGui.CalcTextSize($"{i + 1}. {ime.ImmCand[i]}");
maxTextWidth = maxTextWidth > textSize.X ? maxTextWidth : textSize.X;
}
maxTextWidth = maxTextWidth > ImGui.CalcTextSize(pageInfo).X ? maxTextWidth : ImGui.CalcTextSize(pageInfo).X;
maxTextWidth = maxTextWidth > ImGui.CalcTextSize(ime.ImmComp).X ? maxTextWidth : ImGui.CalcTextSize(ime.ImmComp).X;
var imeWindowMinPos = new Vector2(cursorPos.X, cursorPos.Y);
var imeWindowMaxPos = new Vector2(cursorPos.X + maxTextWidth + (2 * ImGui.GetStyle().WindowPadding.X), cursorPos.Y + (textHeight * (ime.ImmCand.Count + 2)) + (5 * (ime.ImmCand.Count - 1)) + (2 * ImGui.GetStyle().WindowPadding.Y));
var drawList = ImGui.GetForegroundDrawList();
// Draw the background rect
drawList.AddRectFilled(imeWindowMinPos, imeWindowMaxPos, ImGui.GetColorU32(ImGuiCol.WindowBg), ImGui.GetStyle().WindowRounding);
// Add component text
drawList.AddText(new Vector2(drawAreaPosX, nextDrawPosY), ImGui.GetColorU32(ImGuiCol.Text), ime.ImmComp);
nextDrawPosY += textHeight + ImGui.GetStyle().ItemSpacing.Y;
// Add separator
drawList.AddLine(new Vector2(drawAreaPosX, nextDrawPosY), new Vector2(drawAreaPosX + maxTextWidth, nextDrawPosY), ImGui.GetColorU32(ImGuiCol.Separator));
// Add candidate words
for (var i = 0; i < ime.ImmCand.Count; i++)
{
var selected = i == (native.Selection % ImePageSize);
var color = ImGui.GetColorU32(ImGuiCol.Text);
if (selected)
color = ImGui.GetColorU32(ImGuiCol.NavHighlight);
drawList.AddText(new Vector2(drawAreaPosX, nextDrawPosY), color, $"{i + 1}. {ime.ImmCand[i]}");
nextDrawPosY += textHeight + ImGui.GetStyle().ItemSpacing.Y;
}
// Add separator
drawList.AddLine(new Vector2(drawAreaPosX, nextDrawPosY), new Vector2(drawAreaPosX + maxTextWidth, nextDrawPosY), ImGui.GetColorU32(ImGuiCol.Separator));
// Add pages infomation
drawList.AddText(new Vector2(drawAreaPosX, nextDrawPosY), ImGui.GetColorU32(ImGuiCol.Text), pageInfo);
}
}
}

View file

@ -386,6 +386,10 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
if (this.errorModalOnNextFrame)
{
// NOTE(goat): ImGui cannot open a modal if no window is focused, at the moment.
// If people click out of the installer into the game while a plugin is installing, we won't be able to show a modal if we don't grab focus.
ImGui.SetWindowFocus(this.WindowName);
ImGui.OpenPopup(modalTitle);
this.errorModalOnNextFrame = false;
this.errorModalDrawing = true;

View file

@ -0,0 +1,176 @@
using System;
using System.Linq;
using System.Numerics;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Windowing;
using Dalamud.Utility.Numerics;
using Dalamud.Utility.Timing;
using ImGuiNET;
namespace Dalamud.Interface.Internal.Windows;
public class ProfilerWindow : Window
{
private double min;
private double max;
public ProfilerWindow() : base("Profiler", forceMainWindow: true) { }
public override void OnOpen()
{
this.min = Timings.AllTimings.Min(x => x.StartTime);
this.max = Timings.AllTimings.Max(x => x.EndTime);
}
/// <inheritdoc/>
public override void Draw()
{
var width = ImGui.GetWindowWidth();
var actualMin = Timings.AllTimings.Min(x => x.StartTime);
var actualMax = Timings.AllTimings.Max(x => x.EndTime);
ImGui.Text("Timings");
const int childHeight = 300;
if (ImGui.BeginChild("Timings", new Vector2(0, childHeight), true))
{
var pos = ImGui.GetCursorScreenPos();
for (var i = 0; i < width; i += 80)
{
ImGui.PushFont(InterfaceManager.MonoFont);
var lineEnd = childHeight - 20;
ImGui.GetWindowDrawList().AddLine(
pos + new Vector2(i, 0),
pos + new Vector2(i, lineEnd - 10),
ImGui.GetColorU32(ImGuiColors.ParsedGrey.WithW(0x40)));
// Draw ms label for line
var ms = ((i / width) * (this.max - this.min)) + this.min;
var msStr = (ms / 1000).ToString("F2") + "s";
var msSize = ImGui.CalcTextSize(msStr);
var labelPos = pos + new Vector2(i - (msSize.X / 2), (-msSize.Y / 2) + lineEnd);
// nudge label to the side if it's the first, so we're not cut off
if (i == 0)
labelPos.X += msSize.X / 2;
ImGui.GetWindowDrawList().AddText(
labelPos,
ImGui.GetColorU32(ImGuiColors.ParsedGrey.WithW(0x40)),
msStr);
ImGui.PopFont();
}
uint maxRectDept = 0;
foreach (var timingHandle in Timings.AllTimings)
{
var startX = (timingHandle.StartTime - this.min) / (this.max - this.min) * width;
var endX = (timingHandle.EndTime - this.min) / (this.max - this.min) * width;
startX = Math.Max(startX, 0);
endX = Math.Max(endX, 0);
var rectColor = timingHandle.IsMainThread ? ImGuiColors.ParsedBlue : ImGuiColors.ParsedPurple;
rectColor.X -= timingHandle.Depth * 0.12f;
rectColor.Y -= timingHandle.Depth * 0.12f;
rectColor.Z -= timingHandle.Depth * 0.12f;
if (maxRectDept < timingHandle.Depth)
maxRectDept = timingHandle.Depth;
if (startX == endX)
{
continue;
}
var minPos = pos + new Vector2((uint)startX, 20 * timingHandle.Depth);
var maxPos = pos + new Vector2((uint)endX, 20 * (timingHandle.Depth + 1));
ImGui.GetWindowDrawList().AddRectFilled(
minPos,
maxPos,
ImGui.GetColorU32(rectColor));
ImGui.GetWindowDrawList().AddTextClippedEx(minPos, maxPos, timingHandle.Name, null, Vector2.Zero, null);
// Show tooltip when hovered
var mousePos = ImGui.GetMousePos();
if (mousePos.X > pos.X + startX && mousePos.X < pos.X + endX &&
mousePos.Y > pos.Y + (20 * timingHandle.Depth) &&
mousePos.Y < pos.Y + (20 * (timingHandle.Depth + 1)))
{
ImGui.BeginTooltip();
ImGui.Text(timingHandle.Name);
ImGui.Text(timingHandle.MemberName);
ImGui.Text($"{timingHandle.FileName}:{timingHandle.LineNumber}");
ImGui.Text($"Duration: {timingHandle.Duration}ms");
ImGui.EndTooltip();
}
}
uint eventTextDepth = maxRectDept + 2;
foreach (var timingEvent in Timings.Events)
{
var startX = (timingEvent.StartTime - this.min) / (this.max - this.min) * width;
if (startX < 0 || startX > width)
{
continue;
}
ImGui.GetWindowDrawList().AddLine(
pos + new Vector2((uint)startX, 0),
pos + new Vector2((uint)startX, childHeight),
ImGui.GetColorU32(ImGuiColors.ParsedOrange),
1.5f);
const uint padding = 5;
var textSize = ImGui.CalcTextSize(timingEvent.Name);
var textPos = pos + new Vector2((uint)startX + padding, eventTextDepth * 20);
if (textPos.X + textSize.X > pos.X + width - 20)
{
textPos.X = pos.X + (uint)startX - textSize.X - padding;
}
ImGui.GetWindowDrawList().AddText(
textPos,
ImGui.GetColorU32(ImGuiColors.DalamudWhite),
timingEvent.Name);
}
}
ImGui.EndChild();
var sliderMin = (float)this.min / 1000f;
if (ImGui.SliderFloat("Start", ref sliderMin, (float)actualMin / 1000f, (float)this.max / 1000f, "%.1fs"))
{
this.min = sliderMin * 1000f;
}
var sliderMax = (float)this.max / 1000f;
if (ImGui.SliderFloat("End", ref sliderMax, (float)this.min / 1000f, (float)actualMax / 1000f, "%.1fs"))
{
this.max = sliderMax * 1000f;
}
var sizeShown = (float)(this.max - this.min);
var sizeActual = (float)(actualMax - actualMin);
if (ImGui.SliderFloat("Size", ref sizeShown, sizeActual / 10f, sizeActual, "%.1fs"))
{
this.max = this.min + sizeShown;
}
ImGui.Text("Min: " + actualMin.ToString("0.000"));
ImGui.Text("Max: " + actualMax.ToString("0.000"));
ImGui.Text("Timings: " + Timings.AllTimings.Count);
}
}

View file

@ -277,7 +277,7 @@ namespace Dalamud.Interface.Internal.Windows
ImGui.TextColored(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingsSystemMenuMsgHint", "Add buttons for Dalamud plugins and settings to the system menu."));
ImGui.Checkbox(Loc.Localize("DalamudSettingsDisableRmtFiltering", "Disable RMT Filtering"), ref this.disableRmtFiltering);
ImGui.TextColored(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingsDisableRmtFilteringMsgHint", "Disable dalamud's built-in RMT ad filtering."));
ImGui.TextColored(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingsDisableRmtFilteringMsgHint", "Disable Dalamud's built-in RMT ad filtering."));
ImGuiHelpers.ScaledDummy(5);
@ -367,6 +367,8 @@ namespace Dalamud.Interface.Internal.Windows
ImGuiHelpers.ScaledDummy(3);
ImGui.Text(Loc.Localize("DalamudSettingsFontResolutionLevel", "Font resolution level"));
if (interfaceManager.FontResolutionLevelOverride != null)
this.fontResolutionLevel = interfaceManager.FontResolutionLevelOverride.Value;
if (ImGui.Combo("##DalamudSettingsFontResolutionLevelCombo", ref this.fontResolutionLevel, this.fontResolutionLevelStrings, this.fontResolutionLevelStrings.Length))
{
interfaceManager.FontResolutionLevelOverride = this.fontResolutionLevel;
@ -375,7 +377,9 @@ namespace Dalamud.Interface.Internal.Windows
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey);
ImGui.TextWrapped(string.Format(
Loc.Localize("DalamudSettingsFontResolutionLevelHint", "This option allows Dalamud fonts to look better. If your game crashes when changing this option, your PC does not support high font resolutions in Dalamud - you will have to use a lower one.\nCurrent font atlas size is {0}px * {1}px."),
Loc.Localize(
"DalamudSettingsFontResolutionLevelHint",
"This option allows Dalamud fonts to look better.\n* If your game crashes right away, or the option reverts, when changing this option, your PC does not support high font resolutions in Dalamud - you will have to use a lower one.\n* If it doesn't crash or revert immediately, then you can keep the new choice indefinitely as it's not going to crash your game once it worked.\n* Either choose the 3rd or 5th option. Use other options only when neither works well.\n* Current font atlas size is {0}px * {1}px."),
ImGui.GetIO().Fonts.TexWidth,
ImGui.GetIO().Fonts.TexHeight));
ImGui.PopStyleColor();

View file

@ -25,7 +25,8 @@ namespace Dalamud.Interface
private readonly Stopwatch stopwatch;
private readonly string namespaceName;
private bool hasErrorWindow;
private bool hasErrorWindow = false;
private bool lastFrameUiHideState = false;
/// <summary>
/// Initializes a new instance of the <see cref="UiBuilder"/> class and registers it.
@ -78,6 +79,18 @@ namespace Dalamud.Interface
/// </summary>
public event Action AfterBuildFonts;
/// <summary>
/// Gets or sets an action that is called when plugin UI or interface modifications are supposed to be hidden.
/// These may be fired consecutively.
/// </summary>
public event Action ShowUi;
/// <summary>
/// Gets or sets an action that is called when plugin UI or interface modifications are supposed to be shown.
/// These may be fired consecutively.
/// </summary>
public event Action HideUi;
/// <summary>
/// Gets the default Dalamud font based on Noto Sans CJK Medium in 17pt - supporting all game languages and icons.
/// </summary>
@ -137,6 +150,36 @@ namespace Dalamud.Interface
/// </summary>
public ulong FrameCount { get; private set; } = 0;
/// <summary>
/// Gets a value indicating whether or not a cutscene is playing.
/// </summary>
public bool CutsceneActive
{
get
{
var condition = Service<Condition>.Get();
return condition[ConditionFlag.OccupiedInCutSceneEvent]
|| condition[ConditionFlag.WatchingCutscene78];
}
}
/// <summary>
/// Gets a value indicating whether or not gpose is active.
/// </summary>
public bool GposeActive
{
get
{
var condition = Service<Condition>.Get();
return condition[ConditionFlag.WatchingCutscene];
}
}
/// <summary>
/// Gets a value indicating whether this plugin should modify the game's interface at this time.
/// </summary>
public bool ShouldModifyUi => Service<InterfaceManager>.GetNullable()?.IsDispatchingEvents ?? true;
/// <summary>
/// Gets or sets a value indicating whether statistics about UI draw time should be collected.
/// </summary>
@ -166,25 +209,6 @@ namespace Dalamud.Interface
/// </summary>
internal List<long> DrawTimeHistory { get; set; } = new List<long>();
private bool CutsceneActive
{
get
{
var condition = Service<Condition>.Get();
return condition[ConditionFlag.OccupiedInCutSceneEvent]
|| condition[ConditionFlag.WatchingCutscene78];
}
}
private bool GposeActive
{
get
{
var condition = Service<Condition>.Get();
return condition[ConditionFlag.WatchingCutscene];
}
}
/// <summary>
/// Loads an image from the specified file.
/// </summary>
@ -261,16 +285,49 @@ namespace Dalamud.Interface
this.OpenConfigUi?.Invoke();
}
/// <summary>
/// Notify this UiBuilder about plugin UI being hidden.
/// </summary>
internal void NotifyHideUi()
{
this.HideUi?.Invoke();
}
/// <summary>
/// Notify this UiBuilder about plugin UI being shown.
/// </summary>
internal void NotifyShowUi()
{
this.ShowUi?.Invoke();
}
private void OnDraw()
{
var configuration = Service<DalamudConfiguration>.Get();
var gameGui = Service<GameGui>.Get();
var interfaceManager = Service<InterfaceManager>.Get();
if ((gameGui.GameUiHidden && configuration.ToggleUiHide && !(this.DisableUserUiHide || this.DisableAutomaticUiHide)) ||
(this.CutsceneActive && configuration.ToggleUiHideDuringCutscenes && !(this.DisableCutsceneUiHide || this.DisableAutomaticUiHide)) ||
(this.GposeActive && configuration.ToggleUiHideDuringGpose && !(this.DisableGposeUiHide || this.DisableAutomaticUiHide)))
if ((gameGui.GameUiHidden && configuration.ToggleUiHide &&
!(this.DisableUserUiHide || this.DisableAutomaticUiHide)) ||
(this.CutsceneActive && configuration.ToggleUiHideDuringCutscenes &&
!(this.DisableCutsceneUiHide || this.DisableAutomaticUiHide)) ||
(this.GposeActive && configuration.ToggleUiHideDuringGpose &&
!(this.DisableGposeUiHide || this.DisableAutomaticUiHide)))
{
if (!this.lastFrameUiHideState)
{
this.lastFrameUiHideState = true;
this.HideUi?.Invoke();
}
return;
}
if (this.lastFrameUiHideState)
{
this.lastFrameUiHideState = false;
this.ShowUi?.Invoke();
}
if (!interfaceManager.FontsReady)
return;

View file

@ -65,7 +65,7 @@ namespace Dalamud.Interface.Windowing
public ImGuiCond PositionCondition { get; set; }
/// <summary>
/// Gets or sets the size of the window.
/// Gets or sets the size of the window. The size provided will be scaled by the global scale.
/// </summary>
public Vector2? Size { get; set; }
@ -75,7 +75,7 @@ namespace Dalamud.Interface.Windowing
public ImGuiCond SizeCondition { get; set; }
/// <summary>
/// Gets or sets the size constraints of the window.
/// Gets or sets the size constraints of the window. The size constraints provided will be scaled by the global scale.
/// </summary>
public WindowSizeConstraints? SizeConstraints { get; set; }

View file

@ -1393,6 +1393,18 @@ namespace Dalamud
WriteCombine = 0x400,
}
/// <summary>
/// See https://docs.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-setevent
/// Sets the specified event object to the signaled state.
/// </summary>
/// <param name="hEvent">A handle to the event object. The CreateEvent or OpenEvent function returns this handle.</param>
/// <returns>
/// If the function succeeds, the return value is nonzero.
/// If the function fails, the return value is zero. To get extended error information, call GetLastError.
/// </returns>
[DllImport("kernel32.dll")]
public static extern bool SetEvent(IntPtr hEvent);
/// <summary>
/// See https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-freelibrary.
/// Frees the loaded dynamic-link library (DLL) module and, if necessary, decrements its reference count. When the reference

View file

@ -15,6 +15,7 @@ using Dalamud.Game.Text.Sanitizer;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Interface;
using Dalamud.Interface.Internal;
using Dalamud.Plugin.Internal;
using Dalamud.Plugin.Ipc;
using Dalamud.Plugin.Ipc.Exceptions;
@ -139,6 +140,11 @@ namespace Dalamud.Plugin
/// <summary>
/// Gets a value indicating whether Dalamud is running in Debug mode or the /xldev menu is open. This can occur on release builds.
/// </summary>
public bool IsDevMenuOpen => Service<DalamudInterface>.GetNullable() is { IsDevMenuOpen: true }; // Can be null during boot
/// <summary>
/// Gets a value indicating whether a debugger is attached.
/// </summary>
public bool IsDebugging => Debugger.IsAttached;
/// <summary>

View file

@ -12,6 +12,7 @@ using System.Threading.Tasks;
using CheapLoc;
using Dalamud.Configuration;
using Dalamud.Configuration.Internal;
using Dalamud.Game;
using Dalamud.Game.Gui;
using Dalamud.Game.Gui.Dtr;
using Dalamud.Game.Text;
@ -340,6 +341,9 @@ internal partial class PluginManager : IDisposable
{
this.PluginsReady = true;
this.NotifyInstalledPluginsChanged();
// Save signatures, makes sense to do it here since all plugins will be loaded
Service<SigScanner>.Get().Save();
});
}

131
Dalamud/Utility/MapUtil.cs Normal file
View file

@ -0,0 +1,131 @@
using System.Numerics;
using Lumina.Excel.GeneratedSheets;
namespace Dalamud.Utility;
/// <summary>
/// Utility helper class for game maps and coordinate translations that don't require state.
///
/// The conversion methods were found in 89 54 24 10 56 41 55 41 56 48 81 EC, which itself was found by looking for
/// uses of AddonText 1631.
/// </summary>
public static class MapUtil
{
/// <summary>
/// Helper method to convert one of the game's Vector3 X/Z provided by the game to a map coordinate suitable for
/// display to the player.
/// </summary>
/// <param name="value">The raw float of a game Vector3 X or Z coordinate to convert.</param>
/// <param name="scale">The scale factor of the map, generally retrieved from Lumina.</param>
/// <param name="offset">The dimension offset for either X or Z, generally retrieved from Lumina.</param>
/// <returns>Returns a converted float for display to the player.</returns>
public static float ConvertWorldCoordXZToMapCoord(float value, uint scale, int offset)
{
// Derived from E8 ?? ?? ?? ?? 0F B7 4B 1C and simplified.
return (0.02f * offset) + (2048f / scale) + (0.02f * value) + 1.0f;
}
/// <summary>
/// Helper method to convert a game Vector3 Y coordinate to a map coordinate suitable for display to the player.
/// </summary>
/// <param name="value">The raw float of a game Vector3 Y coordinate to convert.</param>
/// <param name="zOffset">The zOffset for this map. Retrieved from TerritoryTypeTransient.</param>
/// <param name="correctZOffset">Optionally enable Z offset correction. When a Z offset of -10,000 is set, replace
/// it with 0 for calculation purposes to show a more sane Z coordinate.</param>
/// <returns>Returns a converted float for display to the player.</returns>
public static float ConvertWorldCoordYToMapCoord(float value, int zOffset, bool correctZOffset = false)
{
// Derived from 48 83 EC 38 80 3D ?? ?? ?? ?? ?? 0F 29 74 24
// zOffset of -10000 indicates that the map should not display a Z coordinate.
if (zOffset == -10000 && correctZOffset) zOffset = 0;
return (value - zOffset) / 100;
}
/// <summary>
/// All-in-one helper method to convert a World Coordinate (internal to the game) to a Map Coordinate (visible to
/// players in the minimap/elsewhere).
/// </summary>
/// <remarks>
/// Note that this method will swap Y and Z in the resulting Vector3 to appropriately reflect the game's display.
/// </remarks>
/// <param name="worldCoordinates">A Vector3 of raw World coordinates from the game.</param>
/// <param name="xOffset">The offset to apply to the incoming X parameter, generally Lumina's Map.OffsetX.</param>
/// <param name="yOffset">The offset to apply to the incoming Y parameter, generally Lumina's Map.OffsetY.</param>
/// <param name="zOffset">The offset to apply to the incoming Z parameter, generally Lumina's TerritoryTypeTransient.OffsetZ.</param>
/// <param name="scale">The global scale to apply to the incoming X and Y parameters, generally Lumina's Map.SizeFactor.</param>
/// <param name="correctZOffset">An optional mode to "correct" a Z offset of -10000 to be a more human-friendly value.</param>
/// <returns>Returns a Vector3 representing visible map coordinates.</returns>
public static Vector3 WorldToMap(
Vector3 worldCoordinates,
int xOffset = 0,
int yOffset = 0,
int zOffset = 0,
uint scale = 100,
bool correctZOffset = false)
{
return new Vector3(
ConvertWorldCoordXZToMapCoord(worldCoordinates.X, scale, xOffset),
ConvertWorldCoordXZToMapCoord(worldCoordinates.Z, scale, yOffset),
ConvertWorldCoordYToMapCoord(worldCoordinates.Y, zOffset, correctZOffset));
}
/// <summary>
/// All-in-one helper method to convert a World Coordinate (internal to the game) to a Map Coordinate (visible to
/// players in the minimap/elsewhere).
/// </summary>
/// <remarks>
/// Note that this method will swap Y and Z to appropriately reflect the game's display.
/// </remarks>
/// <param name="worldCoordinates">A Vector3 of raw World coordinates from the game.</param>
/// <param name="map">A Lumina map to use for offset/scale information.</param>
/// <param name="territoryTransient">A TerritoryTypeTransient to use for Z offset information.</param>
/// <param name="correctZOffset">An optional mode to "correct" a Z offset of -10000 to be a more human-friendly value.</param>
/// <returns>Returns a Vector3 representing visible map coordinates.</returns>
public static Vector3 WorldToMap(
Vector3 worldCoordinates, Map map, TerritoryTypeTransient territoryTransient, bool correctZOffset = false)
{
return WorldToMap(
worldCoordinates,
map.OffsetX,
map.OffsetY,
territoryTransient.OffsetZ,
map.SizeFactor,
correctZOffset);
}
/// <summary>
/// All-in-one helper method to convert a World Coordinate (internal to the game) to a Map Coordinate (visible to
/// players in the minimap/elsewhere).
/// </summary>
/// <param name="worldCoordinates">A Vector2 of raw World coordinates from the game.</param>
/// <param name="xOffset">The offset to apply to the incoming X parameter, generally Lumina's Map.OffsetX.</param>
/// <param name="yOffset">The offset to apply to the incoming Y parameter, generally Lumina's Map.OffsetY.</param>
/// <param name="scale">The global scale to apply to the incoming X and Y parameters, generally Lumina's Map.SizeFactor.</param>
/// <returns>Returns a Vector2 representing visible map coordinates.</returns>
public static Vector2 WorldToMap(
Vector2 worldCoordinates,
int xOffset = 0,
int yOffset = 0,
uint scale = 100)
{
return new Vector2(
ConvertWorldCoordXZToMapCoord(worldCoordinates.X, scale, xOffset),
ConvertWorldCoordXZToMapCoord(worldCoordinates.Y, scale, yOffset));
}
/// <summary>
/// All-in-one helper method to convert a World Coordinate (internal to the game) to a Map Coordinate (visible to
/// players in the minimap/elsewhere).
/// </summary>
/// <param name="worldCoordinates">A Vector2 of raw World coordinates from the game.</param>
/// <param name="map">A Lumina map to use for offset/scale information.</param>
/// <returns>Returns a Vector2 representing visible map coordinates.</returns>
public static Vector2 WorldToMap(Vector2 worldCoordinates, Map map)
{
return WorldToMap(worldCoordinates, map.OffsetX, map.OffsetY, map.SizeFactor);
}
}

View file

@ -0,0 +1,56 @@
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
namespace Dalamud.Utility.Numerics;
/// <summary>
/// Extension methods for vectors.
/// </summary>
[SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Redundant.")]
public static class VectorExtensions
{
public static Vector4 WithX(this Vector4 v, float x)
{
return new Vector4(x, v.Y, v.Z, v.W);
}
public static Vector4 WithY(this Vector4 v, float y)
{
return new Vector4(v.X, y, v.Z, v.W);
}
public static Vector4 WithZ(this Vector4 v, float z)
{
return new Vector4(v.X, v.Y, z, v.W);
}
public static Vector4 WithW(this Vector4 v, float w)
{
return new Vector4(v.X, v.Y, v.Z, w);
}
public static Vector3 WithX(this Vector3 v, float x)
{
return new Vector3(x, v.Y, v.Z);
}
public static Vector3 WithY(this Vector3 v, float y)
{
return new Vector3(v.X, y, v.Z);
}
public static Vector3 WithZ(this Vector3 v, float z)
{
return new Vector3(v.X, v.Y, z);
}
public static Vector2 WithX(this Vector2 v, float x)
{
return new Vector2(x, v.Y);
}
public static Vector2 WithY(this Vector2 v, float y)
{
return new Vector2(v.X, y);
}
}

View file

@ -1,3 +1,5 @@
using System.Diagnostics.CodeAnalysis;
namespace Dalamud.Utility
{
/// <summary>
@ -18,13 +20,13 @@ namespace Dalamud.Utility
/// </summary>
/// <param name="value">The string to test.</param>
/// <returns>true if the value parameter is null or an empty string (""); otherwise, false.</returns>
public static bool IsNullOrEmpty(this string? value) => string.IsNullOrEmpty(value);
public static bool IsNullOrEmpty([NotNullWhen(false)] this string? value) => string.IsNullOrEmpty(value);
/// <summary>
/// Indicates whether a specified string is null, empty, or consists only of white-space characters.
/// </summary>
/// <param name="value">The string to test.</param>
/// <returns>true if the value parameter is null or an empty string (""), or if value consists exclusively of white-space characters.</returns>
public static bool IsNullOrWhitespace(this string? value) => string.IsNullOrWhiteSpace(value);
public static bool IsNullOrWhitespace([NotNullWhen(false)] this string? value) => string.IsNullOrWhiteSpace(value);
}
}

View file

@ -0,0 +1,49 @@
using System;
namespace Dalamud.Utility;
/// <summary>
/// Helpers for working with thread safety.
/// </summary>
public static class ThreadSafety
{
[ThreadStatic]
private static bool isMainThread;
/// <summary>
/// Gets a value indicating whether the current thread is the main thread.
/// </summary>
public static bool IsMainThread => isMainThread;
/// <summary>
/// Throws an exception when the current thread is not the main thread.
/// </summary>
/// <exception cref="InvalidOperationException">Thrown when the current thread is not the main thread.</exception>
public static void AssertMainThread()
{
if (!isMainThread)
{
throw new InvalidOperationException("Not on main thread!");
}
}
/// <summary>
/// Throws an exception when the current thread is the main thread.
/// </summary>
/// <exception cref="InvalidOperationException">Thrown when the current thread is the main thread.</exception>
public static void AssertNotMainThread()
{
if (isMainThread)
{
throw new InvalidOperationException("On main thread!");
}
}
/// <summary>
/// Marks a thread as the main thread.
/// </summary>
internal static void MarkMainThread()
{
isMainThread = true;
}
}

View file

@ -0,0 +1,35 @@
namespace Dalamud.Utility.Timing;
public class TimingEvent
{
internal TimingEvent(string name)
{
this.Name = name;
this.StartTime = Timings.Stopwatch.Elapsed.TotalMilliseconds;
}
/// <summary>
/// Gets the time this timing started.
/// </summary>
public double StartTime { get; private set; }
/// <summary>
/// Gets the name of the timing.
/// </summary>
public string Name { get; init; }
/// <summary>
/// Gets the member that created this timing.
/// </summary>
public string? MemberName { get; init; }
/// <summary>
/// Gets the file name that created this timing.
/// </summary>
public string? FileName { get; init; }
/// <summary>
/// Gets the line number that created this timing.
/// </summary>
public int LineNumber { get; init; }
}

View file

@ -0,0 +1,93 @@
using System;
using System.Diagnostics;
using System.Linq;
namespace Dalamud.Utility.Timing;
/// <summary>
/// Class used for tracking a time interval taken.
/// </summary>
[DebuggerDisplay("{Name} - {Duration}")]
public sealed class TimingHandle : TimingEvent, IDisposable
{
/// <summary>
/// Initializes a new instance of the <see cref="TimingHandle"/> class.
/// </summary>
/// <param name="name">The name of this timing.</param>
internal TimingHandle(string name) : base(name)
{
this.Parent = Timings.Current.Value;
Timings.Current.Value = this;
lock (Timings.AllTimings)
{
if (this.Parent != null)
{
this.ChildCount++;
}
this.EndTime = this.StartTime;
this.IsMainThread = ThreadSafety.IsMainThread;
if (Timings.ActiveTimings.Count > 0)
{
this.Depth = Timings.ActiveTimings.Max(x => x.Depth) + 1;
}
Timings.ActiveTimings.Add(this);
}
}
/// <summary>
/// Gets the time this timing ended.
/// </summary>
public double EndTime { get; private set; }
/// <summary>
/// Gets the duration of this timing.
/// </summary>
public double Duration => Math.Floor(this.EndTime - this.StartTime);
/// <summary>
/// Gets the parent timing.
/// </summary>
public TimingHandle? Parent { get; private set; }
/// <summary>
/// Gets a value indicating whether or not this timing has already returned to its parent.
/// </summary>
public bool Returned { get; private set; }
/// <summary>
/// Gets a value indicating whether or not this timing was started on the main thread.
/// </summary>
public bool IsMainThread { get; private set; }
/// <summary>
/// Gets the number of child timings.
/// </summary>
public uint ChildCount { get; private set; }
/// <summary>
/// Gets the depth of this timing.
/// </summary>
public uint Depth { get; private set; }
/// <inheritdoc/>
public void Dispose()
{
this.EndTime = Timings.Stopwatch.Elapsed.TotalMilliseconds;
Timings.Current.Value = this.Parent;
lock (Timings.AllTimings)
{
if (this.Duration > 1 || this.ChildCount > 0)
{
Timings.AllTimings.Add(this);
this.Returned = this.Parent != null && Timings.ActiveTimings.Contains(this.Parent);
}
Timings.ActiveTimings.Remove(this);
}
}
}

View file

@ -0,0 +1,75 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Threading;
namespace Dalamud.Utility.Timing;
/// <summary>
/// Class for measuring time taken in various subsystems.
/// </summary>
public static class Timings
{
/// <summary>
/// Stopwatch used for timing.
/// </summary>
internal static readonly Stopwatch Stopwatch = Stopwatch.StartNew();
/// <summary>
/// All concluded timings.
/// </summary>
internal static readonly List<TimingHandle> AllTimings = new();
/// <summary>
/// All active timings.
/// </summary>
internal static readonly List<TimingHandle> ActiveTimings = new();
internal static readonly List<TimingEvent> Events = new();
/// <summary>
/// Current active timing entry.
/// </summary>
internal static readonly AsyncLocal<TimingHandle> Current = new();
/// <summary>
/// Start a new timing.
/// </summary>
/// <param name="name">The name of the timing.</param>
/// <param name="memberName">Name of the calling member.</param>
/// <param name="sourceFilePath">Name of the calling file.</param>
/// <param name="sourceLineNumber">Name of the calling line number.</param>
/// <returns>Disposable that stops the timing once disposed.</returns>
public static IDisposable Start(string name, [CallerMemberName] string memberName = "", [CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0)
{
return new TimingHandle(name)
{
MemberName = memberName,
FileName = sourceFilePath,
LineNumber = sourceLineNumber,
};
}
/// <summary>
/// Record a one-time event.
/// </summary>
/// <param name="name">The name of the timing.</param>
/// <param name="memberName">Name of the calling member.</param>
/// <param name="sourceFilePath">Name of the calling file.</param>
/// <param name="sourceLineNumber">Name of the calling line number.</param>
/// <returns>Disposable that stops the timing once disposed.</returns>
public static void Event(string name, [CallerMemberName] string memberName = "", [CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0)
{
lock (Events)
{
Events.Add(new TimingEvent(name)
{
MemberName = memberName,
FileName = sourceFilePath,
LineNumber = sourceLineNumber,
});
}
}
}

407
Dalamud/licenses.txt Normal file
View file

@ -0,0 +1,407 @@
============== SRELL ==============
/*****************************************************************************
**
** SRELL (std::regex-like library) version 3.009
**
** Copyright (c) 2012-2022, Nozomu Katoo. All rights reserved.
**
** Redistribution and use in source and binary forms, with or without
** modification, are permitted provided that the following conditions are
** met:
**
** 1. Redistributions of source code must retain the above copyright notice,
** this list of conditions and the following disclaimer.
**
** 2. Redistributions in binary form must reproduce the above copyright
** notice, this list of conditions and the following disclaimer in the
** documentation and/or other materials provided with the distribution.
**
** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS
** IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
** THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
** PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
** CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
** EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
** PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
** PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
** LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
** NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
** SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
**
******************************************************************************
**/
============== MINHOOK ==============
MinHook - The Minimalistic API Hooking Library for x64/x86
Copyright (C) 2009-2017 Tsuda Kageyu.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER
OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
================================================================================
Portions of this software are Copyright (c) 2008-2009, Vyacheslav Patkov.
================================================================================
Hacker Disassembler Engine 32 C
Copyright (c) 2008-2009, Vyacheslav Patkov.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-------------------------------------------------------------------------------
Hacker Disassembler Engine 64 C
Copyright (c) 2008-2009, Vyacheslav Patkov.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
============== NLOHMANN/JSON ==============
__ _____ _____ _____
__| | __| | | | JSON for Modern C++
| | |__ | | | | | | version 3.10.5
|_____|_____|_____|_|___| https://github.com/nlohmann/json
Licensed under the MIT License <http://opensource.org/licenses/MIT>.
SPDX-License-Identifier: MIT
Copyright (c) 2013-2022 Niels Lohmann <http://nlohmann.me>.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
============== NMD ==============
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <https://unlicense.org>
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <https://unlicense.org>
============== FFXIVCLIENTSTRUCTS ==============
MIT License
Copyright (c) 2021 aers
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
============== DOTNETCOREPLUGINS ==============
https://github.com/natemcmaster/DotNetCorePlugins
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS

View file

@ -5,6 +5,7 @@
#include <filesystem>
#include <iostream>
#include "nethost/nethost.h"
#include "..\..\Dalamud.Boot\logging.h"
CoreCLR::CoreCLR(void* calling_module)
: m_calling_module(calling_module)
@ -82,7 +83,7 @@ int CoreCLR::load_runtime(const std::wstring& runtime_config_path, const struct
// Success_HostAlreadyInitialized
if (result == 1)
{
printf("Success_HostAlreadyInitialized (0x1) ");
logging::I("Success_HostAlreadyInitialized (0x1)");
result = 0;
}

View file

@ -5,6 +5,7 @@
#include <Windows.h>
#include <Shlobj.h>
#include "CoreCLR.h"
#include "..\..\Dalamud.Boot\logging.h"
FILE* g_CmdStream;
void ConsoleSetup(const std::wstring console_name)
@ -16,6 +17,7 @@ void ConsoleSetup(const std::wstring console_name)
freopen_s(&g_CmdStream, "CONOUT$", "w", stdout);
freopen_s(&g_CmdStream, "CONOUT$", "w", stderr);
freopen_s(&g_CmdStream, "CONIN$", "r", stdin);
SetConsoleOutputCP(CP_UTF8);
}
void ConsoleTeardown()
@ -27,6 +29,7 @@ std::optional<CoreCLR> g_clr;
int InitializeClrAndGetEntryPoint(
void* calling_module,
bool enable_etw,
std::wstring runtimeconfig_path,
std::wstring module_path,
std::wstring entrypoint_assembly_name,
@ -42,6 +45,13 @@ int InitializeClrAndGetEntryPoint(
SetEnvironmentVariable(L"DOTNET_legacyCorruptedStateExceptionsPolicy", L"1");
SetEnvironmentVariable(L"COMPLUS_ForceENC", L"1");
// Enable Dynamic PGO
SetEnvironmentVariable(L"DOTNET_TieredPGO", L"1");
SetEnvironmentVariable(L"DOTNET_TC_QuickJitForLoops", L"1");
SetEnvironmentVariable(L"DOTNET_ReadyToRun", L"1");
SetEnvironmentVariable(L"COMPlus_ETWEnabled", enable_etw ? L"1" : L"0");
wchar_t* dotnet_path;
wchar_t* _appdata;
@ -61,7 +71,7 @@ int InitializeClrAndGetEntryPoint(
if (result != 0)
{
printf("Error: Unable to get RoamingAppData path (err=%d)\n", result);
logging::E("Unable to get RoamingAppData path (err={})", result);
return result;
}
@ -71,13 +81,13 @@ int InitializeClrAndGetEntryPoint(
// =========================================================================== //
wprintf(L"with dotnet_path: %s\n", dotnet_path);
wprintf(L"with config_path: %s\n", runtimeconfig_path.c_str());
wprintf(L"with module_path: %s\n", module_path.c_str());
logging::I("with dotnet_path: {}", dotnet_path);
logging::I("with config_path: {}", runtimeconfig_path);
logging::I("with module_path: {}", module_path);
if (!std::filesystem::exists(dotnet_path))
{
printf("Error: Unable to find .NET runtime path\n");
logging::E("Error: Unable to find .NET runtime path");
return 1;
}
@ -88,13 +98,13 @@ int InitializeClrAndGetEntryPoint(
dotnet_path,
};
printf("Loading hostfxr... ");
logging::I("Loading hostfxr...");
if ((result = g_clr->load_hostfxr(&init_parameters)) != 0)
{
printf("\nError: Failed to load the `hostfxr` library (err=0x%08x)\n", result);
logging::E("Failed to load the `hostfxr` library (err=0x{:08x})", result);
return result;
}
printf("Done!\n");
logging::I("Done!");
// =========================================================================== //
@ -105,17 +115,17 @@ int InitializeClrAndGetEntryPoint(
dotnet_path,
};
printf("Loading coreclr... ");;
logging::I("Loading coreclr... ");
if ((result = g_clr->load_runtime(runtimeconfig_path, &runtime_parameters)) != 0)
{
printf("\nError: Failed to load coreclr (err=%d)\n", result);
logging::E("Failed to load coreclr (err=0x{:08X})", static_cast<uint32_t>(result));
return result;
}
printf("Done!\n");
logging::I("Done!");
// =========================================================================== //
printf("Loading module... ");
logging::I("Loading module...");
if ((result = g_clr->load_assembly_and_get_function_pointer(
module_path.c_str(),
entrypoint_assembly_name.c_str(),
@ -123,10 +133,10 @@ int InitializeClrAndGetEntryPoint(
entrypoint_delegate_type_name.c_str(),
nullptr, entrypoint_fn)) != 0)
{
printf("\nError: Failed to load module (err=%d)\n", result);
logging::E("Failed to load module (err={})", result);
return result;
}
printf("Done!\n");
logging::I("Done!");
// =========================================================================== //

View file

@ -3,6 +3,7 @@ void ConsoleTeardown();
int InitializeClrAndGetEntryPoint(
void* calling_module,
bool enable_etw,
std::wstring runtimeconfig_path,
std::wstring module_path,
std::wstring entrypoint_assembly_name,

@ -1 +1 @@
Subproject commit a2972adbd333d0ad9c127fff1cfc288d1cecf6b4
Subproject commit a0244fc290eb64fccfac2fd36704f7371f5c5c7f

1
lib/Nomade040-nmd Submodule

@ -0,0 +1 @@
Subproject commit 33ac3b62c7d1eb28ae6b71d4dd78aa133ef96488

@ -0,0 +1 @@
Subproject commit 4a455528f61b5a375b1f9d44e7d296d47f18bb18

22091
lib/nlohmann-json/json.hpp Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,484 @@
20220511; version 3.009:
* Fixed an optimisation bug that caused /abcd|ab/ not to match "abc".
20220504; version 3.008:
* Fixed the behaviour of [^\P{...}] when the icase flag is set, as it
behaved similarly to the one in v-mode that has been proposed in
TC39.
20220429; version 3.007:
* Further modification to the counter mechanism.
20220428; version 3.006:
* Modified the mechanism of the counter used for repetition.
* Re-removed the implementation of linear search for small character
classes.
20220424; version 3.005:
* Fixed a bug that caused /(?<=$.*)/ not to match the end of "a" when
the multiline flag is set
* Preparations for \A, \z, (?m:) that have been proposed in TC39.
20220420; version 3.004:
* Added a new optimisation for /A*B/ and /A+B/ where a character class
A overlaps a character or character class B, such as /[A-Za-z]+ing/,
/".*"/.
20220416; version 3.003:
* Combined two optimisation functions into one.
* Reduced the amount of code for lookaround (lookahead and lookbehind)
assertions.
20220416; version 3.002:
* Fixed a bug that caused regex_match or regex_search with the
match_continuous flag being set to fail when the entry point
selector introduced in version 3.000 was used internally.
20211025; version 3.001:
* Removed the code for splitting counter as it seemed to be no effect
or to make performance a bit worse.
* Fixed potential bugs.
* Minor improvements.
20211023; version 3.000:
* Updated srell_ucfdata2.hpp and srell_updata.hpp to support Unicode
14.0.0.
* Updated unicode/updataout.cpp to support Unicode 14. (Support in
advance new script names that are expected to be available in RegExp
of ECMAScript 2022).
* Changed the type used to store a Unicode value when char32_t is not
available, from an "unsigned integer type with width of at least 21
bits" to a "one of at least 32 bits".
* Changed the type used to store a repetition count or character class
number when char32_t is not available, from "unsigned int" to
"unsigned integer type of at least 32-bit width".
* Added overflow check in the function that translates digits into a
numeric value. For example, while up to the previous version
/a{0,4294967297}/ was treated as /a{0,1}/ because of overflow when
the unsigned int type is 32-bit width, SRELL now throws error_brace
in cases like this.
* Fixed a bug that caused /[^;]*^;?/ not to match the beginning of an
input string when the multiline flag is not set.
* Implemented a very simple and limited entry point selector.
20211004; version 2.930:
* Added new typedefs whose prefix is u1632w- and support UTF-16 or
UTF-32 depending on the value of WCHAR_MAX. (When 0xFFFF <=
WCHAR_MAX < 0x10FFFF, u1632w- types are aliases of u16w- types.
When 0x10FFFF <= WCHAR_MAX, u1632w- types are aliases of u32w-
types).
* Reduced the amount of memory used for Eytzinger layout search.
* Various improvements. (Some of them are based on suggestions to NIRE
by Marko Njezic).
20210624; version 2.920:
* Added a new optimisation for the quantifier '?' (I.e., {0,1}).
* Changed the version number of the ECMAScript specification
referenced in misc/sample01.cpp to 2021.
20210429; version 2.912:
* Fixed another bug in the optimisation introduced in version 2.900,
which caused /aa|a|aa/ not to match "a" (Thanks to Jan Schrötter for
the report).
Incidentally, this optimisation can be disabled by defining
SRELLDBG_NO_BRANCH_OPT2 prior to including srell.hpp.
20210424; version 2.911:
* Fixed a bug in the optimisation introduced in version 2.900, which
caused /abc|ab|ac/ not to match "ac". (Thanks for the bug report [As
my email to the reporter was rejected by the email server and
returned, it is unclear whether mentioning the name here is okay
with the reporter. So, I refrain]).
20210407; version 2.910:
* Fixed a potential memory leak in move assignment operators used by
the pattern compiler since 2.900. (Thanks to Michal Švec for the
report).
20210214; version 2.901:
* Removed redundant template specialisations.
20210214; version 2.900:
* Added a new optimisation for the alternative expression that consist
of string literals, such as /abc|abd|acde/.
* Fixed the problem that brought u(8|16)[cs]regex_(token_)?iterator
(i.e., regex (token) iterators specialised for char8_t or char16_t)
to a compile error.
* Minor improvements.
20210131; version 2.810:
* Improved internal UTF-8 iterators.
20200724; version 2.800:
* Introduced the Eytzinger layout for binary search in the character
class.
* Reimplemented linear search for small character classes.
* Modified handling of the property data used for parsing the name for
a named capturing group. Now they are loaded only when needed
instead of being loaded into an instance of basic_regex always.
20200714; version 2.730:
* Added code to prevent redundant save and restore operations when
nested capturing round brackets are processed.
* Improved regex_iterator.
20200703; version 2.720:
* Improved case-insensitive (icase) search using the
Boyer-Moore-Horspool algorithm for UTF-8 string that includes
non-ASCII characters or UTF-16 string that includes non-BMP
characters.
* Fixed a bug that caused regex_iterator->prefix().first to point to
the beginning of the subject string instead of the end of the
previous match (regression introduced in version 2.650, when
three-iterators overloads were added to regex_search()).
* In accordance with the fix above, when a three-iterators version of
regex_search() is called, now match_results.position() returns a
distance from the position passed to as the lookbehind limit (3rd
param of regex_search) and match_results.prefix().first points to
the position passed to as the beginning of the subject string (1st
param of regex_search).
* Fixed a bug that could cause a valid UTF-8 sequence being adjacent
to an invalid UTF-8 sequence to be skipped when the BMH algorithm
was used (regression introduced in version 2.630, when UTF-8
handling was modified).
20200701; version 2.710:
* Minor modifications to Boyer-Moore-Horspool search.
20200630; version 2.700:
* Optimisation adjustments.
20200620: version 2.651:
* Move the group name validity check to after parsing the \u escape.
* Updated misc/sample01.cpp to version 1.103. Changed the version
number of the ECMAScript specification referenced by to 2020 (ES11).
20200618: version 2.650:
* To element access functions in match_results, added overload
functions for specifying the group name by a pointer.
* When a three-iterators version of regex_search() is used, SRELL now
sets match_results::prefix::first to the position passed to as the
lookbehind limit (third param) instead of the position passed to as
the beginning of the subject (first param).
* Removed some operations that seem to be redundant.
20200601: version 2.643:
* Added "inline" to operators in syntax_option_type and
match_flag_type types, based on a report that it is needed not to
cause the multiple definition error.
* Minor improvements.
20200530: version 2.642:
* Reduced the size of memory allocated by the basic_regex instance.
20200528: version 2.641:
* The fix in 2.640 was incomplete. Fixed the optimisation bug 1 again.
* Optimisation adjustments.
20200516: version 2.640:
* Fixed an optimisation bug 1: It was possible for regex_match to pass
the end of a subject string under certain conditions.
* Fixed an optimisation bug 2: ^ and $ were not given a chance to
match an appropriate position in some cases when the multiline flag
is set to true.
* Updated srell_ucfdata2.hpp and srell_updata.hpp.
20200509: version 2.630:
* SRELL's pattern compiler no longer permits invalid UTF-8 sequences
in regular expressions. It throws regex_utf8. (Invalid UTF-8
sequences in the subject string are not treated as an error.)
* Fixed BMH search functions not to include extra (invalid) UTF-8
trailing bytes following the real matched substring, in a returned
result.
* Fixed minor issues: 1) basic_regex.flags() did not return the
correct value in some cases, 2) match_results.format() did not
replace $<NAME> with an empty string when any capturing group whose
name is NAME did not exist.
20200502: version 2.620:
* Removed methods used for match_continuous and regex_match in the
class for the Boyer-Moore-Horspool algorithm. Now SRELL always uses
the automaton like earlier versions when they are processed.
* Some clean-ups.
20200428: version 2.611:
* Fixed a bug that caused /\d*/ not to match the head of "abc" but to
match the end of it. (regression introduced in version 2.210.)
20200426: version 2.610:
* Fixed a bug that caused case-insensitive (icase) BMH search to skip
a matched sequence at the beginning of the entire text, when 1)
search is done against UTF-8 or UTF-16 text, and 2) the searched
pattern ends with a character that consists of multiple code units
in that encoding.
* Now SRELL parses a capturing group name according to the ECMA
specification and strictly checks its validity. Group names like
/(?<,>...)/ cause regex_error.
20200418: version 2.600:
* To pass to regex_search() directly the limit of a sequence until
where the automaton can lookbehind, added three-iterators versions
of regex_search().
* [Breaking Change] Removed the match_lblim_avail flag from
match_flag_type and the lookbehind_limit member from match_results
which were added in version 2.300.
* Updated srell_ucfdata2.hpp and srell_updata.hpp to support Unicode
13.0.0.
* Updated unicode/updataout.cpp to support Unicode 13. (Support in
advance new script names that will be available in RegExp of
ECMAScript 2020).
20191118: version 2.500:
* Modified basic_regex to hold precomputed tables for icase matching,
instead of creating them from case folding data when its instance is
first created.
* In accordance with the change above, srell_ucfdata.hpp and
ucfdataout.cpp that outputs the former were replaced with
srell_ucfdata2.hpp that holds precomputed tables and ucfdataout2.cpp
that outputs the former.
* Changed the method of character class matching from linear search to
binary search.
* Changed the timing of optimisation of a character class from "when a
closing bracket ']' is found" to "every time a character or
character range is pushed to its character class array".
* Removed all asserts.
* Modified the pattern compiler to interpret sequential \uHHHH escapes
as a Unicode code point value if they represent a valid surrogate
pair. (By this change, incompatibilities with the ECMAScript
specification disappeared.)
* Fixed the position of an endif directive that caused a compiler
error when -DSRELL_NO_NAMEDCAPTURE is specified.
* Updated updataout.cpp to version 1.101.
* Added a standalone version of SRELL in the single-header directory.
20190914: version 2.401:
* Reduced the size of basic_regex. (It was bloated by my carelessness
when support for Unicode property escapes was added).
* Improved basic_regex::swap().
20190907: version 2.400:
* Improved the performance of character class matching.
* Modified the pattern compiler to interpret the \u escape sequence in
the group name in accordance with the ECMAScript specification.
* Updated ucfdataout.cpp to version 1.200. A new member has been added
to the unicode_casefolding class in srell_ucfdata.hpp that
ucfdataout.cpp generates.
Because SRELL 2.400 and later need this added member, they cannot be
used with srell_ucfdata.hpp output by ucfdataout.cpp version 1.101
or earlier. (No problem in using an older version of SRELL with a
newer version of srell_ucfdata.hpp).
* Some clean-ups and improvements.
20190902: version 2.304:
* Fixed regex_iterator that had been broken by the code clean-up in
version 2.303.
20190810: version 2.303:
* Refixed the problem that was fixed in version 2.302 as the fix was
incomplete.
* Cleaned up code.
20190809: version 2.302:
* Bug fix: When (?...) has a quantifier, strings captured by round
brackets inside it were not cleared in each repetition but carried
over to the next loop. For example,
/(?:(ab)|(cd))+/.exec("abcd") returned ["abcd", "ab", "cd"], instead
of ["abcd", undefined, "cd"]. (The latter is correct).
* Updated misc/sample01.cpp to version 1.102. Rewrote the chapter
numbers in accordance with ECMAScript 2019 (ES10).
20190724: version 2.301:
* In accordance with the ECMAScript spec, restricted the characters
which can be escaped by '\', to the following fifteen characters:
^$\.*+?()[]{}|/
Only in the character class, i.e., inside [], '-' also becomes a
member of the group.
20190717: version 2.300:
* Added a feature for specifying the limit until where the automaton
can lookbehind, separated from the beginning of a target sequence.
(Addition of the match_lblim_avail flag to match_flag_type and the
lookbehind_limit member to match_results).
And, lookbehind_limit of match_results being private and used
internally in regex_iterator is also set in its constructor.
* Removed order restriction of capturing parentheses and
backreferences, in accordance with the ECMAScript spec. Now /\1(.)/,
/(?<=(.)\1)/, and /\k<a>(?<a>.)/ are all okay.
* Updated misc/sample01.cpp to version 1.101. Added one compliance
test from misc.js.
20190714: version 2.230:
* Improved the performance of searching when regular expressions begin
with a character or character class followed by a '*' or '+'. (E.g.,
/[A-Za-z]+ing/).
20190707: version 2.221:
* Changed the feature test macro used for checking availability of
std::u8string, from __cpp_char8_t to __cpp_lib_char8_t.
* When icase specified, if all characters in a character class become
the same character as a result of case-folding, the pattern compiler
has been changed to convert the character class to the character
literal (e.g., /r[Ss\u017F]t/i -> /rst/i).
* Fixed a minor issue.
20190617: version 2.220:
* Changed the internal representation of repetition in the case that
it becomes more compact by not using the counter.
* Fixed an optimisation bug that caused searching for /a{1,2}?b/
against "aab" to return "ab" instead of "aab". (Condition: a
character or character class with a non-greedy quantifier is
followed by its exclusive character or character class).
20190613: version 2.210:
* Improved a method of matching for expressions like /ab|cd|ef/ (where
string literals separaterd by '|' begin with a character exclusive
to each other).
20190603: version 2.202:
* Fixed a bug that caused regex_match to behave like regex_search in
the situation where the BMH algorithm is used.
20190531: version 2.200:
* For searching with a ordinary (non-regex) string, added an
implementation based on the Boyer-Moore-Horspool algorithm.
* Improved UTF-8 iterators.
* Fixed behaviours of \b and \B when icase specified, to match /.\B./i
against "s\u017F".
* Fixed minor issues.
20190508: version 2.100:
* Fixed a bug that caused failure of capturing when 1) a pair of
capturing brackets exists in a lookbehind assertion, and 2) variable
length expressions exist in both the left side of and the inside of
the pair of brackets. E.g., given "1053" =~ /(?<=(\d+)(\d+))$/, no
appropriate string was set for $2.
* Updated srell_ucfdata.hpp and srell_updata.hpp to support Unicode
12.1.0.
* Updated unicode/updataout.cpp to support Unicode 12. (Support in
advance a new binary property and new script names that will be
available in RegExp of ECMAScript 2019 and new script names that are
anticipated to be available in RegExp of ECMAScript 2020).
* Changed the newline character in srell.hpp from CR+LF to LF.
* Modified unicode/*.cpp to output LF as a newline instead of CR+LF.
* Updated misc/sample01.cpp to version 1.100:
1. Rewrote the chapter numbers in subtitles of compliance tests, in
accordance with ECMAScript 2018 Language Specification (ES9).
(The old chapter numbers were based on ECMAScript specifications
up to version 5.1).
2. Added one compliance test from ECMAScript 2018 Language
Specification 21.2.2.3, NOTE.
* Modified the macros for detecting C++11 features.
* Changed the method of the character class.
* For all the constructors and assign functions of basic_regex to have
a default argument for flag_type, reimplemented syntax_option_type
and match_flag_type (missed changes between TR1 -> C++11).
* Experimental support for the char8_t type. If a compiler supports
char8_t (detected by the __cpp_char8_t macro), classes whose names
have the "u8-" prefix accept a sequence of char8_t and handle it as
a UTF-8 string. If char8_t is not supported, the classes handle a
sequence of char as a UTF-8 string, as before.
* As classes that always handle a sequence of char as a UTF-8 string,
new classes whose names have the "u8c-" prefix were added. They
correspond to the classes having the "u8-" prefix in their names up
to version 2.002:
* u8cregex; u8ccmatch, u8csmatch; u8ccsub_match, u8cssub_match;
u8ccregex_iterator, u8csregex_iterator; u8ccregex_token_iterator,
u8csregex_token_iterator.
20180717: version 2.002:
* Changed the maximum number of hexdigits in \u{h...} from six to
'unlimited' in accordance with the ECMAScript specification. ("one
to six hexadecimal digits" of the old implementation was based on
the proposal document).
* Updated updataout.cpp to version 1.001. Encounting unknown
(newly-encoded) script names is no longer treated as an error.
* Updated srell_ucfdata.hpp and srell_updata.hpp to support Unicode
11.0.0.
20180204: version 2.001:
* When icase is specified, [\W] (a character class containing \W) no
longer matches any of [KkSs\u017F\u212A] (ecma262 issue #512).
20180127: version 2.000:
* Added the following features that are to be included into RegExp of
ECMAScript 2018:
* New syntax option flag for '.' to match every code point, dotall,
was added to srell::regex_constants as a value of
syntax_option_type and to srell::basic_regex as a value of
flag_type.
* New expressions to support the Unicode property, \p{...} and
\P{...}.
* Named capture groups (?<NAME>...) and the new expression for
backreference to a named capture group, \k<NAME>.
* The behaviors of lookbehind assertions changed. Now both (?<=...)
and (?<!...) support variable-length lookbehind.
20180125; version 1.401:
* Limited the maximum of numbers that are recognised as backreference
in match_results.format() up to 99, in accordance with the
ECMAScript specification. (I.e., restricted to $1..$9 and $01..$99).
* Removed an unused macro and its related code.
20180101; version 1.400:
* Changed the behaviour of the pattern compiler so that an empty
non-capturing group can have a quantifier, for example, /(?:)*/. It
is a meaningless expression, but changed just for compatibility with
RegExp of ECMAScript.
* Fixed a hang bug: This occured when 1) a non-capturing group has a
quantifier, 2) and the length of the group itself can be zero-width,
3) and a backreference that can be zero-width is included in the
group somewhere other than the last, such as /(.*)(?:\1.*)*/.
20171216; version 1.300:
* Fixed an important bug: /^(;[^;]*)*$/ did not match ";;;;" because
of a bug in optimisation. This problem occured when a sequence of
regular expressions ended like /(A...B*)*$/ where a character or
character set that A represents and the one that B represents are
exclusive to each other.
20170621; version 1.200:
* Updated srell_ucfdata.hpp to support Unicode 10.0.0.
* Improved u8regex_traits to handle corrupt UTF-8 sequences more
safely.
20150618; version 1.141:
Updated srell_ucfdata.hpp to support Unicode 8.0.0.
20150517; version 1.140:
* Modified the method for regex_match() to determine whether a
sequence of regular expressions is matched against a sequence of
characters. (Issue raised at #2273 in C++ Standard Library Issues
List).
* Restricted the accepted range of X in the expression "\cX" to
[A-Za-z] in accordance with the ECMAScript specification.
* Fixed the problem that caused parens in a lookaround assertion not
to capture a sequence correctly in some circumstances because the
bug fix done in version 1.111 was imperfect.
20150503; version 1.130:
* Improved case-folding functions.
* Updated unicode/ucfdataout.cpp to version 1.100.
* Fixed a typo in #if directives for u(16|32)[cs]match.
20150425; version 1.120:
* Fixed the bug that caused characters in U+010000-U+10FFFF in UTF-8
(i.e., four octet length characters) not to have been recognised.
* Updated misc/sample01.cpp to version 1.010.
20150402; version 1.111:
* Fixed the problem that caused $2 of "aaa" =~ /((.*)*)/ to be empty
instead of "aaa" because of a bug in optimisation.
20141101; version 1.110:
* Several fixes based on a bug report:
1. Added "this->" to compile() in basic_regex::assign().
2. Implemented operator=() functions explicitly instead of using
default ones generated automatically.
* unicode/ucfdataout.cpp revised and updated to version 1.001.
20140622; version 1.101:
Updated srell_ucfdata.hpp to support Unicode 7.0.0.
20121118; version 1.100:
The first released version.

View file

@ -0,0 +1,421 @@
20220511; version 3.009:
・最適化バグにより /abcd|ab/ が "abc" にマッチしなかった問題を修正。
20220504; version 3.008:
・icase指定時の[^\P{...}]の振る舞いが、TC39で提案中のv-modeのそれに近
いものになっていた問題を修正。
20220429; version 3.007:
・カウンタの仕組みをさらに変更。
20220428; version 3.006:
・繰り返し処理用のカウンタを調整。
・小さな文字クラス用の線形探索を再削除。
20220424; version 3.005:
・multiline指定時に /(?<=$.*)/ が "a" の終わりにマッチしなかった問題を
修正。
・TC39で提案中の\A, \z, (?m:)の準備。
20220420; version 3.004:
・'*' または '+' 付きの文字クラスが後続する文字または文字クラスと排他
的になっていない表現用の最適化処理を追加。例:/[A-Za-z]+ing/,
/".*"/ など。
20220416; version 3.003:
・2つの最適化函数を1つに統合。
・先読み (lookahead)・戻り読み (lookbehind) 用のコード量を削減。
20220416; version 3.002:
・3.000で導入した簡易エントリーポイント選択の使用時に、regex_matchや
match_continuousフラグが指定されたregex_searchが機能しない場合があっ
た問題を修正。
20211025; version 3.001:
・カウンタ分割を廃止。効果がないかむしろ若干速度が低下しているように見
えるため。
・潜在的なバグを修正。
・その他細かな改良など。
20211023; version 3.000:
・srell_ucfdata2.hppとsrell_updata.hppとをUnicode 14.0.0対応に更新。
・unicode/updataout.cppをUnicode 14対応に更新ECMAScript 2022で対応さ
れる見込みのスクリプト名の先行対応)。
・char32_t未対応のコンパイラでUnicode値を保持するため内部で使用する型
を「21ビット以上あるunsigned整数型」から「32ビット以上あるunsigned整
数型」に変更。
・char32_t未対応のコンパイラで繰り返し回数や文字クラス番号を保持するの
に使う型を「unsigned int」から「32ビット以上あるunsigned整数型」に変
更。
・数値用パーザにoverflowチェックを追加。例unsigned int型が32ビットの
幅の時、前の版まで /a{0,4294967297}/ は /a{0,1}/ 相当になってしまっ
ていましたが、前記のチェックを入れたことによりこのような場合には
error_braceがthrowされるようになっています。
・非multilineモード時に /[^;]*^;?/ が入力文字列の先頭にマッチしなかっ
たバグを修正。
・ごく簡易なエントリーポイント選択を実装。
20211004; version 2.930:
・WCHAR_MAXの値に基づいてUTF-16/UTF-32対応が切り替わるu1632w-型を新規
に追加WCHAR_MAXが0xFFFF以上・0x10FFFF未満ならu1632w-型はu16w-型の
別名となり、WCHAR_MAXが0x10FFFF以上ならu1632w-型はu32w-型の別名とな
ります)。
・Eytzinger layout検索時に使われるメモリ使用量を削減。
・その他細かな改良などいくつかはNIREに対するMarko Njezic氏の改善案に
基づきます)。
20210624; version 2.920:
・?{0,1}相当)用の最適化処理を追加。
・misc/sample01.cpp内で参照しているECMAScript仕様書の版を2021に変更。
20210429; version 2.912:
・2.900で導入した最適化処理のバグにより /aa|a|aa/ が "a" にマッチしな
くなっていた問題を修正報告してくださったJan Schrötter氏に感謝しま
す)。
ちなみにこの最適化処理は、srell.hppをincludeする前に
SRELLDBG_NO_BRANCH_OPT2マクロを定義しておくと無効化できます。
20210424; version 2.911:
・2.900で導入した最適化処理内の不用意な行削除が原因で、/abc|ab|ac/ が
"ac" に対してマッチしなくなっていた問題を修正(バグ報告に感謝します)。
20210407; version 2.910:
・2.900以降、パターンコンパイラ内部でmove代入演算子が使われる時にメモ
リリークしていた問題を修正報告してくださったMichal Švec氏に感謝し
ます)。
20210214; version 2.901:
・不要なテンプレートの特殊化を削除。
20210214; version 2.900:
・文字列のみからなる選択(例:/abc|abd|acde/)用の最適化処理を新規に追
加。
・u(8|16)[cs]regex_(token_)?iteratorがコンパイルエラーとなり使用できな
かった問題を修正。
・その他細かな改良など。
20210131; version 2.810:
・UTF-8用内部iteratorの改良。
20200724; version 2.800:
・文字クラスの二分探索にEytzinger layoutを導入。
・小さな文字クラス用に線形探索を再実装。
・名前付き括弧の名前部分をパーズするためのプロパティーデータの扱いを変
更。basic_regex型インスタンス内に読み込むのを止めて、必要な時のみ読
み込むように。
20200714; version 2.730:
・入れ子になった捕獲括弧で冗長な退避・復元処理をせぬように変更。
・regex_iteratorの改良。
20200703; version 2.720:
・非ASCII文字を含むUTF-8文字列または非BMPの文字を含むUTF-16文字列を、
Boyer-Moore-Horspoolアルゴリズムを用いて、大文字小文字の区別無しで
(icase/case-insensitiveで) 検索する場合の処理の改良。
・Version 2.650での変更により、regex_iterator->prefix().firstが前回マ
ッチした位置の終端ではなく文字列全体の最初を指すようにになってしまっ
ていたのを修正。
・上記修正に合わせて3イテレータ版のregex_search()が呼ばれる場合、
match_results.position()は戻り読みの逆行限界として渡された位置
regex_searchの第3引数を起点とした位置を返し、
match_results.prefix().firstは検索開始位置同第1引数を指すように
変更。
・BMH検索時に、不正なUTF-8シークウェンスの前後にある有効なシークウェン
スが読み飛ばされてしまう問題を修正2.630でUTF-8の処理方法を変えた時
に混入したバグ)。
20200701; version 2.710:
・Boyer-Moore-Horspool検索の調整。
20200630; version 2.700:
・最適化処理の調整。
20200620: version 2.651:
・グループ名のチェックを行う位置を\uエスケープの解釈後に移動。
・misc/sample01.cppをversion 1.103に更新。参照しているECMAScript仕様書
の版を2020(ES11)に変更。
20200618: version 2.650:
・名前付き括弧に捕獲された文字列へのアクセス用函数に、グループ名をポイ
ンタで指定するoverloadをmatch_resultsに追加。
・3イテレータ版のregex_search()使用時には、検索の開始位置ではなく戻り
読み (lookbehind) の逆行限界として渡された位置のほうを
match_results::prefix::firstにセットするよう変更。
・不要と思われる処理をいくつか削除。
20200601: version 2.643:
・syntax_option_typeおよびmatch_flag_typeのoperator函数にinline指定を
追加(これがないとリンク時に多重定義エラーが出ることがあるとのご指摘
がありました)。
・その他細かな改良など。
20200530: version 2.642:
・basic_regex型インスタンスが確保するメモリのサイズを削減。
20200528: version 2.641:
・2.640での修正1が不完全であったため再修正。
・最適化処理の調整。
20200516: version 2.640:
・最適化バグの修正1: regex_matchが入力文字列の終端を通り過ぎてしまうこ
とがあった問題を修正。
・最適化バグの修正2: multilineフラグ指定時に ^ や $ が適切な位置でのマ
ッチングをさせてもらえなくなってしまっていた問題を修正。
・srell_ucfdata2.hppとsrell_updata.hppとを更新。
20200509: version 2.630:
・正規表現中に不正なUTF-8のシークウェンスがあった場合、パターンコンパ
イラがregex_utf8をthrowするように仕様変更検索対象文字列中に不正な
UTF-8の並びがあってもエラー扱いされません
・UTF-8でBMH検索が行われる際、マッチした箇所の直後に余分な後続
(trailing) バイトが続いていた場合にその部分もマッチング結果に含めて
しまう問題を修正。
・basic_regex.flags() が正しい値を返さないことがあったのを修正。
・正規表現中で実際には使われていないグループ名 (NAME) を
match_results.format()に渡す書式文字列の中で$<NAME>のようにして指定
すると、その部分が空文字に置換されずそのまま残ってしまう問題を修正。
20200502: version 2.620:
・Boyer-Moore-Horspoolアルゴリズム用クラスからmatch_continuous指定時用
およびregex_match用の函数を削除。これらの処理時は以前のようにオート
マトンを使うように変更。
・その他クリーンナップ。
20200428: version 2.611:
・/\d*/ が "abc" の冒頭にマッチせず末尾にマッチする問題を修正Version
2.210で混入したバグ)。
20200426: version 2.610:
・Case-insensitive (icase) なBMH検索が行われる際、探している文字列が検
索対象テキスト全体の先頭にあった場合に読み飛ばされてしまうことがある
バグを修正UTF-8またはUTF-16で、検索文字列の末尾が複数のコードユニ
ットからなる文字である場合に発生)。
・キャプチャグループ名のパーズをECMAScriptの仕様書通りきっちり行うよう
に変更。これにより、前の版までは受理されていた /(?<,>...)/ のような
グループ名はregex_errorがthrowされるように。
20200418: version 2.600:
・戻り読み (lookbehind) の逆行限界を直接regex_search()に渡せるように
3イテレータ版のregex_search()を追加。
・[非互換変更] 2.300で導入したmatch_flag_typeのmatch_lblim_availフラグ
と、match_resultsのlookbehind_limitメンバとを廃止。
・srell_ucfdata2.hppとsrell_updata.hppとをUnicode 13.0.0対応に更新。
・unicode/updataout.cppをUnicode 13対応に更新ECMAScript 2020で対応さ
れる見込みのスクリプト名の先行対応)。
20191118: version 2.500:
・初めてbasic_regex型インスタンスが作られた時にcase foldingデータから
icaseマッチング用テーブルを展開するのに代えて、最初から計算済みテー
ブルを保持しているように仕様変更。
・上記変更に併せてsrell_ucfdata.hppおよびそれを出力するucfdataout.cpp
はお役御免とし、代わりに展開済みicase用テーブルを保持する
srell_ucfdata2.hppとそれを出力するucfdataout2.cppとを追加。
・文字クラスの照合方法を線形探索から二分探索に変更。
・文字クラスの最適化処理のタイミングを「']' が見つかった時にまとめて一
括」から「文字または文字コードの範囲をpushするたびごと逐次」に変更。
・assertをすべて削除。
・連続する\uHHHHがサロゲートペアをなしている場合はUnicode値として解釈
するように変更これによりECMAScript仕様との相違はなくなりました
・SRELL_NO_NAMEDCAPTUREマクロ使用時にコンパイルエラーが出ていたのを修
正。
・updataout.cppを1.101にヴァージョンアップ。
・単体版のsrellを追加single-headerディレクトリ内
20190914: version 2.401:
・basic_regex型インスタンスのサイズを削減Unicode property escapes対
応時にうっかり膨張させてしまっていました)。
・basic_regex::swap()の改良。
20190907: version 2.400:
・文字クラスの照合速度を改善。
・パターンコンパイル時にグループ名中の\uエスケープを解釈するように変更
ECMAScriptの仕様に準拠
・ucfdataout.cppを1.200にヴァージョンアップ。このプログラムが出力する
srell_ucfdata.hpp中のunicode_casefoldingクラスに、新たにメンバ変数が
追加されました。
SRELL 2.400以降はこの追加されたメンバ変数をコンパイル時に必要とする
ため、ucfdataout.cpp 1.101以前によって出力されたsrell_ucfdata.hppを
SRELL 2.400以降で使うことはできません古いSRELLで新しい
srell_ucfdata.hppを使うことは可
・その他コードの整理や改良など。
20190902: version 2.304:
・Version 2.303のコード整理で壊れてしまっていたregex_iteratorを修復。
20190810: version 2.303:
・2.302の修正が不完全であったため再修正。
・その他コードの整理。
20190809: version 2.302:
・(?...) に繰り返し指定がついている時、内側の括弧によって捕獲された文
字列がループごとにクリアされず持ち越されていたバグを修正。
例:/(?:(ab)|(cd))+/.exec("abcd") → 1番括弧はundefinedになるはずが
"ab"になってしまっていた。
・misc/sample01.cppをversion 1.102に更新。テスト名中の章番号を
ECMAScript 2019 (ES10) 準拠に変更
20190724: version 2.301:
・ECMAScriptの仕様に準じて、\でエスケープ可能な文字の種類を次の15字に
限定。^$\.*+?()[]{}|/
文字クラス内([]内ではこの15字に加えて '-' も対象に。
20190717: version 2.300:
・検索対象範囲とは別に、戻り読み (lookbehind) の逆行限界を指定できる機
能を追加match_flag_typeへのmatch_lblim_availフラグの追加と
match_resultsへのlookbehind_limitメンバの追加
これに併せてregex_iteratorのコンストラクタ内でも、内部で使うprivate
なmatch_results型インスタンスのlookbehind_limitメンバに値を設定する
ように変更。
・ECMAScriptの仕様に合わせて、後方参照が対応する捕獲括弧より先に出現し
てもエラー扱いせぬように変更。/\1(.)/, /(?<=(.)\1)/, /\k<a>(?<a>.)/
などすべてOKに。
・misc/sample01.cppをversion 1.101に更新。misc.jsより準拠テストを1つ追
加。
20190714: version 2.230:
・正規表現が '*' か '+' かを伴う文字または文字クラスで始まる場合の検索
速度を改善(例:/[A-Za-z]+ing/)。
20190707: version 2.221:
・std::u8stringの利用可否は__cpp_char8_tではなく__cpp_lib_char8_tを用
いて判断するように変更。
・icase指定時にcase-folding処理をした結果、文字クラス内の文字がすべて
同じ文字になった場合には、文字クラスを解消して文字リテラルとして処理
するように変更。例:/r[Ss\u017F]t/i → /rst/i。
・その他問題を修正。
20190617: version 2.220:
・カウンタを使わぬほうが内部表現がコンパクトになる繰り返しはカウンタを
使わぬように変更。
・最適化バグにより、/a{1,2}?b/.exec("aab") が "aab" ではなく "ab" を返
していたのを修正(発生条件:最短一致優先の回数指定が付いている文字ま
たは文字クラスの後ろに、その文字集合と排他的な文字または文字クラスが
続いている場合)。
20190613: version 2.210:
・/ab|cd|ef/ のような表現('|' で区切られている文字列の先頭文字が互い
に排他的な場合)の照合方法を改良。
20190603: version 2.202:
・BMHアルゴリズムが使われる状況で、regex_matchがregex_search相当の処理
をしてしまうバグを修正。
20190531: version 2.200:
・通常の正規表現ではないテキスト検索用に、Boyer-Moore-Horspoolアル
ゴリズムに基づく実装を追加。
・UTF-8用iteratorの改良。
・icase指定時の\b/\Bの挙動を修正。/.\B./i が "s\u017F" にマッチするよ
うに。
・その他問題を修正。
20190508: version 2.100:
・Lookbehind中に文字列のキャプチャがあり、かつその中および左方に可変長
の正規表現があった場合、文字列の捕獲に失敗することがあったのを修正。
例:"1053" =~ /(?<=(\d+)(\d+))$/ で$2に適切な文字列がセットされず。
・srell_ucfdata.hppとsrell_updata.hppとをUnicode 12.1.0対応に更新。
・unicode/updataout.cppをUnicode 12対応に更新ECMAScript 2020で対応さ
れる見込みのスクリプト名の先行対応)。
・srell.hpp中の改行コードをCR+LFからLFに変更。
・unicode/*.cppが出力するファイルの改行コードをCR+LFからLFに変更。
・misc/sample01.cppをversion 1.010に更新。
1. テスト名中の章番号をECMAScript 2018 (ES9) 準拠に変更(前版までは
ECMAScript 5.1までの章番号準拠でした)。
2. ECMAScript 2018規格の2.2.2.3 NOTEから準拠テストを1つ追加。
・C++11の機能の使用可否を判定するマクロを変更。
・文字クラスの処理方法を変更。
・basic_regexの全コンストラクタと全assign函数とでflag_typeのdefault引
数を指定できるように、syntax_option_typeとmatch_flag_typeとを再実装
TR1→C++11間の変更の見落とし
・char8_t型に試験対応。コンパイラがchar8_tに対応している場合
__cpp_char8_tマクロ定義の有無で判断、"u8-"というprefixの付いた
クラスは「char8_t型文字列を受け取り、それをUTF-8として扱う」ように。
char8_tに未対応の場合は従来通り、char型文字列をUTF-8として処理。
・常に「char型文字列をUTF-8として扱う」クラスとして新規に"u8c-"という
prefixに付いたクラスを追加。2.002までの"u8-"付きクラス相当。
・u8cregex; u8ccmatch, u8csmatch; u8ccsub_match, u8cssub_match;
u8ccregex_iterator, u8csregex_iterator; u8ccregex_token_iterator,
u8csregex_token_iterator.
20180717: version 2.002:
・ECMAScriptの仕様に合わせて \u{h...} の h... 部分の最大桁数を6から無
制限に変更変更前の16桁というのは提案書に基づく実装でした
・updataout.cppを1.001に更新。新規に追加されたスクリプト名をエラー扱い
せぬように修整。
・srell_ucfdata.hppとsrell_updata.hppとをUnicode 11.0.0対応に更新。
20180204: version 2.001:
・icase指定時に、[\W]\Wを含む文字classが [KkSs\u017F\u212A] のいず
れにもマッチせぬよう変更関連ecma262 issue #512
20180127; version 2.000:
・ECMAScript 2018のRegExpに追加されることになった次の機能を実装:
・'.' があらゆるコードポイントにマッチするようにするための指定
"dotall" フラグを、srell::regex_constants内の syntax_option_type
および srell::basic_regex内の flag_type に追加。
・Unicode property用の表現、\p{...} と \P{...} とを追加。
・名前付きキャプチャ (?<NAME>...) と、名前付きキャプチャによって捕獲
された文字列を後方参照するための正規表現、\k<NAME> とを追加。
・戻り読み (lookbehind) の振る舞いを変更。(?<=...), (?<!...) とも可変
幅の戻り読みに対応。
20180125; version 1.401:
・ECMAScriptの仕様に合わせて、match_results.format()内で後方参照として
認識される数値を99までに制限即ち$1$9および$01$99のみ有効
・長い間メンテナンスしていないマクロを削除。
20180101; version 1.400:
・/(?:)*/ のように、空のnon-capturingグループにも量指定子を付けられる
ように変更ECMAScriptのRegExpとの互換性確保のための変更で、使い道は
おそらくありません)。
・次の3条件が揃った時に固まってしまったのを修正: 1) non-capturingグル
ープに量指定子が付いていて、2) そのグループ自身が0幅になり得て、3)
そのグループ内の最後以外の場所に、0幅になり得る後方参照が現れる時。
たとえば /(.*)(?:\1.*)*/ のような表現。
20171216; version 1.300:
・最適化処理のバグにより、/^(;[^;]*)*$/ が ";;;;" にマッチしなかった問
題を修正。この問題の発生条件は次の通り:
・/(A...B*)*$/ のような終わり方をしていて、かつAとBとが互いに排他的
な文字または文字集合である場合。
20170621; version 1.200:
・srell_ucfdata.hppをUnicode 10.0.0対応に。
・不正なUTF-8 sequenceに対するu8regex_traitsの振る舞いを改善。
20150618; version 1.141:
srell_ucfdata.hppをUnicode 8.0.0対応に。
20150517; version 1.140:
・regex_match()がマッチの成否を判定する方法の変更。
C++ Standard Library Issues List #2273 への対応)
・ECMAScriptの仕様に合わせて \cX の X の範囲を [A-Za-z] に制限。
・look-around assertions中の丸括弧が、ある条件下で正しく文字列をキャプ
チャせぬ場合があった問題を修正。Version 1.111での修正が不完全であっ
たことによるもの。
20150503; version 1.130:
・case-folding用函数の改善。
・unicode/ucfdataout.cppをversion 1.100に。
・u(16|32)[cs]match用の#if directives中にあったtypoを修正。
20150425; version 1.120:
・UTF-8文字列においてU+010000-U+10FFFFの範囲の文字4オクテット長の文
字)が認識されぬバグを修正。
・misc/sample01.cppをversion 1.010に。
20150402; version 1.111:
・最適化処理のバグにより、"aaa" =~ /((.*)*)/ の $2 が "aaa" ではなく空
になってしまう問題を修正。
20141101; version 1.110:
・バグ報告による修正:
1. basic_regex::assign() 内の compile() に "this->" を追加。
2. operator=() 函数を明示的に実装。
・unicode/ucfdataout.cppをversion 1.001 に。
20140622; version 1.101:
srell_ucfdata.hppをUnicode 7.0.0対応に。
20121118; version 1.100:
最初のリリース版。

View file

@ -0,0 +1,32 @@
/*****************************************************************************
**
** SRELL (std::regex-like library) version 3.009
**
** Copyright (c) 2012-2022, Nozomu Katoo. All rights reserved.
**
** Redistribution and use in source and binary forms, with or without
** modification, are permitted provided that the following conditions are
** met:
**
** 1. Redistributions of source code must retain the above copyright notice,
** this list of conditions and the following disclaimer.
**
** 2. Redistributions in binary form must reproduce the above copyright
** notice, this list of conditions and the following disclaimer in the
** documentation and/or other materials provided with the distribution.
**
** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS
** IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
** THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
** PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
** CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
** EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
** PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
** PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
** LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
** NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
** SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
**
******************************************************************************
**/

View file

@ -0,0 +1,379 @@
//
// A sample program for SRELL (tests and benchmarks).
// 2021/06/24; version 1.104
//
// Macro Options:
// -DSTD_REGEX: std::regex used.
// -DBOOST_REGEX: boost::regex used.
// -DBOOST_XPRESSIVE: boost::xpressive used.
// unspecified or others: SRELL used.
//
#include <cstdio>
#include <cstring>
#include <ctime>
#include <string>
#include <vector>
#if defined(STD_REGEX)
#include <regex>
#define RE_PREFIX std
#pragma message("std::regex selected.")
#elif defined(BOOST_REGEX)
#include <boost/regex.hpp>
#define RE_PREFIX boost
#pragma message("boost::regex selected.")
#elif defined(BOOST_XPRESSIVE)
#include <boost/xpressive/xpressive.hpp>
#define RE_PREFIX boost::xpressive
#pragma message("boost::xpressive selected.")
#else
#include "../srell.hpp"
#define RE_PREFIX srell
#pragma message("srell selected.")
#endif
bool test(const std::string &str, const std::string &exp, const unsigned int max, const std::vector<std::string> *const expected = NULL)
{
#if !defined(BOOST_XPRESSIVE)
RE_PREFIX::regex re;
#else
boost::xpressive::cregex re;
#endif
RE_PREFIX::cmatch mr;
bool b = false;
unsigned int num_of_failures = 0;
try
{
std::string matched;
std::string msg;
#if !defined(BOOST_XPRESSIVE)
re.assign(exp, RE_PREFIX::regex::ECMAScript);
#else
re = boost::xpressive::cregex::compile(exp, boost::xpressive::cregex::ECMAScript | boost::xpressive::cregex::not_dot_newline);
#endif
const clock_t st = std::clock();
for (unsigned int i = 0; i < max; i++)
#if !defined(BOOST_REGEX)
b = RE_PREFIX::regex_search(str.c_str(), str.c_str() + str.size(), mr, re);
#else
b = RE_PREFIX::regex_search(str.c_str(), str.c_str() + str.size(), mr, re, boost::regex_constants::match_not_dot_newline);
#endif
const clock_t ed = std::clock();
// std::fprintf(stdout, "\t\"%s\" =~ /%s/\n", str.c_str(), exp.c_str()); // Perl 5 style.
std::fprintf(stdout, "\t/%s/.exec(\"%s\");\n", exp.c_str(), str.c_str()); // ECMAScript style.
if (max > 1)
std::fprintf(stdout, "\t%u times\n", max);
std::fprintf(stdout, "\t%s (%ld msec)\n", b ? "Found" : "Not Found", static_cast<long>(static_cast<double>(ed - st) * 1000 / CLOCKS_PER_SEC));
for (RE_PREFIX::cmatch::size_type i = 0; i < mr.size(); ++i)
{
if (i)
std::fprintf(stdout, "\t$%u = ", i);
else
std::fputs("\t$& = ", stdout);
if (mr[i].matched)
{
matched = mr[i].str();
msg = '"' + matched + '"' + " (%u+%u)";
}
else
msg = matched = "(undefined)";
if (expected)
{
if (i < expected->size())
{
if (matched == expected->operator[](i))
msg += "; passed!";
else
{
msg += "; failed... (expected: \"" + expected->operator[](i) + "\")";
++num_of_failures;
}
}
else
{
msg += "; failed..."; // should not exist.
++num_of_failures;
}
}
msg += '\n';
std::fprintf(stdout, msg.c_str(), mr.position(i), mr.length(i));
}
if (!num_of_failures && expected->size() != mr.size())
++num_of_failures;
std::fprintf(stdout, "Result: %s.\n\n", num_of_failures ? "failed" : "passed");
return num_of_failures == 0;
}
catch (const RE_PREFIX::regex_error &e)
{
std::fprintf(stdout, "Error (regex_error): %d \"%s\"\n\n", e.code(), e.what());
}
catch (const std::exception &e)
{
std::fprintf(stdout, "Error (std::exception): \"%s\"\n\n", e.what());
}
return false;
}
int main()
{
const unsigned int count = 100000;
std::string exp;
std::string str;
std::vector<std::string> expected;
unsigned int num_of_tests = 0;
unsigned int num_of_tests_passed = 0;
unsigned int num_of_benches = 0;
unsigned int num_of_benches_passed = 0;
std::fputs("Test 1 (ECMAScript 2021 Language Specification 22.2.2.3, NOTE)\n", stdout);
str = "abc";
exp = "((a)|(ab))((c)|(bc))";
expected.resize(7);
expected[0] = "abc";
expected[1] = "a";
expected[2] = "a";
expected[3] = "(undefined)";
expected[4] = "bc";
expected[5] = "(undefined)";
expected[6] = "bc";
if (test(str, exp, 1, &expected))
++num_of_tests_passed;
++num_of_tests;
std::fputs("Test 2a (ECMAScript 2021 Language Specification 22.2.2.5.1, NOTE 2)\n", stdout);
str = "abcdefghi";
exp = "a[a-z]{2,4}";
expected.resize(1);
expected[0] = "abcde";
if (test(str, exp, 1, &expected))
++num_of_tests_passed;
++num_of_tests;
std::fputs("Test 2b (ECMAScript 2021 Language Specification 22.2.2.5.1, NOTE 2)\n", stdout);
str = "abcdefghi";
exp = "a[a-z]{2,4}?";
expected[0] = "abc";
if (test(str, exp, 1, &expected))
++num_of_tests_passed;
++num_of_tests;
std::fputs("Test 3 (ECMAScript 2021 Language Specification 22.2.2.5.1, NOTE 2)\n", stdout);
str = "aabaac";
exp = "(aa|aabaac|ba|b|c)*";
expected.resize(2);
expected[0] = "aaba";
expected[1] = "ba";
if (test(str, exp, 1, &expected))
++num_of_tests_passed;
++num_of_tests;
std::fputs("Test 4 (ECMAScript 2021 Language Specification 22.2.2.5.1, NOTE 3)\n", stdout);
str = "zaacbbbcac";
exp = "(z)((a+)?(b+)?(c))*";
expected.resize(6);
expected[0] = "zaacbbbcac";
expected[1] = "z";
expected[2] = "ac";
expected[3] = "a";
expected[4] = "(undefined)";
expected[5] = "c";
if (test(str, exp, 1, &expected))
++num_of_tests_passed;
++num_of_tests;
std::fputs("Test 5a (ECMAScript 2021 Language Specification 22.2.2.5.1, NOTE 4)\n", stdout);
str = "b";
exp = "(a*)*";
expected.resize(2);
expected[0] = "";
expected[1] = "";
if (test(str, exp, 1, &expected))
++num_of_tests_passed;
++num_of_tests;
std::fputs("Test 5b (ECMAScript 2021 Language Specification 22.2.2.5.1, NOTE 4)\n", stdout);
str = "baaaac";
exp = "(a*)b\\1+";
expected[0] = "b";
expected[1] = "";
if (test(str, exp, 1, &expected))
++num_of_tests_passed;
++num_of_tests;
std::fputs("Test 6a (ECMAScript 2021 Language Specification 22.2.2.8.2, NOTE 2)\n", stdout);
str = "baaabac";
exp = "(?=(a+))";
expected[0] = "";
expected[1] = "aaa";
if (test(str, exp, 1, &expected))
++num_of_tests_passed;
++num_of_tests;
std::fputs("Test 6b (ECMAScript 2021 Language Specification 22.2.2.8.2, NOTE 2)\n", stdout);
str = "baaabac";
exp = "(?=(a+))a*b\\1";
expected[0] = "aba";
expected[1] = "a";
if (test(str, exp, 1, &expected))
++num_of_tests_passed;
++num_of_tests;
std::fputs("Test 7 (ECMAScript 2021 Language Specification 22.2.2.8.2, NOTE 3)\n", stdout);
str = "baaabaac";
exp = "(.*?)a(?!(a+)b\\2c)\\2(.*)";
expected.resize(4);
expected[0] = "baaabaac";
expected[1] = "ba";
expected[2] = "(undefined)";
expected[3] = "abaac";
if (test(str, exp, 1, &expected))
++num_of_tests_passed;
++num_of_tests;
std::fputs("Test 8 (from https://github.com/tc39/test262/tree/master/test/built-ins/RegExp/lookBehind/misc.js)\n", stdout);
str = "abc";
exp = "(abc\\1)";
expected.resize(2);
expected[0] = "abc";
expected[1] = "abc";
if (test(str, exp, 1, &expected))
++num_of_tests_passed;
++num_of_tests;
#ifndef SKIP_BENCHMARK
std::fputs("Benchmark 01\n", stdout);
//0123456
str = "aaaabaa";
exp = "^(.*)*b\\1$";
expected.resize(2);
expected[0] = "aaaabaa";
expected[1] = "aa";
if (test(str, exp, count, &expected))
++num_of_benches_passed;
++num_of_benches;
std::fputs("Benchmark 02\n", stdout);
//012345678
str = "aaaabaaaa";
exp = "^(.*)*b\\1\\1$";
expected[0] = "aaaabaaaa";
expected[1] = "aa";
if (test(str, exp, count, &expected))
++num_of_benches_passed;
++num_of_benches;
std::fputs("Benchmark 03\n", stdout);
//01
str = "ab";
exp = "(.*?)*b\\1";
expected[0] = "b";
expected[1] = "";
if (test(str, exp, count * 10, &expected))
++num_of_benches_passed;
++num_of_benches;
std::fputs("Benchmark 04\n", stdout);
//01234567
str = "acaaabbb";
exp = "(a(.)a|\\2(.)b){2}";
expected.resize(4);
expected[0] = "aaabb";
expected[1] = "bb";
expected[2] = "(undefined)";
expected[3] = "b";
if (test(str, exp, count * 10, &expected))
++num_of_benches_passed;
++num_of_benches;
std::fputs("Benchmark 05\n", stdout);
str = "aabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaa";
exp = "(a*)(b)*\\1\\1\\1";
expected.resize(3);
expected[0] = "aabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaa";
expected[1] = "aa";
expected[2] = "b";
if (test(str, exp, count, &expected))
++num_of_benches_passed;
++num_of_benches;
std::fputs("Benchmark 06a\n", stdout);
str = "aaaaaaaaaab";
exp = "(.*)*b";
expected.resize(2);
expected[0] = "aaaaaaaaaab";
expected[1] = "aaaaaaaaaa";
if (test(str, exp, count * 10, &expected))
++num_of_benches_passed;
++num_of_benches;
std::fputs("Benchmark 06b\n", stdout);
str = "aaaaaaaaaab";
exp = "(.*)+b";
if (test(str, exp, count * 10, &expected)) // the same results expected.
++num_of_benches_passed;
++num_of_benches;
std::fputs("Benchmark 06c\n", stdout);
str = "aaaaaaaaaab";
exp = "(.*){2,}b";
expected[1] = "";
if (test(str, exp, count * 10, &expected))
++num_of_benches_passed;
++num_of_benches;
std::fputs("Benchmark 07\n", stdout);
str = "aaaaaaaaaabc";
exp = "(?=(a+))(abc)";
expected.resize(3);
expected[0] = "abc";
expected[1] = "a";
expected[2] = "abc";
if (test(str, exp, count, &expected))
++num_of_benches_passed;
++num_of_benches;
std::fputs("Benchmark 08\n", stdout);
str = "1234-5678-1234-456";
exp = "(\\d{4}[-]){3}\\d{3,4}";
expected.resize(2);
expected[0] = "1234-5678-1234-456";
expected[1] = "1234-";
if (test(str, exp, count * 5, &expected))
++num_of_benches_passed;
++num_of_benches;
std::fputs("Benchmark 09\n", stdout);
str = "aaaaaaaaaaaaaaaaaaaaa";
exp = "(.*)*b";
expected.resize(0);
if (test(str, exp, 1, &expected))
++num_of_benches_passed;
++num_of_benches;
#endif // !defined(SKIP_BENCHMARK)
std::fprintf(stdout, "Results of tests: %u/%u passed.\n", num_of_tests_passed, num_of_tests);
std::fprintf(stdout, "Results of benchmarks: %u/%u passed.\n", num_of_benches_passed, num_of_benches);
return 0;
std::fputs("Benchmark 10\n", stdout);
str = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxz";
exp = "(x+y*)+a";
test(str, exp, 1);
return 0;
}

View file

@ -0,0 +1,21 @@
How to Use
Put the following three files in one directory, and include srell.hpp.
1. srell.hpp
2. srell_ucfdata2.hpp (data for case folding)
3. srell_updata.hpp (data for Unicode properties)
The files in the following directories are supplements. As SRELL does not use
them, it is safe to remove them.
* misc
Contains a source code file for a simple test and benchmark program.
* single-header
Contains a standalone version of srell.hpp into which srell_ucfdata2.hpp
and srell_updata.hpp have been merged.
* unicode
Contains source code files for programs that generate srell_ucfdata.hpp and
srell_update.hpp from latest Unicode data text files.

View file

@ -0,0 +1,23 @@
■使用法
次のファイルを同じディレクトリに置き、srell.hppをincludeするだけです。
・srell.hpp
・srell_ucfdata2.hppcase folding用データ
・srell_updata.hppUnicode property用データ
■付属物
以下のディレクトリ内にあるものはおまけのようなものです。
SRELL側からは参照していませんので、削除してしまってもライブラリの動作に
影響はありません。
・misc
簡単なテスト及びベンチマークプログラムのソースが入っています。
・single-header
srell.hppの中にsrell_ucfdata2.hppとsrell_updata.hppとを統合してしまい、
これ単体で使用できるようにしたstandalone版が入っています。
・unicode
最新のUnicodeデータからsrell_ucfdata.hpp及びsrell_updata.hppを作るため
のプログラムのソースが入っています。

File diff suppressed because it is too large Load diff

9868
lib/srell3_009/srell.hpp Normal file

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,76 @@
Contents of this directory:
1. ucfdataout2.cpp
2. updataout.cpp
----
1. ucfdataout2.cpp
This is a C++ source file for a program that generates a newer version
of srell_ucfdata2.hpp, which is a C++ source file that SRELL 2.500- includes
for case-folding. It is generated by ucfdataout with CaseFolding.txt provided
by the Unicode Consortium.
+---------------------------------------------------------------------------
| What is CaseFolding.txt?
|
| It is a data file needed for case-insensitive matching based on the
| Unicode Standard. Whenever a new version of the Unicode Standard is
| released, CaseFolding.txt may also be updated accordingly.
|
+---------------------------------------------------------------------------
1-1. Usage
1) compile ucfdataout2.cpp,
2) get the latest version of CaseFolding.txt, which is available at
http://www.unicode.org/Public/UNIDATA/CaseFolding.txt ,
3) put CaseFolding.txt and a binary file generated at 1) in the same
directory and run the binary file,
4) move the newly generated "srell_ucfdata2.hpp" to the directory in where
SRELL is put.
1-2. Compatibility
srell_ucfdata2.hpp is not compatible with srell_ucfdata.hpp that SRELL up
to 2.401 was using.
----
2. updataout.cpp
This is a C++ source file for a program that generates a newer version
of srell_updata.hpp, which is a C++ source file that SRELL includes for
the Unicode property escapes (\p{...} and \P{...}). It is generated by
updataout with the following text files provided by the Unicode Consortium:
* DerivedCoreProperties.txt
* DerivedNormalizationProps.txt
* emoji-data.txt
* PropList.txt
* ScriptExtensions.txt
* Scripts.txt
* UnicodeData.txt
As well as CaseFolding.txt mentioned above, these files may be updated
accordingly whenever a new version of the Unicode Standard is released.
2-1. Usage
1) compile updataout.cpp,
2) get the latest versions of the text files mentioned above, which are
available at:
a. emoji-data.txt: http://www.unicode.org/Public/UNIDATA/emoji/
b. others: http://www.unicode.org/Public/UNIDATA/ ,
3) put the text files and a binary file generated at 1) in the same
directory and run the binary file,
4) move the newly generated "srell_updata.hpp" to the directory in where
SRELL is put.
Note: emoji-data.txt has been moved from /Public/UNIDATA/ to
/Public/emoji/(version number)/ since Unicode 11.0.0.
Since Unicode 13.0.0, moved to /Public/UNIDATA/emoji/ .
2-2. Compatibility
srell_updata.hpp does not have compatibility issues as of this release.

View file

@ -0,0 +1,84 @@
■同梱物について
1. ucfdataout2.cpp
2. updataout.cpp
----
1. ucfdataout2.cpp
srell_ucfdata2.hppの最新版を作成するプログラムのソースファイルです。SRELLの
2.5以降はcase-insensitiveな大文字小文字の違いを無視した照合を行うために、
このsrell_ucfdata2.hppを必要とします。
ucfdataout2は、Unicode Consortiumより提供されているCaseFolding.txtというテキ
ストデータからsrell_ucfdata2.hppを自動生成します。
+---------------------------------------------------------------------------
| CaseFolding.txtとは
|
| Case-insensitiveな照合を行う際には、大文字小文字の違いを吸収するために
| "case-folding" と呼ばれる処理が行われます。Unicode規格に基づいた
| case-foldingを行うために、Unicode Consortiumから提供されているのが
| CaseFolding.txtです。
|
| このデータファイルはUnicode規格がアップデートされるとそれに合わせて
| アップデートされる可能性があります。
|
+---------------------------------------------------------------------------
1-1. 使用方法
1) ucfdataout2.cppをコンパイルします。
2) 最新版のCaseFolding.txtを次のURLより取得します。
http://www.unicode.org/Public/UNIDATA/CaseFolding.txt ,
3) CaseFolding.txtと、1)で作成したバイナリとを同じフォルダに置いて
バイナリを実行します。
4) srell_ucfdata2.hppが生成されますので、それをSRELLの置かれているディレク
トリへと移動させます。
1-2. 互換性
srell_ucfdata2.hppは、SRELL 2.401までが利用していたsrell_updata.hppと互換
性がありません。
----
2. updataout.cpp
srell_updata.hppの最新版を作成するプログラムのソースファイルです。SRELLは
Unicode property escapes\p{...} と \P{...})を含む正規表現と文字列との照合
を行うために、このsrell_updata.hppを必要とします。
updataoutは、Unicode Consortiumより提供されている次のテキストデータから
srell_updata.hppを自動生成します。
・DerivedCoreProperties.txt
・DerivedNormalizationProps.txt
・emoji-data.txt
・PropList.txt
・ScriptExtensions.txt
・Scripts.txt
・UnicodeData.txt
先述のCaseFolding.txt同様、これらのテキストデータファイルもUnicode規格が
アップデートされるとそれに合わせてアップデートされる可能性があります。
2-1. 使用方法
1) updataout.cppをコンパイルします。
2) 前記テキストファイルの最新版を次のURLより取得します。
a. emoji-data.txt: http://www.unicode.org/Public/UNIDATA/emoji/
b. それ以外: http://www.unicode.org/Public/UNIDATA/
3) これらのテキストファイルと、1)で作成したバイナリとを同じフォルダに
置いてバイナリを実行します。
4) srell_updata.hppが生成されますので、それをSRELLの置かれているディレク
トリへと移動させます。
補註: Unicode 11.0.0以降、emoji-data.txt は /Public/UNIDATA/ から
/Public/emoji/(ヴァージョン番号)/ へ移されました。
さらに Unicode 13.0.0以降、/Public/UNIDATA/emoji/ へ移されました。
2-2. 互換性
srell_updata.hpp には非互換となるような変更はこれまでのところ加えられてい
ません。

View file

@ -0,0 +1,590 @@
//
// ucfdataout.cpp: version 2.100 (2020/05/13).
//
// This is a program that generates srell_ucfdata.hpp from CaseFolding.txt
// provided by the Unicode Consortium. The latese version is available at:
// http://www.unicode.org/Public/UNIDATA/CaseFolding.txt
//
#include <cstdio>
#include <cstdlib>
#include <string>
#include <map>
#include "../srell.hpp"
#if defined(_MSC_VER) && _MSC_VER >= 1400
#pragma warning(disable:4996)
#endif
namespace unishared
{
template <const std::size_t BufSize, typename Type>
std::string stringify(const Type value, const char *const fmt)
{
char buffer[BufSize];
std::sprintf(buffer, fmt, value);
return std::string(buffer);
}
bool read_file(std::string &str, const char *const filename, const char *const dir)
{
const std::string path(std::string(dir ? dir : "") + filename);
FILE *const fp = std::fopen(path.c_str(), "r");
std::fprintf(stdout, "Reading '%s'... ", path.c_str());
if (fp)
{
static const std::size_t bufsize = 4096;
char *const buffer = static_cast<char *>(std::malloc(bufsize));
if (buffer)
{
for (;;)
{
const std::size_t size = std::fread(buffer, 1, bufsize, fp);
if (!size)
break;
str.append(buffer, size);
}
std::fclose(fp);
std::fputs("done.\n", stdout);
std::free(buffer);
return true;
}
}
std::fputs("failed...\n", stdout);
return false;
}
bool write_file(const char *const filename, const std::string &str)
{
FILE *const fp = std::fopen(filename, "wb");
std::fprintf(stdout, "Writing '%s'... ", filename);
if (fp)
{
const bool success = std::fwrite(str.c_str(), 1, str.size(), fp) == str.size();
std::fclose(fp);
if (success)
{
std::fputs("done.\n", stdout);
return true;
}
}
std::fputs("failed...\n", stdout);
return false;
}
}
// namespace unishared
struct ucf_options
{
const char *infilename;
const char *outfilename;
const char *indir;
int version;
int errorno;
ucf_options(const int argc, const char *const *const argv)
: infilename("CaseFolding.txt")
, outfilename("srell_ucfdata2.hpp")
, indir("")
, version(2)
, errorno(0)
{
bool outfile_specified = false;
for (int index = 1; index < argc; ++index)
{
const char firstchar = argv[index][0];
if (firstchar == '-' || firstchar == '/')
{
const char *const option = argv[index] + 1;
++index;
if (std::strcmp(option, "i") == 0)
{
if (index >= argc)
goto NO_ARGUMENT;
infilename = argv[index];
}
else if (std::strcmp(option, "o") == 0)
{
if (index >= argc)
goto NO_ARGUMENT;
outfilename = argv[index];
outfile_specified = true;
}
else if (std::strcmp(option, "v") == 0)
{
if (index >= argc)
goto NO_ARGUMENT;
version = static_cast<int>(std::strtol(argv[index], NULL, 10));
if (!outfile_specified && version < 2)
{
static const char *const v1name = "srell_ucfdata.hpp";
outfilename = v1name;
}
}
else if (std::strcmp(option, "id") == 0)
{
if (index >= argc)
goto NO_ARGUMENT;
indir = argv[index];
}
else
{
--index;
goto UNKNOWN_OPTION;
}
continue;
NO_ARGUMENT:
std::fprintf(stdout, "[Error] no argument for \"%s\" specified.\n", argv[--index]);
errorno = -2;
}
else
{
UNKNOWN_OPTION:
std::fprintf(stdout, "[Error] unknown option \"%s\" found.\n", argv[index]);
errorno = -1;
}
}
}
};
// struct ucf_options
class unicode_casefolding
{
public:
unicode_casefolding()
: maxdelta_(0L), maxdelta_cp_(0L), ucf_maxcodepoint_(0L), rev_maxcodepoint_(0L)
, ucf_numofsegs_(1U), rev_numofsegs_(1U), numofcps_from_(0U), numofcps_to_(0U)
, max_appearance_(0U), nextoffset_(0x100L), rev_charsets_(1, -1L)
{
}
int create_ucfdata(std::string &outdata, const ucf_options &opts)
{
const std::string indent("\t\t\t");
int errorno = opts.errorno;
std::string buf;
if (errorno)
return errorno;
if (unishared::read_file(buf, opts.infilename, opts.indir))
{
static const srell::regex re_line("^.*$", srell::regex::multiline);
const srell::cregex_iterator eos;
srell::cregex_iterator iter(buf.c_str(), buf.c_str() + buf.size(), re_line);
srell::cmatch match;
int colcount = 0;
for (; iter != eos; ++iter)
{
if (iter->length(0))
{
static const srell::regex re_datainfo("^# (.*)$");
if (!srell::regex_match((*iter)[0].first, (*iter)[0].second, match, re_datainfo))
{
outdata.append(1, '\n');
break;
}
outdata += "// " + match.str(1) + "\n";
}
}
if (opts.version <= 1)
outdata += "template <typename T1, typename T2, typename T3>\nstruct unicode_casefolding\n{\n\tstatic const T1 *table()\n\t{\n\t\tstatic const T1 ucftable[] =\n\t\t{\n";
else
outdata += "template <typename T2, typename T3>\nstruct unicode_casefolding\n{\n";
for (; iter != eos; ++iter)
{
static const srell::regex re_cfdata("^\\s*([0-9A-Fa-f]+); ([CS]); ([0-9A-Fa-f]+);\\s*#\\s*(.*)$");
const srell::cmatch &line = *iter;
if (srell::regex_match(line[0].first, line[0].second, match, re_cfdata))
{
const std::string from(match[1]);
const std::string to(match[3]);
const std::string type(match[2]);
const std::string name(match[4]);
update(from, to);
if (opts.version == 1)
outdata += indent + "{ 0x" + from + ", 0x" + to + " },\t// " + type + "; " + name + "\n";
else if (opts.version <= 0)
{
if (colcount == 0)
outdata += indent;
outdata += "{ 0x" + from + ", 0x" + to + " },";
if (++colcount == 4)
{
outdata.append(1, '\n');
colcount = 0;
}
}
}
else if (opts.version == 1)
{
static const srell::regex re_comment_or_emptyline("^#.*|^$");
if (!srell::regex_match(line[0].first, line[0].second, re_comment_or_emptyline))
outdata += indent + "// " + line.str(0) + "\n";
}
}
if (colcount > 0)
outdata.append(1, '\n');
if (opts.version <= 1)
outdata += indent + "{ 0, 0 }\n\t\t};\n\t\treturn ucftable;\n\t}\n";
outdata += "\tstatic const T2 ucf_maxcodepoint = 0x" + unishared::stringify<16>(ucf_maxcodepoint_, "%.4lX") + ";\n";
outdata += "\tstatic const T3 ucf_deltatablesize = 0x" + unishared::stringify<16>(ucf_numofsegs_ << 8, "%X") + ";\n";
outdata += "\tstatic const T2 rev_maxcodepoint = 0x" + unishared::stringify<16>(rev_maxcodepoint_, "%.4lX") + ";\n";
outdata += "\tstatic const T3 rev_indextablesize = 0x" + unishared::stringify<16>(rev_numofsegs_ << 8, "%X") + ";\n";
outdata += "\tstatic const T3 rev_charsettablesize = " + unishared::stringify<16>(numofcps_to_ * 2 + numofcps_from_ + 1, "%u") + ";\t// 1 + " + unishared::stringify<16>(numofcps_to_, "%u") + " * 2 + " + unishared::stringify<16>(numofcps_from_, "%u") + "\n";
outdata += "\tstatic const T3 rev_maxset = " + unishared::stringify<16>(maxset(), "%u") + ";\n";
outdata += "\tstatic const T2 eos = 0;\n";
if (opts.version >= 2)
{
outdata += "\n\tstatic const T2 ucf_deltatable[];\n\tstatic const T3 ucf_segmenttable[];\n\tstatic const T3 rev_indextable[];\n\tstatic const T3 rev_segmenttable[];\n\tstatic const T2 rev_charsettable[];\n\n\tstatic const T2 *ucf_deltatable_ptr()\n\t{\n\t\treturn ucf_deltatable;\n\t}\n\tstatic const T3 *ucf_segmenttable_ptr()\n\t{\n\t\treturn ucf_segmenttable;\n\t}\n\tstatic const T3 *rev_indextable_ptr()\n\t{\n\t\treturn rev_indextable;\n\t}\n\tstatic const T3 *rev_segmenttable_ptr()\n\t{\n\t\treturn rev_segmenttable;\n\t}\n\tstatic const T2 *rev_charsettable_ptr()\n\t{\n\t\treturn rev_charsettable;\n\t}\n};\n\n";
out_v2tables(outdata);
outdata += "#define SRELL_UCFDATA_VERSION 200\n";
}
else
outdata += "};\n#define SRELL_UCFDATA_VER 201909L\n";
std::fprintf(stdout, "MaxDelta: %+ld (U+%.4lX->U+%.4lX)\n", maxdelta_, maxdelta_cp_, maxdelta_cp_ + maxdelta_);
}
else
errorno = 1;
return errorno;
}
private:
void update(const std::string &from, const std::string &to)
{
const long cp_from = std::strtol(from.c_str(), NULL, 16);
const long cp_to = std::strtol(to.c_str(), NULL, 16);
const long delta = cp_to - cp_from;
const long segno_from = cp_from >> 8;
const long segno_to = cp_to >> 8;
update_tables(cp_from, cp_to, segno_from);
++numofcps_from_;
if (std::abs(maxdelta_) < std::abs(delta))
{
maxdelta_cp_ = cp_from;
maxdelta_ = delta;
}
if (ucf_maxcodepoint_ < cp_from)
ucf_maxcodepoint_ = cp_from;
if (rev_maxcodepoint_ < cp_to)
rev_maxcodepoint_ = cp_to;
if (rev_maxcodepoint_ < cp_from)
rev_maxcodepoint_ = cp_from;
if (!ucf_countedsegnos.count(segno_from))
{
ucf_countedsegnos[segno_from] = 1;
++ucf_numofsegs_;
}
if (!rev_countedsegnos.count(segno_to))
{
rev_countedsegnos[segno_to] = 1;
++rev_numofsegs_;
}
if (!rev_countedsegnos.count(segno_from))
{
rev_countedsegnos[segno_from] = 1;
++rev_numofsegs_;
}
if (!cps_counted_as_foldedto.count(cp_to))
{
cps_counted_as_foldedto[cp_to] = 1;
++numofcps_to_;
}
if (appearance_counts_.count(to))
++appearance_counts_[to];
else
appearance_counts_[to] = 1;
if (max_appearance_ < appearance_counts_[to])
max_appearance_ = appearance_counts_[to];
}
unsigned int maxset() const
{
return max_appearance_ + 1;
}
void out_v2tables(std::string &outdata)
{
const char *const headers[] = {
"template <typename T2, typename T3>\nconst ",
" unicode_casefolding<T2, T3>::",
"[] =\n{\n"
};
create_revtables();
out_lowertable(outdata, headers, "T2", "ucf_deltatable", ucf_deltas_, ucf_segments_);
outdata.append(1, '\n');
out_uppertable(outdata, headers, "T3", "ucf_segmenttable", ucf_segments_);
outdata.append(1, '\n');
out_lowertable(outdata, headers, "T3", "rev_indextable", rev_indices_, rev_segments_);
outdata.append(1, '\n');
out_uppertable(outdata, headers, "T3", "rev_segmenttable", rev_segments_);
outdata.append(1, '\n');
out_cstable(outdata, headers, "T2", "rev_charsettable", rev_charsets_);
}
// Updates ucf_segments_, ucf_deltas_, and rev_charsets_.
void update_tables(const long cp_from, const long cp_to, const long segno_from)
{
if (segno_from >= static_cast<long>(ucf_segments_.size()))
ucf_segments_.resize(segno_from + 1, 0L);
long &offset_of_segment = ucf_segments_[segno_from];
if (offset_of_segment == 0L)
{
offset_of_segment = nextoffset_;
nextoffset_ += 0x100L;
ucf_deltas_.resize(nextoffset_, 0L);
}
ucf_deltas_[offset_of_segment + (cp_from & 0xffL)] = cp_to - cp_from;
for (long index = 0L;; ++index)
{
if (index == static_cast<long>(rev_charsets_.size()))
{
rev_charsets_.push_back(cp_to);
rev_charsets_.push_back(cp_from);
rev_charsets_.push_back(-1L);
break;
}
if (rev_charsets_[index] == cp_to)
{
for (++index; rev_charsets_[index] != -1L; ++index);
rev_charsets_.insert(index, 1, cp_from);
break;
}
}
}
// Creates rev_segments_ and rev_indices_ from rev_charsets_.
void create_revtables()
{
long nextoffset = 0x100L;
for (long index = 0L; index < static_cast<long>(rev_charsets_.size()); ++index)
{
const long bocs = index; // Beginning of charset.
for (; rev_charsets_[index] != -1L; ++index)
{
const long &u21ch = rev_charsets_[index];
const long segno = u21ch >> 8L;
if (segno >= static_cast<long>(rev_segments_.size()))
rev_segments_.resize(segno + 1, 0L);
long &offset_of_segment = rev_segments_[segno];
if (offset_of_segment == 0L)
{
offset_of_segment = nextoffset;
nextoffset += 0x100L;
rev_indices_.resize(nextoffset, 0L);
}
rev_indices_[offset_of_segment + (u21ch & 0xffL)] = bocs;
}
}
}
void out_lowertable(std::string &outdata, const char *const headers[], const char *const type, const char *const funcname, const std::basic_string<long> &table, const std::basic_string<long> &segtable) const
{
int end = static_cast<int>(table.size());
outdata += headers[0];
outdata += type;
outdata += headers[1];
outdata += funcname;
outdata += headers[2];
for (int i = 0; i < end;)
{
const int col = i & 15;
if ((i & 255) == 0)
{
if (i)
{
for (int j = 0; j < static_cast<int>(segtable.size()); ++j)
{
if (segtable[j] == i)
{
outdata += "\n\t// For u+" + unishared::stringify<16>(j, "%.2X") + "xx (" + unishared::stringify<16>(i, "%d") + ")\n";
break;
}
}
}
else
outdata += "\t// For common (0)\n";
}
outdata += col == 0 ? "\t" : (col & 3) == 0 ? " " : " ";
if (table[i] >= 0L)
outdata += unishared::stringify<16>(table[i], "%ld");
else
outdata += "static_cast<", outdata += type, outdata += ">(", outdata += unishared::stringify<16>(table[i], "%ld") + ")";
if (++i == end)
outdata.append(1, '\n');
else if (col == 15)
outdata += ",\n";
else
outdata.append(1, ',');
}
outdata += "};\n";
}
void out_uppertable(std::string &outdata, const char *const headers[], const char *const type, const char *const funcname, const std::basic_string<long> &table) const
{
int end = static_cast<int>(table.size());
outdata += headers[0];
outdata += type;
outdata += headers[1];
outdata += funcname;
outdata += headers[2];
for (int i = 0; i < end;)
{
const int col = i & 15;
outdata += col == 0 ? "\t" : (col & 3) == 0 ? " " : " ";
if (table[i] >= 0)
outdata += unishared::stringify<16>(table[i], "%ld");
else
outdata += "static_cast<", outdata += type, outdata += ">(", outdata += unishared::stringify<16>(table[i], "%ld") + ")";
if (++i == end)
outdata.append(1, '\n');
else if (col == 15)
outdata += ",\n";
else
outdata.append(1, ',');
}
outdata += "};\n";
}
void out_cstable(std::string &outdata, const char *const headers[], const char *const type, const char *const funcname, const std::basic_string<long> &table) const
{
int end = static_cast<int>(table.size());
bool newline = true;
int bos = 0;
int prevprintedbos = -1;
outdata += headers[0];
outdata += type;
outdata += headers[1];
outdata += funcname;
outdata += headers[2];
for (int i = 0; i < end;)
{
const long val = table[i];
outdata += newline ? "\t" : " ";
newline = false;
if (val == -1L)
outdata += "eos";
else
outdata += "0x", outdata += unishared::stringify<16>(val, "%.4lX");
if (++i != end)
outdata.append(1, ',');
if (val == -1L)
{
if (prevprintedbos != bos / 10 || i == end)
{
outdata += "\t// ";
outdata += unishared::stringify<16>(bos, "%d");
prevprintedbos = bos / 10;
}
outdata.append(1, '\n');
newline = true;
bos = i;
}
}
outdata += "};\n";
}
typedef std::map<long, char> flagset_type;
long maxdelta_; // = 0L;
long maxdelta_cp_; // = 0L;
long ucf_maxcodepoint_; // = 0L; // The max code point for case-folding.
long rev_maxcodepoint_; // = 0L; // The max code point for reverse lookup.
unsigned int ucf_numofsegs_; // = 1U; // The number of segments in the delta table.
unsigned int rev_numofsegs_; // = 1U; // The number of segments in the table for reverse lookup.
unsigned int numofcps_from_; // = 0U; // The number of code points in "folded from"s.
unsigned int numofcps_to_; // = 0U; // The number of code points in "folded to"s.
flagset_type ucf_countedsegnos; // The set of segment nos marked as "counted" for case-folding.
flagset_type rev_countedsegnos; // The set of segment nos marked as "counted" for reverse lookup.
flagset_type cps_counted_as_foldedto; // The set of code points marked as "folded to".
unsigned int max_appearance_;
std::map<std::string, unsigned int> appearance_counts_;
long nextoffset_;
std::basic_string<long> ucf_deltas_;
std::basic_string<long> ucf_segments_;
std::basic_string<long> rev_indices_;
std::basic_string<long> rev_segments_;
std::basic_string<long> rev_deltas_;
std::basic_string<long> rev_charsets_;
};
// class unicode_casefolding
int main(const int argc, const char *const *const argv)
{
ucf_options ucfopts(argc, argv);
std::string outdata;
unicode_casefolding ucf;
int errorno = ucf.create_ucfdata(outdata, ucfopts);
if (errorno == 0)
{
if (!unishared::write_file(ucfopts.outfilename, outdata))
errorno = 2;
}
return errorno;
}

File diff suppressed because it is too large Load diff