Merge branch 'master' into new_im_hooks

This commit is contained in:
Soreepeong 2024-03-21 00:39:26 +09:00
commit 50f74c55a7
460 changed files with 40079 additions and 12893 deletions

View file

@ -35,7 +35,7 @@ dotnet_naming_rule.private_instance_fields_rule.severity = warning
dotnet_naming_rule.private_instance_fields_rule.style = lower_camel_case_style dotnet_naming_rule.private_instance_fields_rule.style = lower_camel_case_style
dotnet_naming_rule.private_instance_fields_rule.symbols = private_instance_fields_symbols dotnet_naming_rule.private_instance_fields_rule.symbols = private_instance_fields_symbols
dotnet_naming_rule.private_static_fields_rule.severity = warning dotnet_naming_rule.private_static_fields_rule.severity = warning
dotnet_naming_rule.private_static_fields_rule.style = upper_camel_case_style dotnet_naming_rule.private_static_fields_rule.style = lower_camel_case_style
dotnet_naming_rule.private_static_fields_rule.symbols = private_static_fields_symbols dotnet_naming_rule.private_static_fields_rule.symbols = private_static_fields_symbols
dotnet_naming_rule.private_static_readonly_rule.severity = warning dotnet_naming_rule.private_static_readonly_rule.severity = warning
dotnet_naming_rule.private_static_readonly_rule.style = upper_camel_case_style dotnet_naming_rule.private_static_readonly_rule.style = upper_camel_case_style
@ -57,6 +57,7 @@ dotnet_naming_symbols.private_static_fields_symbols.required_modifiers = static
dotnet_naming_symbols.private_static_readonly_symbols.applicable_accessibilities = private dotnet_naming_symbols.private_static_readonly_symbols.applicable_accessibilities = private
dotnet_naming_symbols.private_static_readonly_symbols.applicable_kinds = field dotnet_naming_symbols.private_static_readonly_symbols.applicable_kinds = field
dotnet_naming_symbols.private_static_readonly_symbols.required_modifiers = static,readonly dotnet_naming_symbols.private_static_readonly_symbols.required_modifiers = static,readonly
dotnet_separate_import_directive_groups = true
dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:suggestion dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:suggestion
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:suggestion dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:suggestion
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:suggestion dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:suggestion
@ -97,22 +98,32 @@ resharper_apply_on_completion = true
resharper_auto_property_can_be_made_get_only_global_highlighting = none resharper_auto_property_can_be_made_get_only_global_highlighting = none
resharper_auto_property_can_be_made_get_only_local_highlighting = none resharper_auto_property_can_be_made_get_only_local_highlighting = none
resharper_autodetect_indent_settings = true resharper_autodetect_indent_settings = true
resharper_blank_lines_around_single_line_auto_property = 1
resharper_braces_for_ifelse = required_for_multiline resharper_braces_for_ifelse = required_for_multiline
resharper_can_use_global_alias = false resharper_can_use_global_alias = false
resharper_csharp_align_multiline_parameter = true resharper_csharp_align_multiline_parameter = true
resharper_csharp_align_multiple_declaration = true resharper_csharp_align_multiple_declaration = true
resharper_csharp_empty_block_style = multiline resharper_csharp_empty_block_style = multiline
resharper_csharp_int_align_comments = true resharper_csharp_int_align_comments = false
resharper_csharp_new_line_before_while = true resharper_csharp_new_line_before_while = true
resharper_csharp_wrap_after_declaration_lpar = true resharper_csharp_wrap_after_declaration_lpar = true
resharper_csharp_wrap_after_invocation_lpar = true
resharper_csharp_wrap_arguments_style = chop_if_long
resharper_enforce_line_ending_style = true resharper_enforce_line_ending_style = true
resharper_instance_members_qualify_declared_in = this_class, base_class
resharper_int_align = false
resharper_member_can_be_private_global_highlighting = none resharper_member_can_be_private_global_highlighting = none
resharper_member_can_be_private_local_highlighting = none resharper_member_can_be_private_local_highlighting = none
resharper_new_line_before_finally = false resharper_new_line_before_finally = true
resharper_parentheses_non_obvious_operations = none, multiplicative, additive, arithmetic, shift, bitwise_and, bitwise_exclusive_or, bitwise_inclusive_or, bitwise
resharper_parentheses_redundancy_style = remove_if_not_clarifies_precedence
resharper_place_accessorholder_attribute_on_same_line = false resharper_place_accessorholder_attribute_on_same_line = false
resharper_place_field_attribute_on_same_line = false resharper_place_field_attribute_on_same_line = false
resharper_place_simple_initializer_on_single_line = true
resharper_show_autodetect_configure_formatting_tip = false resharper_show_autodetect_configure_formatting_tip = false
resharper_space_within_single_line_array_initializer_braces = true
resharper_use_indent_from_vs = false resharper_use_indent_from_vs = false
resharper_wrap_array_initializer_style = chop_if_long
# ReSharper inspection severities # ReSharper inspection severities
resharper_arrange_missing_parentheses_highlighting = hint resharper_arrange_missing_parentheses_highlighting = hint

View file

@ -42,7 +42,48 @@ jobs:
with: with:
name: dalamud-artifact name: dalamud-artifact
path: bin\Release path: bin\Release
check_api_compat:
name: "Check API Compatibility"
if: ${{ github.event_name == 'pull_request' }}
needs: build
runs-on: windows-latest
steps:
- name: "Install .NET SDK"
uses: actions/setup-dotnet@v3
with:
dotnet-version: 7
- name: "Install ApiCompat"
run: |
dotnet tool install -g Microsoft.DotNet.ApiCompat.Tool
- name: "Download Proposed Artifacts"
uses: actions/download-artifact@v2
with:
name: dalamud-artifact
path: .\right
- name: "Download Live (Stg) Artifacts"
run: |
Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -OutFile latest.zip
Expand-Archive -Force latest.zip "left"
- name: "Verify Compatibility"
run: |
$FILES_TO_VALIDATE = "Dalamud.dll","FFXIVClientStructs.dll","Lumina.dll","Lumina.Excel.dll"
$retcode = 0
foreach ($file in $FILES_TO_VALIDATE) {
$testout = ""
Write-Output "::group::=== API COMPATIBILITY CHECK: ${file} ==="
apicompat -l "left\${file}" -r "right\${file}" | Tee-Object -Variable testout
Write-Output "::endgroup::"
if ($testout -ne "APICompat ran successfully without finding any breaking changes.") {
Write-Output "::error::${file} did not pass. Please review it for problems."
$retcode = 1
}
}
exit $retcode
deploy_stg: deploy_stg:
name: Deploy dalamud-distrib staging name: Deploy dalamud-distrib staging
if: ${{ github.repository_owner == 'goatcorp' && github.event_name == 'push' }} if: ${{ github.repository_owner == 'goatcorp' && github.event_name == 'push' }}

View file

@ -11,7 +11,8 @@ jobs:
strategy: strategy:
matrix: matrix:
branches: branches:
- v9 - net8
#- new_im_hooks # Unmergeable
defaults: defaults:
run: run:

View file

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

View file

@ -76,6 +76,9 @@
<ClCompile Include="DalamudStartInfo.cpp"> <ClCompile Include="DalamudStartInfo.cpp">
<Filter>Dalamud.Boot DLL</Filter> <Filter>Dalamud.Boot DLL</Filter>
</ClCompile> </ClCompile>
<ClCompile Include="ntdll.cpp">
<Filter>Dalamud.Boot DLL</Filter>
</ClCompile>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ClInclude Include="..\lib\CoreCLR\CoreCLR.h"> <ClInclude Include="..\lib\CoreCLR\CoreCLR.h">
@ -143,6 +146,9 @@
</ClInclude> </ClInclude>
<ClInclude Include="resource.h" /> <ClInclude Include="resource.h" />
<ClInclude Include="crashhandler_shared.h" /> <ClInclude Include="crashhandler_shared.h" />
<ClInclude Include="ntdll.h">
<Filter>Dalamud.Boot DLL</Filter>
</ClInclude>
<ClInclude Include="..\lib\reshade\include\reshade.hpp"> <ClInclude Include="..\lib\reshade\include\reshade.hpp">
<Filter>ReshadePlugin</Filter> <Filter>ReshadePlugin</Filter>
</ClInclude> </ClInclude>
@ -174,4 +180,14 @@
<ItemGroup> <ItemGroup>
<Image Include="dalamud.ico" /> <Image Include="dalamud.ico" />
</ItemGroup> </ItemGroup>
</Project> <ItemGroup>
<MASM Include="rewrite_entrypoint_thunks.asm">
<Filter>Dalamud.Boot DLL</Filter>
</MASM>
</ItemGroup>
<ItemGroup>
<None Include="module.def">
<Filter>Dalamud.Boot DLL</Filter>
</None>
</ItemGroup>
</Project>

View file

@ -68,19 +68,37 @@ void from_json(const nlohmann::json& json, DalamudStartInfo::ClientLanguage& val
} }
} }
void from_json(const nlohmann::json& json, DalamudStartInfo::LoadMethod& value) {
if (json.is_number_integer()) {
value = static_cast<DalamudStartInfo::LoadMethod>(json.get<int>());
}
else if (json.is_string()) {
const auto langstr = unicode::convert<std::string>(json.get<std::string>(), &unicode::lower);
if (langstr == "entrypoint")
value = DalamudStartInfo::LoadMethod::Entrypoint;
else if (langstr == "inject")
value = DalamudStartInfo::LoadMethod::DllInject;
}
}
void from_json(const nlohmann::json& json, DalamudStartInfo& config) { void from_json(const nlohmann::json& json, DalamudStartInfo& config) {
if (!json.is_object()) if (!json.is_object())
return; return;
config.DalamudLoadMethod = json.value("LoadMethod", config.DalamudLoadMethod);
config.WorkingDirectory = json.value("WorkingDirectory", config.WorkingDirectory); config.WorkingDirectory = json.value("WorkingDirectory", config.WorkingDirectory);
config.ConfigurationPath = json.value("ConfigurationPath", config.ConfigurationPath); config.ConfigurationPath = json.value("ConfigurationPath", config.ConfigurationPath);
config.LogPath = json.value("LogPath", config.LogPath);
config.LogName = json.value("LogName", config.LogName);
config.PluginDirectory = json.value("PluginDirectory", config.PluginDirectory); config.PluginDirectory = json.value("PluginDirectory", config.PluginDirectory);
config.DefaultPluginDirectory = json.value("DefaultPluginDirectory", config.DefaultPluginDirectory);
config.AssetDirectory = json.value("AssetDirectory", config.AssetDirectory); config.AssetDirectory = json.value("AssetDirectory", config.AssetDirectory);
config.Language = json.value("Language", config.Language); config.Language = json.value("Language", config.Language);
config.GameVersion = json.value("GameVersion", config.GameVersion); config.GameVersion = json.value("GameVersion", config.GameVersion);
config.DelayInitializeMs = json.value("DelayInitializeMs", config.DelayInitializeMs);
config.TroubleshootingPackData = json.value("TroubleshootingPackData", std::string{}); config.TroubleshootingPackData = json.value("TroubleshootingPackData", std::string{});
config.DelayInitializeMs = json.value("DelayInitializeMs", config.DelayInitializeMs);
config.NoLoadPlugins = json.value("NoLoadPlugins", config.NoLoadPlugins);
config.NoLoadThirdPartyPlugins = json.value("NoLoadThirdPartyPlugins", config.NoLoadThirdPartyPlugins);
config.BootLogPath = json.value("BootLogPath", config.BootLogPath); config.BootLogPath = json.value("BootLogPath", config.BootLogPath);
config.BootShowConsole = json.value("BootShowConsole", config.BootShowConsole); config.BootShowConsole = json.value("BootShowConsole", config.BootShowConsole);
@ -103,6 +121,7 @@ void from_json(const nlohmann::json& json, DalamudStartInfo& config) {
} }
config.CrashHandlerShow = json.value("CrashHandlerShow", config.CrashHandlerShow); config.CrashHandlerShow = json.value("CrashHandlerShow", config.CrashHandlerShow);
config.NoExceptionHandlers = json.value("NoExceptionHandlers", config.NoExceptionHandlers);
} }
void DalamudStartInfo::from_envvars() { void DalamudStartInfo::from_envvars() {

View file

@ -26,15 +26,25 @@ struct DalamudStartInfo {
}; };
friend void from_json(const nlohmann::json&, ClientLanguage&); friend void from_json(const nlohmann::json&, ClientLanguage&);
enum class LoadMethod : int {
Entrypoint,
DllInject,
};
friend void from_json(const nlohmann::json&, LoadMethod&);
LoadMethod DalamudLoadMethod = LoadMethod::Entrypoint;
std::string WorkingDirectory; std::string WorkingDirectory;
std::string ConfigurationPath; std::string ConfigurationPath;
std::string LogPath;
std::string LogName;
std::string PluginDirectory; std::string PluginDirectory;
std::string DefaultPluginDirectory;
std::string AssetDirectory; std::string AssetDirectory;
ClientLanguage Language = ClientLanguage::English; ClientLanguage Language = ClientLanguage::English;
std::string GameVersion; std::string GameVersion;
int DelayInitializeMs = 0;
std::string TroubleshootingPackData; std::string TroubleshootingPackData;
int DelayInitializeMs = 0;
bool NoLoadPlugins;
bool NoLoadThirdPartyPlugins;
std::string BootLogPath; std::string BootLogPath;
bool BootShowConsole = false; bool BootShowConsole = false;
@ -49,6 +59,7 @@ struct DalamudStartInfo {
std::set<std::string> BootUnhookDlls{}; std::set<std::string> BootUnhookDlls{};
bool CrashHandlerShow = false; bool CrashHandlerShow = false;
bool NoExceptionHandlers = false;
friend void from_json(const nlohmann::json&, DalamudStartInfo&); friend void from_json(const nlohmann::json&, DalamudStartInfo&);
void from_envvars(); void from_envvars();

View file

@ -14,6 +14,7 @@ struct exception_info
CONTEXT ContextRecord; CONTEXT ContextRecord;
uint64_t nLifetime; uint64_t nLifetime;
HANDLE hThreadHandle; HANDLE hThreadHandle;
HANDLE hEventHandle;
DWORD dwStackTraceLength; DWORD dwStackTraceLength;
DWORD dwTroubleshootingPackDataLength; DWORD dwTroubleshootingPackDataLength;
}; };

View file

@ -17,7 +17,7 @@ static void OnReshadeOverlay(reshade::api::effect_runtime *runtime) {
s_pfnReshadeOverlayCallback(reinterpret_cast<void*>(runtime->get_native())); s_pfnReshadeOverlayCallback(reinterpret_cast<void*>(runtime->get_native()));
} }
DWORD WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) { HRESULT WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) {
g_startInfo.from_envvars(); g_startInfo.from_envvars();
std::string jsonParseError; std::string jsonParseError;
@ -122,7 +122,7 @@ DWORD WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) {
logging::I("Calling InitializeClrAndGetEntryPoint"); logging::I("Calling InitializeClrAndGetEntryPoint");
void* entrypoint_vfn; void* entrypoint_vfn;
int result = InitializeClrAndGetEntryPoint( const auto result = InitializeClrAndGetEntryPoint(
g_hModule, g_hModule,
g_startInfo.BootEnableEtw, g_startInfo.BootEnableEtw,
runtimeconfig_path, runtimeconfig_path,
@ -132,7 +132,7 @@ DWORD WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) {
L"Dalamud.EntryPoint+InitDelegate, Dalamud", L"Dalamud.EntryPoint+InitDelegate, Dalamud",
&entrypoint_vfn); &entrypoint_vfn);
if (result != 0) if (FAILED(result))
return result; return result;
using custom_component_entry_point_fn = void (CORECLR_DELEGATE_CALLTYPE*)(LPVOID, HANDLE, LPVOID); using custom_component_entry_point_fn = void (CORECLR_DELEGATE_CALLTYPE*)(LPVOID, HANDLE, LPVOID);
@ -141,8 +141,8 @@ DWORD WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) {
// ============================== VEH ======================================== // // ============================== VEH ======================================== //
logging::I("Initializing VEH..."); logging::I("Initializing VEH...");
if (utils::is_running_on_linux()) { if (g_startInfo.NoExceptionHandlers) {
logging::I("=> VEH was disabled, running on linux"); logging::W("=> Exception handlers are disabled from DalamudStartInfo.");
} else if (g_startInfo.BootVehEnabled) { } else if (g_startInfo.BootVehEnabled) {
if (veh::add_handler(g_startInfo.BootVehFull, g_startInfo.WorkingDirectory)) if (veh::add_handler(g_startInfo.BootVehFull, g_startInfo.WorkingDirectory))
logging::I("=> Done!"); logging::I("=> Done!");
@ -164,10 +164,10 @@ DWORD WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) {
entrypoint_fn(lpParam, hMainThreadContinue, g_bReshadeAvailable ? &s_pfnReshadeOverlayCallback : nullptr); entrypoint_fn(lpParam, hMainThreadContinue, g_bReshadeAvailable ? &s_pfnReshadeOverlayCallback : nullptr);
logging::I("Done!"); logging::I("Done!");
return 0; return S_OK;
} }
DllExport DWORD WINAPI Initialize(LPVOID lpParam) { extern "C" DWORD WINAPI Initialize(LPVOID lpParam) {
return InitializeImpl(lpParam, CreateEvent(nullptr, TRUE, FALSE, nullptr)); return InitializeImpl(lpParam, CreateEvent(nullptr, TRUE, FALSE, nullptr));
} }

View file

@ -2,39 +2,9 @@
#include "hooks.h" #include "hooks.h"
#include "ntdll.h"
#include "logging.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() hooks::getprocaddress_singleton_import_hook::getprocaddress_singleton_import_hook()
: m_pfnGetProcAddress(GetProcAddress) : m_pfnGetProcAddress(GetProcAddress)
, m_thunk("kernel32!GetProcAddress(Singleton Import Hook)", , m_thunk("kernel32!GetProcAddress(Singleton Import Hook)",

View file

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

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

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

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

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

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -6,6 +6,7 @@
#include "logging.h" #include "logging.h"
#include "utils.h" #include "utils.h"
#include "hooks.h"
#include "crashhandler_shared.h" #include "crashhandler_shared.h"
#include "DalamudStartInfo.h" #include "DalamudStartInfo.h"
@ -24,8 +25,10 @@
PVOID g_veh_handle = nullptr; PVOID g_veh_handle = nullptr;
bool g_veh_do_full_dump = false; bool g_veh_do_full_dump = false;
std::optional<hooks::import_hook<decltype(SetUnhandledExceptionFilter)>> g_HookSetUnhandledExceptionFilter;
HANDLE g_crashhandler_process = nullptr; HANDLE g_crashhandler_process = nullptr;
HANDLE g_crashhandler_event = nullptr;
HANDLE g_crashhandler_pipe_write = nullptr; HANDLE g_crashhandler_pipe_write = nullptr;
std::recursive_mutex g_exception_handler_mutex; std::recursive_mutex g_exception_handler_mutex;
@ -101,8 +104,24 @@ bool is_ffxiv_address(const wchar_t* module_name, const DWORD64 address)
static void append_injector_launch_args(std::vector<std::wstring>& args) static void append_injector_launch_args(std::vector<std::wstring>& args)
{ {
args.emplace_back(L"-g"); args.emplace_back(L"--game=\"" + utils::loaded_module::current_process().path().wstring() + L"\"");
args.emplace_back(utils::loaded_module::current_process().path().wstring()); switch (g_startInfo.DalamudLoadMethod) {
case DalamudStartInfo::LoadMethod::Entrypoint:
args.emplace_back(L"--mode=entrypoint");
break;
case DalamudStartInfo::LoadMethod::DllInject:
args.emplace_back(L"--mode=inject");
}
args.emplace_back(L"--dalamud-working-directory=\"" + unicode::convert<std::wstring>(g_startInfo.WorkingDirectory) + L"\"");
args.emplace_back(L"--dalamud-configuration-path=\"" + unicode::convert<std::wstring>(g_startInfo.ConfigurationPath) + L"\"");
args.emplace_back(L"--logpath=\"" + unicode::convert<std::wstring>(g_startInfo.LogPath) + L"\"");
args.emplace_back(L"--logname=\"" + unicode::convert<std::wstring>(g_startInfo.LogName) + L"\"");
args.emplace_back(L"--dalamud-plugin-directory=\"" + unicode::convert<std::wstring>(g_startInfo.PluginDirectory) + L"\"");
args.emplace_back(L"--dalamud-asset-directory=\"" + unicode::convert<std::wstring>(g_startInfo.AssetDirectory) + L"\"");
args.emplace_back(std::format(L"--dalamud-client-language={}", static_cast<int>(g_startInfo.Language)));
args.emplace_back(std::format(L"--dalamud-delay-initialize={}", g_startInfo.DelayInitializeMs));
// NoLoadPlugins/NoLoadThirdPartyPlugins: supplied from DalamudCrashHandler
if (g_startInfo.BootShowConsole) if (g_startInfo.BootShowConsole)
args.emplace_back(L"--console"); args.emplace_back(L"--console");
if (g_startInfo.BootEnableEtw) if (g_startInfo.BootEnableEtw)
@ -128,6 +147,85 @@ static void append_injector_launch_args(std::vector<std::wstring>& args)
} }
LONG exception_handler(EXCEPTION_POINTERS* ex) LONG exception_handler(EXCEPTION_POINTERS* ex)
{
// block any other exceptions hitting the handler while the messagebox is open
const auto lock = std::lock_guard(g_exception_handler_mutex);
exception_info exinfo{};
exinfo.pExceptionPointers = ex;
exinfo.ExceptionPointers = *ex;
exinfo.ContextRecord = *ex->ContextRecord;
exinfo.ExceptionRecord = ex->ExceptionRecord ? *ex->ExceptionRecord : EXCEPTION_RECORD{};
const auto time_now = std::chrono::system_clock::now();
auto lifetime = std::chrono::duration_cast<std::chrono::seconds>(
time_now.time_since_epoch()).count()
- std::chrono::duration_cast<std::chrono::seconds>(
g_time_start.time_since_epoch()).count();
exinfo.nLifetime = lifetime;
DuplicateHandle(GetCurrentProcess(), GetCurrentThread(), g_crashhandler_process, &exinfo.hThreadHandle, 0, TRUE, DUPLICATE_SAME_ACCESS);
DuplicateHandle(GetCurrentProcess(), g_crashhandler_event, g_crashhandler_process, &exinfo.hEventHandle, 0, TRUE, DUPLICATE_SAME_ACCESS);
std::wstring stackTrace;
if (!g_clr)
{
stackTrace = L"(no CLR stack trace available)";
}
else if (void* fn; const auto err = static_cast<DWORD>(g_clr->get_function_pointer(
L"Dalamud.EntryPoint, Dalamud",
L"VehCallback",
L"Dalamud.EntryPoint+VehDelegate, Dalamud",
nullptr, nullptr, &fn)))
{
stackTrace = std::format(L"Failed to read stack trace: 0x{:08x}", err);
}
else
{
stackTrace = static_cast<wchar_t*(*)()>(fn)();
// Don't free it, as the program's going to be quit anyway
}
exinfo.dwStackTraceLength = static_cast<DWORD>(stackTrace.size());
exinfo.dwTroubleshootingPackDataLength = static_cast<DWORD>(g_startInfo.TroubleshootingPackData.size());
if (DWORD written; !WriteFile(g_crashhandler_pipe_write, &exinfo, static_cast<DWORD>(sizeof exinfo), &written, nullptr) || sizeof exinfo != written)
return EXCEPTION_CONTINUE_SEARCH;
if (const auto nb = static_cast<DWORD>(std::span(stackTrace).size_bytes()))
{
if (DWORD written; !WriteFile(g_crashhandler_pipe_write, stackTrace.data(), nb, &written, nullptr) || nb != written)
return EXCEPTION_CONTINUE_SEARCH;
}
if (const auto nb = static_cast<DWORD>(std::span(g_startInfo.TroubleshootingPackData).size_bytes()))
{
if (DWORD written; !WriteFile(g_crashhandler_pipe_write, g_startInfo.TroubleshootingPackData.data(), nb, &written, nullptr) || nb != written)
return EXCEPTION_CONTINUE_SEARCH;
}
AllowSetForegroundWindow(GetProcessId(g_crashhandler_process));
HANDLE waitHandles[] = { g_crashhandler_process, g_crashhandler_event };
DWORD waitResult = WaitForMultipleObjects(2, waitHandles, FALSE, INFINITE);
switch (waitResult) {
case WAIT_OBJECT_0:
logging::E("DalamudCrashHandler.exe exited unexpectedly");
break;
case WAIT_OBJECT_0 + 1:
logging::I("Crashing thread was resumed");
break;
default:
logging::E("Unexpected WaitForMultipleObjects return code 0x{:x}", waitResult);
}
return EXCEPTION_CONTINUE_SEARCH;
}
LONG WINAPI structured_exception_handler(EXCEPTION_POINTERS* ex)
{
return exception_handler(ex);
}
LONG WINAPI vectored_exception_handler(EXCEPTION_POINTERS* ex)
{ {
if (ex->ExceptionRecord->ExceptionCode == 0x12345678) if (ex->ExceptionRecord->ExceptionCode == 0x12345678)
{ {
@ -143,50 +241,7 @@ LONG exception_handler(EXCEPTION_POINTERS* ex)
return EXCEPTION_CONTINUE_SEARCH; return EXCEPTION_CONTINUE_SEARCH;
} }
// block any other exceptions hitting the veh while the messagebox is open return exception_handler(ex);
const auto lock = std::lock_guard(g_exception_handler_mutex);
exception_info exinfo{};
exinfo.pExceptionPointers = ex;
exinfo.ExceptionPointers = *ex;
exinfo.ContextRecord = *ex->ContextRecord;
exinfo.ExceptionRecord = ex->ExceptionRecord ? *ex->ExceptionRecord : EXCEPTION_RECORD{};
const auto time_now = std::chrono::system_clock::now();
auto lifetime = std::chrono::duration_cast<std::chrono::seconds>(
time_now.time_since_epoch()).count()
- std::chrono::duration_cast<std::chrono::seconds>(
g_time_start.time_since_epoch()).count();
exinfo.nLifetime = lifetime;
DuplicateHandle(GetCurrentProcess(), GetCurrentThread(), g_crashhandler_process, &exinfo.hThreadHandle, 0, TRUE, DUPLICATE_SAME_ACCESS);
std::wstring stackTrace;
if (void* fn; const auto err = static_cast<DWORD>(g_clr->get_function_pointer(
L"Dalamud.EntryPoint, Dalamud",
L"VehCallback",
L"Dalamud.EntryPoint+VehDelegate, Dalamud",
nullptr, nullptr, &fn)))
{
stackTrace = std::format(L"Failed to read stack trace: 0x{:08x}", err);
}
else
{
stackTrace = static_cast<wchar_t*(*)()>(fn)();
// Don't free it, as the program's going to be quit anyway
}
exinfo.dwStackTraceLength = static_cast<DWORD>(stackTrace.size());
exinfo.dwTroubleshootingPackDataLength = static_cast<DWORD>(g_startInfo.TroubleshootingPackData.size());
if (DWORD written; !WriteFile(g_crashhandler_pipe_write, &exinfo, static_cast<DWORD>(sizeof exinfo), &written, nullptr) || sizeof exinfo != written)
return EXCEPTION_CONTINUE_SEARCH;
if (DWORD written; !WriteFile(g_crashhandler_pipe_write, &stackTrace[0], static_cast<DWORD>(std::span(stackTrace).size_bytes()), &written, nullptr) || std::span(stackTrace).size_bytes() != written)
return EXCEPTION_CONTINUE_SEARCH;
if (DWORD written; !WriteFile(g_crashhandler_pipe_write, &g_startInfo.TroubleshootingPackData[0], static_cast<DWORD>(std::span(g_startInfo.TroubleshootingPackData).size_bytes()), &written, nullptr) || std::span(g_startInfo.TroubleshootingPackData).size_bytes() != written)
return EXCEPTION_CONTINUE_SEARCH;
SuspendThread(GetCurrentThread());
return EXCEPTION_CONTINUE_SEARCH;
} }
bool veh::add_handler(bool doFullDump, const std::string& workingDirectory) bool veh::add_handler(bool doFullDump, const std::string& workingDirectory)
@ -194,8 +249,15 @@ bool veh::add_handler(bool doFullDump, const std::string& workingDirectory)
if (g_veh_handle) if (g_veh_handle)
return false; return false;
g_veh_handle = AddVectoredExceptionHandler(1, exception_handler); g_veh_handle = AddVectoredExceptionHandler(TRUE, vectored_exception_handler);
SetUnhandledExceptionFilter(nullptr);
g_HookSetUnhandledExceptionFilter.emplace("kernel32.dll!SetUnhandledExceptionFilter (lpTopLevelExceptionFilter)", "kernel32.dll", "SetUnhandledExceptionFilter", 0);
g_HookSetUnhandledExceptionFilter->set_detour([](LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter) -> LPTOP_LEVEL_EXCEPTION_FILTER
{
logging::I("Overwriting UnhandledExceptionFilter from {} to {}", reinterpret_cast<ULONG_PTR>(lpTopLevelExceptionFilter), reinterpret_cast<ULONG_PTR>(structured_exception_handler));
return g_HookSetUnhandledExceptionFilter->call_original(structured_exception_handler);
});
SetUnhandledExceptionFilter(structured_exception_handler);
g_veh_do_full_dump = doFullDump; g_veh_do_full_dump = doFullDump;
g_time_start = std::chrono::system_clock::now(); g_time_start = std::chrono::system_clock::now();
@ -308,6 +370,12 @@ bool veh::add_handler(bool doFullDump, const std::string& workingDirectory)
return false; return false;
} }
if (!(g_crashhandler_event = CreateEventW(NULL, FALSE, FALSE, NULL)))
{
logging::W("Failed to create crash synchronization event: CreateEventW error 0x{:x}", GetLastError());
return false;
}
CloseHandle(pi.hThread); CloseHandle(pi.hThread);
g_crashhandler_process = pi.hProcess; g_crashhandler_process = pi.hProcess;
@ -321,6 +389,8 @@ bool veh::remove_handler()
if (g_veh_handle && RemoveVectoredExceptionHandler(g_veh_handle) != 0) if (g_veh_handle && RemoveVectoredExceptionHandler(g_veh_handle) != 0)
{ {
g_veh_handle = nullptr; g_veh_handle = nullptr;
g_HookSetUnhandledExceptionFilter.reset();
SetUnhandledExceptionFilter(nullptr);
return true; return true;
} }
return false; return false;

View file

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

View file

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

View file

@ -0,0 +1,27 @@
namespace Dalamud.Common;
/// <summary>
/// Enum describing the language the game loads in.
/// </summary>
public enum ClientLanguage
{
/// <summary>
/// Indicating a Japanese game client.
/// </summary>
Japanese,
/// <summary>
/// Indicating an English game client.
/// </summary>
English,
/// <summary>
/// Indicating a German game client.
/// </summary>
German,
/// <summary>
/// Indicating a French game client.
/// </summary>
French,
}

View file

@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
</ItemGroup>
</Project>

View file

@ -1,16 +1,14 @@
using System; using Dalamud.Common.Game;
using System.Collections.Generic;
using Dalamud.Game;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace Dalamud; namespace Dalamud.Common;
/// <summary> /// <summary>
/// Struct containing information needed to initialize Dalamud. /// Struct containing information needed to initialize Dalamud.
/// Modify DalamudStartInfo.h and DalamudStartInfo.cpp along with this record.
/// </summary> /// </summary>
[Serializable] [Serializable]
public record DalamudStartInfo : IServiceType public record DalamudStartInfo
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="DalamudStartInfo"/> class. /// Initializes a new instance of the <see cref="DalamudStartInfo"/> class.
@ -21,36 +19,9 @@ public record DalamudStartInfo : IServiceType
} }
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="DalamudStartInfo"/> class. /// Gets or sets the Dalamud load method.
/// </summary> /// </summary>
/// <param name="other">Object to copy values from.</param> public LoadMethod LoadMethod { get; set; }
public DalamudStartInfo(DalamudStartInfo other)
{
this.WorkingDirectory = other.WorkingDirectory;
this.ConfigurationPath = other.ConfigurationPath;
this.LogPath = other.LogPath;
this.LogName = other.LogName;
this.PluginDirectory = other.PluginDirectory;
this.AssetDirectory = other.AssetDirectory;
this.Language = other.Language;
this.GameVersion = other.GameVersion;
this.DelayInitializeMs = other.DelayInitializeMs;
this.TroubleshootingPackData = other.TroubleshootingPackData;
this.NoLoadPlugins = other.NoLoadPlugins;
this.NoLoadThirdPartyPlugins = other.NoLoadThirdPartyPlugins;
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;
this.CrashHandlerShow = other.CrashHandlerShow;
}
/// <summary> /// <summary>
/// Gets or sets the working directory of the XIVLauncher installations. /// Gets or sets the working directory of the XIVLauncher installations.
@ -96,7 +67,7 @@ public record DalamudStartInfo : IServiceType
/// <summary> /// <summary>
/// Gets or sets troubleshooting information to attach when generating a tspack file. /// Gets or sets troubleshooting information to attach when generating a tspack file.
/// </summary> /// </summary>
public string TroubleshootingPackData { get; set; } public string? TroubleshootingPackData { get; set; }
/// <summary> /// <summary>
/// Gets or sets 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.
@ -172,4 +143,9 @@ public record DalamudStartInfo : IServiceType
/// Gets or sets a value indicating whether to show crash handler console window. /// Gets or sets a value indicating whether to show crash handler console window.
/// </summary> /// </summary>
public bool CrashHandlerShow { get; set; } public bool CrashHandlerShow { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to disable all kinds of global exception handlers.
/// </summary>
public bool NoExceptionHandlers { get; set; }
} }

View file

@ -1,11 +1,9 @@
using System;
using System.Globalization; using System.Globalization;
using System.Linq;
using System.Text; using System.Text;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace Dalamud.Game; namespace Dalamud.Common.Game;
/// <summary> /// <summary>
/// A GameVersion object contains give hierarchical numeric components: year, month, /// A GameVersion object contains give hierarchical numeric components: year, month,
@ -168,14 +166,14 @@ public sealed class GameVersion : ICloneable, IComparable, IComparable<GameVersi
return Parse(ver); return Parse(ver);
} }
public static bool operator ==(GameVersion v1, GameVersion v2) public static bool operator ==(GameVersion? v1, GameVersion? v2)
{ {
if (v1 is null) if (v1 is null)
{ {
return v2 is null; return v2 is null;
} }
return v1.Equals(v2); return v2 is not null && v1.Equals(v2);
} }
public static bool operator !=(GameVersion v1, GameVersion v2) public static bool operator !=(GameVersion v1, GameVersion v2)
@ -290,7 +288,7 @@ public sealed class GameVersion : ICloneable, IComparable, IComparable<GameVersi
} }
catch catch
{ {
result = null; result = null!;
return false; return false;
} }
} }
@ -299,7 +297,7 @@ public sealed class GameVersion : ICloneable, IComparable, IComparable<GameVersi
public object Clone() => new GameVersion(this.Year, this.Month, this.Day, this.Major, this.Minor); public object Clone() => new GameVersion(this.Year, this.Month, this.Day, this.Major, this.Minor);
/// <inheritdoc/> /// <inheritdoc/>
public int CompareTo(object obj) public int CompareTo(object? obj)
{ {
if (obj == null) if (obj == null)
return 1; return 1;
@ -315,7 +313,7 @@ public sealed class GameVersion : ICloneable, IComparable, IComparable<GameVersi
} }
/// <inheritdoc/> /// <inheritdoc/>
public int CompareTo(GameVersion value) public int CompareTo(GameVersion? value)
{ {
if (value == null) if (value == null)
return 1; return 1;
@ -348,7 +346,7 @@ public sealed class GameVersion : ICloneable, IComparable, IComparable<GameVersi
} }
/// <inheritdoc/> /// <inheritdoc/>
public override bool Equals(object obj) public override bool Equals(object? obj)
{ {
if (obj is not GameVersion value) if (obj is not GameVersion value)
return false; return false;
@ -357,7 +355,7 @@ public sealed class GameVersion : ICloneable, IComparable, IComparable<GameVersi
} }
/// <inheritdoc/> /// <inheritdoc/>
public bool Equals(GameVersion value) public bool Equals(GameVersion? value)
{ {
if (value == null) if (value == null)
{ {

View file

@ -1,8 +1,6 @@
using System;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace Dalamud.Game; namespace Dalamud.Common.Game;
/// <summary> /// <summary>
/// Converts a <see cref="GameVersion"/> to and from a string (e.g. <c>"2010.01.01.1234.5678"</c>). /// Converts a <see cref="GameVersion"/> to and from a string (e.g. <c>"2010.01.01.1234.5678"</c>).

View file

@ -0,0 +1,17 @@
namespace Dalamud.Common;
/// <summary>
/// Enum describing the method Dalamud has been loaded.
/// </summary>
public enum LoadMethod
{
/// <summary>
/// Load Dalamud by rewriting the games entrypoint.
/// </summary>
Entrypoint,
/// <summary>
/// Load Dalamud via DLL-injection.
/// </summary>
DllInject,
}

View file

@ -27,8 +27,8 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Lumina" Version="3.10.2" /> <PackageReference Include="Lumina" Version="3.16.0" />
<PackageReference Include="Lumina.Excel" Version="6.4.0" /> <PackageReference Include="Lumina.Excel" Version="6.5.2" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.333"> <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.333">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
@ -50,4 +50,10 @@
<Private>false</Private> <Private>false</Private>
</ProjectReference> </ProjectReference>
</ItemGroup> </ItemGroup>
<ItemGroup>
<None Update="Dalamud.CorePlugin.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project> </Project>

View file

@ -0,0 +1,9 @@
{
"Author": "Dalamud Maintainers",
"Name": "CorePlugin",
"Punchline": "Testbed for developing Dalamud features.",
"Description": "Develop and debug internal Dalamud features using CorePlugin. You have full access to all types in Dalamud assembly.",
"InternalName": "CorePlugin",
"ApplicableVersion": "any",
"Tags": []
}

View file

@ -2,11 +2,13 @@ using System;
using System.IO; using System.IO;
using Dalamud.Configuration.Internal; using Dalamud.Configuration.Internal;
using Dalamud.Game;
using Dalamud.Game.Command; using Dalamud.Game.Command;
using Dalamud.Interface.Windowing; using Dalamud.Interface.Windowing;
using Dalamud.Logging;
using Dalamud.Plugin; using Dalamud.Plugin;
using Dalamud.Plugin.Services;
using Dalamud.Utility; using Dalamud.Utility;
using Serilog;
namespace Dalamud.CorePlugin namespace Dalamud.CorePlugin
{ {
@ -37,9 +39,6 @@ namespace Dalamud.CorePlugin
{ {
} }
/// <inheritdoc/>
public string Name => "Dalamud.CorePlugin";
/// <inheritdoc/> /// <inheritdoc/>
public void Dispose() public void Dispose()
{ {
@ -50,36 +49,41 @@ namespace Dalamud.CorePlugin
private readonly WindowSystem windowSystem = new("Dalamud.CorePlugin"); private readonly WindowSystem windowSystem = new("Dalamud.CorePlugin");
private Localization localization; private Localization localization;
private IPluginLog pluginLog;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="PluginImpl"/> class. /// Initializes a new instance of the <see cref="PluginImpl"/> class.
/// </summary> /// </summary>
/// <param name="pluginInterface">Dalamud plugin interface.</param> /// <param name="pluginInterface">Dalamud plugin interface.</param>
/// <param name="log">Logging service.</param> /// <param name="log">Logging service.</param>
public PluginImpl(DalamudPluginInterface pluginInterface, PluginLog log) public PluginImpl(DalamudPluginInterface pluginInterface, IPluginLog log)
{ {
try try
{ {
// this.InitLoc(); // this.InitLoc();
this.Interface = pluginInterface; this.Interface = pluginInterface;
this.pluginLog = log;
this.windowSystem.AddWindow(new PluginWindow()); this.windowSystem.AddWindow(new PluginWindow());
this.Interface.UiBuilder.Draw += this.OnDraw; this.Interface.UiBuilder.Draw += this.OnDraw;
this.Interface.UiBuilder.OpenConfigUi += this.OnOpenConfigUi; this.Interface.UiBuilder.OpenConfigUi += this.OnOpenConfigUi;
this.Interface.UiBuilder.OpenMainUi += this.OnOpenMainUi;
this.Interface.UiBuilder.DefaultFontHandle.ImFontChanged += (fc, _) =>
{
Log.Information($"CorePlugin : DefaultFontHandle.ImFontChanged called {fc}");
};
Service<CommandManager>.Get().AddHandler("/coreplug", new(this.OnCommand) { HelpMessage = $"Access the {this.Name} plugin." }); Service<CommandManager>.Get().AddHandler("/coreplug", new(this.OnCommand) { HelpMessage = "Access the plugin." });
log.Information("CorePlugin ctor!"); log.Information("CorePlugin ctor!");
} }
catch (Exception ex) catch (Exception ex)
{ {
PluginLog.Error(ex, "kaboom"); log.Error(ex, "kaboom");
} }
} }
/// <inheritdoc/>
public string Name => "Dalamud.CorePlugin";
/// <summary> /// <summary>
/// Gets the plugin interface. /// Gets the plugin interface.
/// </summary> /// </summary>
@ -93,8 +97,6 @@ namespace Dalamud.CorePlugin
this.Interface.UiBuilder.Draw -= this.OnDraw; this.Interface.UiBuilder.Draw -= this.OnDraw;
this.windowSystem.RemoveAllWindows(); this.windowSystem.RemoveAllWindows();
this.Interface.ExplicitDispose();
} }
/// <summary> /// <summary>
@ -127,13 +129,13 @@ namespace Dalamud.CorePlugin
} }
catch (Exception ex) catch (Exception ex)
{ {
PluginLog.Error(ex, "Boom"); this.pluginLog.Error(ex, "Boom");
} }
} }
private void OnCommand(string command, string args) private void OnCommand(string command, string args)
{ {
PluginLog.Information("Command called!"); this.pluginLog.Information("Command called!");
// this.window.IsOpen = true; // this.window.IsOpen = true;
} }
@ -143,6 +145,11 @@ namespace Dalamud.CorePlugin
// this.window.IsOpen = true; // this.window.IsOpen = true;
} }
private void OnOpenMainUi()
{
Log.Verbose("Opened main UI");
}
#endif #endif
} }
} }

View file

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

View file

@ -81,12 +81,6 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<!-- This prevents us from having to include Dalamud itself as a dependency --> <ProjectReference Include="..\Dalamud.Common\Dalamud.Common.csproj" />
<!-- If the files move just update the paths here -->
<Compile Include="..\Dalamud\ClientLanguage.cs" Link="Included\%(Filename)%(Extension)" />
<Compile Include="..\Dalamud\IServiceType.cs" Link="Included\%(Filename)%(Extension)" />
<Compile Include="..\Dalamud\DalamudStartInfo.cs" Link="Included\%(Filename)%(Extension)" />
<Compile Include="..\Dalamud\Game\GameVersion.cs" Link="Included\Game\%(Filename)%(Extension)" />
<Compile Include="..\Dalamud\Game\GameVersionConverter.cs" Link="Included\Game\%(Filename)%(Extension)" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View file

@ -9,7 +9,8 @@ using System.Runtime.InteropServices;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Dalamud.Game; using Dalamud.Common;
using Dalamud.Common.Game;
using Newtonsoft.Json; using Newtonsoft.Json;
using Reloaded.Memory.Buffers; using Reloaded.Memory.Buffers;
using Serilog; using Serilog;
@ -30,88 +31,100 @@ namespace Dalamud.Injector
/// </summary> /// </summary>
/// <param name="argc">Count of arguments.</param> /// <param name="argc">Count of arguments.</param>
/// <param name="argvPtr">char** string arguments.</param> /// <param name="argvPtr">char** string arguments.</param>
public delegate void MainDelegate(int argc, IntPtr argvPtr); /// <returns>Return value (HRESULT).</returns>
public delegate int MainDelegate(int argc, IntPtr argvPtr);
/// <summary> /// <summary>
/// Start the Dalamud injector. /// Start the Dalamud injector.
/// </summary> /// </summary>
/// <param name="argc">Count of arguments.</param> /// <param name="argc">Count of arguments.</param>
/// <param name="argvPtr">byte** string arguments.</param> /// <param name="argvPtr">byte** string arguments.</param>
public static void Main(int argc, IntPtr argvPtr) /// <returns>Return value (HRESULT).</returns>
public static int Main(int argc, IntPtr argvPtr)
{ {
List<string> args = new(argc); try
unsafe
{ {
var argv = (IntPtr*)argvPtr; List<string> args = new(argc);
for (var i = 0; i < argc; i++)
args.Add(Marshal.PtrToStringUni(argv[i]));
}
Init(args); unsafe
args.Remove("-v"); // Remove "verbose" flag
if (args.Count >= 2 && args[1].ToLowerInvariant() == "launch-test")
{
Environment.Exit(ProcessLaunchTestCommand(args));
return;
}
DalamudStartInfo startInfo = null;
if (args.Count == 1)
{
// No command defaults to inject
args.Add("inject");
args.Add("--all");
#if !DEBUG
args.Add("--warn");
#endif
}
else if (int.TryParse(args[1], out var _))
{
// Assume that PID has been passed.
args.Insert(1, "inject");
// If originally second parameter exists, then assume that it's a base64 encoded start info.
// Dalamud.Injector.exe inject [pid] [base64]
if (args.Count == 4)
{ {
startInfo = JsonConvert.DeserializeObject<DalamudStartInfo>(Encoding.UTF8.GetString(Convert.FromBase64String(args[3]))); var argv = (IntPtr*)argvPtr;
args.RemoveAt(3); for (var i = 0; i < argc; i++)
args.Add(Marshal.PtrToStringUni(argv[i]));
}
Init(args);
args.Remove("-v"); // Remove "verbose" flag
if (args.Count >= 2 && args[1].ToLowerInvariant() == "launch-test")
{
return ProcessLaunchTestCommand(args);
}
DalamudStartInfo startInfo = null;
if (args.Count == 1)
{
// No command defaults to inject
args.Add("inject");
args.Add("--all");
#if !DEBUG
args.Add("--warn");
#endif
}
else if (int.TryParse(args[1], out var _))
{
// Assume that PID has been passed.
args.Insert(1, "inject");
// If originally second parameter exists, then assume that it's a base64 encoded start info.
// Dalamud.Injector.exe inject [pid] [base64]
if (args.Count == 4)
{
startInfo = JsonConvert.DeserializeObject<DalamudStartInfo>(Encoding.UTF8.GetString(Convert.FromBase64String(args[3])));
args.RemoveAt(3);
}
}
startInfo = ExtractAndInitializeStartInfoFromArguments(startInfo, args);
// Remove already handled arguments
args.Remove("--console");
args.Remove("--msgbox1");
args.Remove("--msgbox2");
args.Remove("--msgbox3");
args.Remove("--etw");
args.Remove("--veh");
args.Remove("--veh-full");
args.Remove("--no-plugin");
args.Remove("--no-3rd-plugin");
args.Remove("--crash-handler-console");
args.Remove("--no-exception-handlers");
var mainCommand = args[1].ToLowerInvariant();
if (mainCommand.Length > 0 && mainCommand.Length <= 6 && "inject"[..mainCommand.Length] == mainCommand)
{
return ProcessInjectCommand(args, startInfo);
}
else if (mainCommand.Length > 0 && mainCommand.Length <= 6 &&
"launch"[..mainCommand.Length] == mainCommand)
{
return ProcessLaunchCommand(args, startInfo);
}
else if (mainCommand.Length > 0 && mainCommand.Length <= 4 &&
"help"[..mainCommand.Length] == mainCommand)
{
return ProcessHelpCommand(args, args.Count >= 3 ? args[2] : null);
}
else
{
throw new CommandLineException($"\"{mainCommand}\" is not a valid command.");
} }
} }
catch (Exception e)
startInfo = ExtractAndInitializeStartInfoFromArguments(startInfo, args);
// Remove already handled arguments
args.Remove("--console");
args.Remove("--msgbox1");
args.Remove("--msgbox2");
args.Remove("--msgbox3");
args.Remove("--etw");
args.Remove("--veh");
args.Remove("--veh-full");
args.Remove("--no-plugin");
args.Remove("--no-3rd-plugin");
args.Remove("--crash-handler-console");
var mainCommand = args[1].ToLowerInvariant();
if (mainCommand.Length > 0 && mainCommand.Length <= 6 && "inject"[..mainCommand.Length] == mainCommand)
{ {
Environment.Exit(ProcessInjectCommand(args, startInfo)); Log.Error(e, "Operation failed.");
} return e.HResult;
else if (mainCommand.Length > 0 && mainCommand.Length <= 6 && "launch"[..mainCommand.Length] == mainCommand)
{
Environment.Exit(ProcessLaunchCommand(args, startInfo));
}
else if (mainCommand.Length > 0 && mainCommand.Length <= 4 && "help"[..mainCommand.Length] == mainCommand)
{
Environment.Exit(ProcessHelpCommand(args, args.Count >= 3 ? args[2] : null));
}
else
{
throw new CommandLineException($"\"{mainCommand}\" is not a valid command.");
} }
} }
@ -187,6 +200,7 @@ namespace Dalamud.Injector
CullLogFile(logPath, 1 * 1024 * 1024); CullLogFile(logPath, 1 * 1024 * 1024);
Log.Logger = new LoggerConfiguration() Log.Logger = new LoggerConfiguration()
.WriteTo.Console(standardErrorFromLevel: LogEventLevel.Debug)
.WriteTo.File(logPath, fileSizeLimitBytes: null) .WriteTo.File(logPath, fileSizeLimitBytes: null)
.MinimumLevel.ControlledBy(levelSwitch) .MinimumLevel.ControlledBy(levelSwitch)
.CreateLogger(); .CreateLogger();
@ -375,12 +389,22 @@ namespace Dalamud.Injector
#else #else
startInfo.LogPath ??= xivlauncherDir; startInfo.LogPath ??= xivlauncherDir;
#endif #endif
startInfo.LogName ??= string.Empty;
// Set boot defaults // Set boot defaults
startInfo.BootShowConsole = args.Contains("--console"); startInfo.BootShowConsole = args.Contains("--console");
startInfo.BootEnableEtw = args.Contains("--etw"); startInfo.BootEnableEtw = args.Contains("--etw");
startInfo.BootLogPath = GetLogPath(startInfo.LogPath, "dalamud.boot", startInfo.LogName); startInfo.BootLogPath = GetLogPath(startInfo.LogPath, "dalamud.boot", startInfo.LogName);
startInfo.BootEnabledGameFixes = new List<string> { "prevent_devicechange_crashes", "disable_game_openprocess_access_check", "redirect_openprocess", "backup_userdata_save", "prevent_icmphandle_crashes" }; startInfo.BootEnabledGameFixes = new()
{
// See: xivfixes.h, xivfixes.cpp
"prevent_devicechange_crashes",
"disable_game_openprocess_access_check",
"redirect_openprocess",
"backup_userdata_save",
"prevent_icmphandle_crashes",
"symbol_load_patches",
};
startInfo.BootDotnetOpenProcessHookMode = 0; startInfo.BootDotnetOpenProcessHookMode = 0;
startInfo.BootWaitMessageBox |= args.Contains("--msgbox1") ? 1 : 0; startInfo.BootWaitMessageBox |= args.Contains("--msgbox1") ? 1 : 0;
startInfo.BootWaitMessageBox |= args.Contains("--msgbox2") ? 2 : 0; startInfo.BootWaitMessageBox |= args.Contains("--msgbox2") ? 2 : 0;
@ -392,6 +416,7 @@ namespace Dalamud.Injector
startInfo.NoLoadThirdPartyPlugins = args.Contains("--no-3rd-plugin"); startInfo.NoLoadThirdPartyPlugins = args.Contains("--no-3rd-plugin");
// startInfo.BootUnhookDlls = new List<string>() { "kernel32.dll", "ntdll.dll", "user32.dll" }; // startInfo.BootUnhookDlls = new List<string>() { "kernel32.dll", "ntdll.dll", "user32.dll" };
startInfo.CrashHandlerShow = args.Contains("--crash-handler-console"); startInfo.CrashHandlerShow = args.Contains("--crash-handler-console");
startInfo.NoExceptionHandlers = args.Contains("--no-exception-handlers");
return startInfo; return startInfo;
} }
@ -433,7 +458,7 @@ namespace Dalamud.Injector
Console.WriteLine("Verbose logging:\t[-v]"); Console.WriteLine("Verbose logging:\t[-v]");
Console.WriteLine("Show Console:\t[--console] [--crash-handler-console]"); Console.WriteLine("Show Console:\t[--console] [--crash-handler-console]");
Console.WriteLine("Enable ETW:\t[--etw]"); Console.WriteLine("Enable ETW:\t[--etw]");
Console.WriteLine("Enable VEH:\t[--veh], [--veh-full]"); Console.WriteLine("Enable VEH:\t[--veh], [--veh-full], [--no-exception-handlers]");
Console.WriteLine("Show messagebox:\t[--msgbox1], [--msgbox2], [--msgbox3]"); Console.WriteLine("Show messagebox:\t[--msgbox1], [--msgbox2], [--msgbox3]");
Console.WriteLine("No plugins:\t[--no-plugin] [--no-3rd-plugin]"); Console.WriteLine("No plugins:\t[--no-plugin] [--no-3rd-plugin]");
Console.WriteLine("Logging:\t[--logname=<logfile suffix>] [--logpath=<log base directory>]"); Console.WriteLine("Logging:\t[--logname=<logfile suffix>] [--logpath=<log base directory>]");
@ -677,11 +702,11 @@ namespace Dalamud.Injector
mode = mode == null ? "entrypoint" : mode.ToLowerInvariant(); mode = mode == null ? "entrypoint" : mode.ToLowerInvariant();
if (mode.Length > 0 && mode.Length <= 10 && "entrypoint"[0..mode.Length] == mode) if (mode.Length > 0 && mode.Length <= 10 && "entrypoint"[0..mode.Length] == mode)
{ {
mode = "entrypoint"; dalamudStartInfo.LoadMethod = LoadMethod.Entrypoint;
} }
else if (mode.Length > 0 && mode.Length <= 6 && "inject"[0..mode.Length] == mode) else if (mode.Length > 0 && mode.Length <= 6 && "inject"[0..mode.Length] == mode)
{ {
mode = "inject"; dalamudStartInfo.LoadMethod = LoadMethod.DllInject;
} }
else else
{ {
@ -793,16 +818,12 @@ namespace Dalamud.Injector
noFixAcl, noFixAcl,
p => p =>
{ {
if (!withoutDalamud && mode == "entrypoint") if (!withoutDalamud && dalamudStartInfo.LoadMethod == LoadMethod.Entrypoint)
{ {
var startInfo = AdjustStartInfo(dalamudStartInfo, gamePath); var startInfo = AdjustStartInfo(dalamudStartInfo, gamePath);
Log.Information("Using start info: {0}", JsonConvert.SerializeObject(startInfo)); Log.Information("Using start info: {0}", JsonConvert.SerializeObject(startInfo));
if (RewriteRemoteEntryPointW(p.Handle, gamePath, JsonConvert.SerializeObject(startInfo)) != 0) Marshal.ThrowExceptionForHR(
{ RewriteRemoteEntryPointW(p.Handle, gamePath, JsonConvert.SerializeObject(startInfo)));
Log.Error("[HOOKS] RewriteRemoteEntryPointW failed");
throw new Exception("RewriteRemoteEntryPointW failed");
}
Log.Verbose("RewriteRemoteEntryPointW called!"); Log.Verbose("RewriteRemoteEntryPointW called!");
} }
}, },
@ -810,7 +831,7 @@ namespace Dalamud.Injector
Log.Verbose("Game process started with PID {0}", process.Id); Log.Verbose("Game process started with PID {0}", process.Id);
if (!withoutDalamud && mode == "inject") if (!withoutDalamud && dalamudStartInfo.LoadMethod == LoadMethod.DllInject)
{ {
var startInfo = AdjustStartInfo(dalamudStartInfo, gamePath); var startInfo = AdjustStartInfo(dalamudStartInfo, gamePath);
Log.Information("Using start info: {0}", JsonConvert.SerializeObject(startInfo)); Log.Information("Using start info: {0}", JsonConvert.SerializeObject(startInfo));
@ -888,7 +909,7 @@ namespace Dalamud.Injector
var gameVerStr = File.ReadAllText(Path.Combine(ffxivDir, "ffxivgame.ver")); var gameVerStr = File.ReadAllText(Path.Combine(ffxivDir, "ffxivgame.ver"));
var gameVer = GameVersion.Parse(gameVerStr); var gameVer = GameVersion.Parse(gameVerStr);
return new DalamudStartInfo(startInfo) return startInfo with
{ {
GameVersion = gameVer, GameVersion = gameVer,
}; };

View file

@ -1,85 +0,0 @@
using System.Diagnostics.CodeAnalysis;
namespace Dalamud.Interface;
internal static class ArrayExtensions
{
/// <summary> Iterate over enumerables with additional index. </summary>
public static IEnumerable<(T Value, int Index)> WithIndex<T>(this IEnumerable<T> list)
=> list.Select((x, i) => (x, i));
/// <summary> Remove an added index from an indexed enumerable. </summary>
public static IEnumerable<T> WithoutIndex<T>(this IEnumerable<(T Value, int Index)> list)
=> list.Select(x => x.Value);
/// <summary> Remove the value and only keep the index from an indexed enumerable. </summary>
public static IEnumerable<int> WithoutValue<T>(this IEnumerable<(T Value, int Index)> list)
=> list.Select(x => x.Index);
// Find the index of the first object fulfilling predicate's criteria in the given list.
// Returns -1 if no such object is found.
public static int IndexOf<T>(this IEnumerable<T> array, Predicate<T> predicate)
{
var i = 0;
foreach (var obj in array)
{
if (predicate(obj))
return i;
++i;
}
return -1;
}
// Find the index of the first occurrence of needle in the given list.
// Returns -1 if needle is not contained in the list.
public static int IndexOf<T>(this IEnumerable<T> array, T needle) where T : notnull
{
var i = 0;
foreach (var obj in array)
{
if (needle.Equals(obj))
return i;
++i;
}
return -1;
}
// Find the first object fulfilling predicate's criteria in the given list, if one exists.
// Returns true if an object is found, false otherwise.
public static bool FindFirst<T>(this IEnumerable<T> array, Predicate<T> predicate, [NotNullWhen(true)] out T? result)
{
foreach (var obj in array)
{
if (predicate(obj))
{
result = obj!;
return true;
}
}
result = default;
return false;
}
// Find the first occurrence of needle in the given list and return the value contained in the list in result.
// Returns true if an object is found, false otherwise.
public static bool FindFirst<T>(this IEnumerable<T> array, T needle, [NotNullWhen(true)] out T? result) where T : notnull
{
foreach (var obj in array)
{
if (obj.Equals(needle))
{
result = obj;
return true;
}
}
result = default;
return false;
}
}

View file

@ -1,17 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0-windows</TargetFramework>
<PlatformTarget>x64</PlatformTarget>
<Platforms>x64;AnyCPU</Platforms>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<RootNamespace>Dalamud.Interface</RootNamespace>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\lib\ImGuiScene\deps\ImGui.NET\src\ImGui.NET-472\ImGui.NET-472.csproj" />
</ItemGroup>
</Project>

View file

@ -1,41 +0,0 @@
using Dalamud.Interface.Raii;
using ImGuiNET;
namespace Dalamud.Interface;
public static class ImGuiTable
{
// Draw a simple table with the given data using the drawRow action.
// Headers and thus columns and column count are defined by columnTitles.
public static void DrawTable<T>(string label, IEnumerable<T> data, Action<T> drawRow, ImGuiTableFlags flags = ImGuiTableFlags.None,
params string[] columnTitles)
{
if (columnTitles.Length == 0)
return;
using var table = ImRaii.Table(label, columnTitles.Length, flags);
if (!table)
return;
foreach (var title in columnTitles)
{
ImGui.TableNextColumn();
ImGui.TableHeader(title);
}
foreach (var datum in data)
{
ImGui.TableNextRow();
drawRow(datum);
}
}
// Draw a simple table with the given data using the drawRow action inside a collapsing header.
// Headers and thus columns and column count are defined by columnTitles.
public static void DrawTabbedTable<T>(string label, IEnumerable<T> data, Action<T> drawRow, ImGuiTableFlags flags = ImGuiTableFlags.None,
params string[] columnTitles)
{
if (ImGui.CollapsingHeader(label))
DrawTable($"{label}##Table", data, drawRow, flags, columnTitles);
}
}

View file

@ -1,6 +0,0 @@
namespace Dalamud.Interface;
public static class InterfaceHelpers
{
public static float GlobalScale = 1.0f;
}

View file

@ -1,4 +1,4 @@
using Dalamud.Game; using Dalamud.Common.Game;
using Xunit; using Xunit;
namespace Dalamud.Test.Game namespace Dalamud.Test.Game

View file

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

View file

@ -1,5 +1,7 @@
namespace Dalamud; namespace Dalamud;
// TODO(v10): Delete this, and use Dalamud.Common.ClientLanguage instead for everything.
/// <summary> /// <summary>
/// Enum describing the language the game loads in. /// Enum describing the language the game loads in.
/// </summary> /// </summary>

View file

@ -1,12 +1,16 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Dalamud.Game.Text; using Dalamud.Game.Text;
using Dalamud.Interface.FontIdentifier;
using Dalamud.Interface.Internal.Windows.PluginInstaller;
using Dalamud.Interface.Style; using Dalamud.Interface.Style;
using Dalamud.IoC.Internal;
using Dalamud.Plugin.Internal.Profiles; using Dalamud.Plugin.Internal.Profiles;
using Dalamud.Storage;
using Dalamud.Utility; using Dalamud.Utility;
using Newtonsoft.Json; using Newtonsoft.Json;
using Serilog; using Serilog;
@ -18,7 +22,11 @@ namespace Dalamud.Configuration.Internal;
/// Class containing Dalamud settings. /// Class containing Dalamud settings.
/// </summary> /// </summary>
[Serializable] [Serializable]
internal sealed class DalamudConfiguration : IServiceType [ServiceManager.ProvidedService]
#pragma warning disable SA1015
[InherentDependency<ReliableFileStorage>] // We must still have this when unloading
#pragma warning restore SA1015
internal sealed class DalamudConfiguration : IInternalDisposableService
{ {
private static readonly JsonSerializerSettings SerializerSettings = new() private static readonly JsonSerializerSettings SerializerSettings = new()
{ {
@ -28,7 +36,7 @@ internal sealed class DalamudConfiguration : IServiceType
}; };
[JsonIgnore] [JsonIgnore]
private string configPath; private string? configPath;
[JsonIgnore] [JsonIgnore]
private bool isSaveQueued; private bool isSaveQueued;
@ -42,12 +50,12 @@ internal sealed class DalamudConfiguration : IServiceType
/// <summary> /// <summary>
/// Event that occurs when dalamud configuration is saved. /// Event that occurs when dalamud configuration is saved.
/// </summary> /// </summary>
public event DalamudConfigurationSavedDelegate DalamudConfigurationSaved; public event DalamudConfigurationSavedDelegate? DalamudConfigurationSaved;
/// <summary> /// <summary>
/// Gets or sets a list of muted works. /// Gets or sets a list of muted works.
/// </summary> /// </summary>
public List<string> BadWords { get; set; } public List<string>? BadWords { get; set; }
/// <summary> /// <summary>
/// Gets or sets a value indicating whether or not the taskbar should flash once a duty is found. /// Gets or sets a value indicating whether or not the taskbar should flash once a duty is found.
@ -62,12 +70,12 @@ internal sealed class DalamudConfiguration : IServiceType
/// <summary> /// <summary>
/// Gets or sets the language code to load Dalamud localization with. /// Gets or sets the language code to load Dalamud localization with.
/// </summary> /// </summary>
public string LanguageOverride { get; set; } = null; public string? LanguageOverride { get; set; } = null;
/// <summary> /// <summary>
/// Gets or sets the last loaded Dalamud version. /// Gets or sets the last loaded Dalamud version.
/// </summary> /// </summary>
public string LastVersion { get; set; } = null; public string? LastVersion { get; set; } = null;
/// <summary> /// <summary>
/// Gets or sets a value indicating the last seen FTUE version. /// Gets or sets a value indicating the last seen FTUE version.
@ -78,7 +86,7 @@ internal sealed class DalamudConfiguration : IServiceType
/// <summary> /// <summary>
/// Gets or sets the last loaded Dalamud version. /// Gets or sets the last loaded Dalamud version.
/// </summary> /// </summary>
public string LastChangelogMajorMinor { get; set; } = null; public string? LastChangelogMajorMinor { get; set; } = null;
/// <summary> /// <summary>
/// Gets or sets the chat type used by default for plugin messages. /// Gets or sets the chat type used by default for plugin messages.
@ -100,6 +108,11 @@ internal sealed class DalamudConfiguration : IServiceType
/// </summary> /// </summary>
public List<ThirdPartyRepoSettings> ThirdRepoList { get; set; } = new(); public List<ThirdPartyRepoSettings> ThirdRepoList { get; set; } = new();
/// <summary>
/// Gets or sets a value indicating whether or not a disclaimer regarding third-party repos has been dismissed.
/// </summary>
public bool? ThirdRepoSpeedbumpDismissed { get; set; } = null;
/// <summary> /// <summary>
/// Gets or sets a list of hidden plugins. /// Gets or sets a list of hidden plugins.
/// </summary> /// </summary>
@ -133,15 +146,18 @@ internal sealed class DalamudConfiguration : IServiceType
/// <summary> /// <summary>
/// Gets or sets a value indicating whether to use AXIS fonts from the game. /// Gets or sets a value indicating whether to use AXIS fonts from the game.
/// </summary> /// </summary>
public bool UseAxisFontsFromGame { get; set; } = false; [Obsolete($"See {nameof(DefaultFontSpec)}")]
public bool UseAxisFontsFromGame { get; set; } = true;
/// <summary> /// <summary>
/// Gets or sets the gamma value to apply for Dalamud fonts. Effects text thickness. /// Gets or sets the default font spec.
///
/// Before gamma is applied...
/// * ...TTF fonts loaded with stb or FreeType are in linear space.
/// * ...the game's prebaked AXIS fonts are in gamma space with gamma value of 1.4.
/// </summary> /// </summary>
public IFontSpec? DefaultFontSpec { get; set; }
/// <summary>
/// Gets or sets the gamma value to apply for Dalamud fonts. Do not use.
/// </summary>
[Obsolete("It happens that nobody touched this setting", true)]
public float FontGammaLevel { get; set; } = 1.4f; public float FontGammaLevel { get; set; } = 1.4f;
/// <summary> /// <summary>
@ -199,6 +215,11 @@ internal sealed class DalamudConfiguration : IServiceType
/// </summary> /// </summary>
public bool LogOpenAtStartup { get; set; } public bool LogOpenAtStartup { get; set; }
/// <summary>
/// Gets or sets the number of lines to keep for the Dalamud Console window.
/// </summary>
public int LogLinesLimit { get; set; } = 10000;
/// <summary> /// <summary>
/// Gets or sets a value indicating whether or not the dev bar should open at startup. /// Gets or sets a value indicating whether or not the dev bar should open at startup.
/// </summary> /// </summary>
@ -218,8 +239,16 @@ internal sealed class DalamudConfiguration : IServiceType
/// Gets or sets a value indicating whether or not plugin user interfaces should trigger sound effects. /// Gets or sets a value indicating whether or not plugin user interfaces should trigger sound effects.
/// This setting is effected by the in-game "System Sounds" option and volume. /// This setting is effected by the in-game "System Sounds" option and volume.
/// </summary> /// </summary>
[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "ABI")]
public bool EnablePluginUISoundEffects { get; set; } public bool EnablePluginUISoundEffects { get; set; }
/// <summary>
/// Gets or sets a value indicating whether or not an additional button allowing pinning and clickthrough options should be shown
/// on plugin title bars when using the Window System.
/// </summary>
[JsonProperty("EnablePluginUiAdditionalOptionsExperimental")]
public bool EnablePluginUiAdditionalOptions { get; set; } = false;
/// <summary> /// <summary>
/// Gets or sets a value indicating whether viewports should always be disabled. /// Gets or sets a value indicating whether viewports should always be disabled.
/// </summary> /// </summary>
@ -248,7 +277,7 @@ internal sealed class DalamudConfiguration : IServiceType
/// <summary> /// <summary>
/// Gets or sets the kind of beta to download when <see cref="DalamudBetaKey"/> matches the server value. /// Gets or sets the kind of beta to download when <see cref="DalamudBetaKey"/> matches the server value.
/// </summary> /// </summary>
public string DalamudBetaKind { get; set; } public string? DalamudBetaKind { get; set; }
/// <summary> /// <summary>
/// Gets or sets a value indicating whether or not any plugin should be loaded when the game is started. /// Gets or sets a value indicating whether or not any plugin should be loaded when the game is started.
@ -414,26 +443,45 @@ internal sealed class DalamudConfiguration : IServiceType
/// </summary> /// </summary>
public double UiBuilderHitch { get; set; } = 100; public double UiBuilderHitch { get; set; } = 100;
/// <summary>
/// Gets or sets the page of the plugin installer that is shown by default when opened.
/// </summary>
public PluginInstallerWindow.PluginInstallerOpenKind PluginInstallerOpen { get; set; } = PluginInstallerWindow.PluginInstallerOpenKind.AllPlugins;
/// <summary> /// <summary>
/// Load a configuration from the provided path. /// Load a configuration from the provided path.
/// </summary> /// </summary>
/// <param name="path">The path to load the configuration file from.</param> /// <param name="path">Path to read from.</param>
/// <param name="fs">File storage.</param>
/// <returns>The deserialized configuration file.</returns> /// <returns>The deserialized configuration file.</returns>
public static DalamudConfiguration Load(string path) public static DalamudConfiguration Load(string path, ReliableFileStorage fs)
{ {
DalamudConfiguration deserialized = null; DalamudConfiguration deserialized = null;
try try
{ {
deserialized = JsonConvert.DeserializeObject<DalamudConfiguration>(File.ReadAllText(path), SerializerSettings); fs.ReadAllText(path, text =>
{
deserialized =
JsonConvert.DeserializeObject<DalamudConfiguration>(text, SerializerSettings);
// If this reads as null, the file was empty, that's no good
if (deserialized == null)
throw new Exception("Read config was null.");
});
} }
catch (Exception ex) catch (FileNotFoundException)
{ {
Log.Warning(ex, "Failed to load DalamudConfiguration at {0}", path); // ignored
}
catch (Exception e)
{
Log.Error(e, "Could not load DalamudConfiguration at {Path}, creating new", path);
} }
deserialized ??= new DalamudConfiguration(); deserialized ??= new DalamudConfiguration();
deserialized.configPath = path; deserialized.configPath = path;
return deserialized; return deserialized;
} }
@ -452,6 +500,13 @@ internal sealed class DalamudConfiguration : IServiceType
{ {
this.Save(); this.Save();
} }
/// <inheritdoc/>
void IInternalDisposableService.DisposeService()
{
// Make sure that we save, if a save is queued while we are shutting down
this.Update();
}
/// <summary> /// <summary>
/// Save the file, if needed. Only needs to be done once a frame. /// Save the file, if needed. Only needs to be done once a frame.
@ -470,8 +525,11 @@ internal sealed class DalamudConfiguration : IServiceType
private void Save() private void Save()
{ {
ThreadSafety.AssertMainThread(); ThreadSafety.AssertMainThread();
if (this.configPath is null)
throw new InvalidOperationException("configPath is not set.");
Util.WriteAllTextSafe(this.configPath, JsonConvert.SerializeObject(this, SerializerSettings)); Service<ReliableFileStorage>.Get().WriteAllText(
this.configPath, JsonConvert.SerializeObject(this, SerializerSettings));
this.DalamudConfigurationSaved?.Invoke(this); this.DalamudConfigurationSaved?.Invoke(this);
} }
} }

View file

@ -1,3 +1,5 @@
using System;
namespace Dalamud.Configuration.Internal; namespace Dalamud.Configuration.Internal;
/// <summary> /// <summary>
@ -14,4 +16,9 @@ internal sealed class DevPluginSettings
/// Gets or sets a value indicating whether this plugin should automatically reload on file change. /// Gets or sets a value indicating whether this plugin should automatically reload on file change.
/// </summary> /// </summary>
public bool AutomaticReloading { get; set; } = false; public bool AutomaticReloading { get; set; } = false;
/// <summary>
/// Gets or sets an ID uniquely identifying this specific instance of a devPlugin.
/// </summary>
public Guid WorkingPluginId { get; set; } = Guid.Empty;
} }

View file

@ -1,6 +1,6 @@
using System.IO; using System.IO;
using Dalamud.Utility; using Dalamud.Storage;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace Dalamud.Configuration; namespace Dalamud.Configuration;
@ -31,24 +31,39 @@ public sealed class PluginConfigurations
/// </summary> /// </summary>
/// <param name="config">Plugin configuration.</param> /// <param name="config">Plugin configuration.</param>
/// <param name="pluginName">Plugin name.</param> /// <param name="pluginName">Plugin name.</param>
public void Save(IPluginConfiguration config, string pluginName) /// <param name="workingPluginId">WorkingPluginId of the plugin.</param>
public void Save(IPluginConfiguration config, string pluginName, Guid workingPluginId)
{ {
Util.WriteAllTextSafe(this.GetConfigFile(pluginName).FullName, SerializeConfig(config)); Service<ReliableFileStorage>.Get()
.WriteAllText(this.GetConfigFile(pluginName).FullName, SerializeConfig(config), workingPluginId);
} }
/// <summary> /// <summary>
/// Load plugin configuration. /// Load plugin configuration.
/// </summary> /// </summary>
/// <param name="pluginName">Plugin name.</param> /// <param name="pluginName">Plugin name.</param>
/// <param name="workingPluginId">WorkingPluginID of the plugin.</param>
/// <returns>Plugin configuration.</returns> /// <returns>Plugin configuration.</returns>
public IPluginConfiguration? Load(string pluginName) public IPluginConfiguration? Load(string pluginName, Guid workingPluginId)
{ {
var path = this.GetConfigFile(pluginName); var path = this.GetConfigFile(pluginName);
if (!path.Exists) IPluginConfiguration? config = null;
return null; try
{
Service<ReliableFileStorage>.Get().ReadAllText(path.FullName, text =>
{
config = DeserializeConfig(text);
if (config == null)
throw new Exception("Read config was null.");
}, workingPluginId);
}
catch (FileNotFoundException)
{
// ignored
}
return DeserializeConfig(File.ReadAllText(path.FullName)); return config;
} }
/// <summary> /// <summary>

View file

@ -1,4 +1,3 @@
using System;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
@ -7,12 +6,13 @@ using System.Runtime.InteropServices;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Dalamud.Common;
using Dalamud.Configuration.Internal; using Dalamud.Configuration.Internal;
using Dalamud.Game; using Dalamud.Game;
using Dalamud.Game.Gui.Internal;
using Dalamud.Interface.Internal;
using Dalamud.Plugin.Internal; using Dalamud.Plugin.Internal;
using Dalamud.Storage;
using Dalamud.Utility; using Dalamud.Utility;
using Dalamud.Utility.Timing;
using PInvoke; using PInvoke;
using Serilog; using Serilog;
@ -28,6 +28,7 @@ namespace Dalamud;
/// <summary> /// <summary>
/// The main Dalamud class containing all subsystems. /// The main Dalamud class containing all subsystems.
/// </summary> /// </summary>
[ServiceManager.ProvidedService]
internal sealed class Dalamud : IServiceType internal sealed class Dalamud : IServiceType
{ {
#region Internals #region Internals
@ -40,26 +41,48 @@ internal sealed class Dalamud : IServiceType
/// Initializes a new instance of the <see cref="Dalamud"/> class. /// Initializes a new instance of the <see cref="Dalamud"/> class.
/// </summary> /// </summary>
/// <param name="info">DalamudStartInfo instance.</param> /// <param name="info">DalamudStartInfo instance.</param>
/// <param name="fs">ReliableFileStorage instance.</param>
/// <param name="configuration">The Dalamud configuration.</param> /// <param name="configuration">The Dalamud configuration.</param>
/// <param name="mainThreadContinueEvent">Event used to signal the main thread to continue.</param> /// <param name="mainThreadContinueEvent">Event used to signal the main thread to continue.</param>
public Dalamud(DalamudStartInfo info, DalamudConfiguration configuration, IntPtr mainThreadContinueEvent) public Dalamud(DalamudStartInfo info, ReliableFileStorage fs, DalamudConfiguration configuration, IntPtr mainThreadContinueEvent)
{ {
this.StartInfo = info;
this.unloadSignal = new ManualResetEvent(false); this.unloadSignal = new ManualResetEvent(false);
this.unloadSignal.Reset(); this.unloadSignal.Reset();
// Directory resolved signatures(CS, our own) will be cached in
var cacheDir = new DirectoryInfo(Path.Combine(this.StartInfo.WorkingDirectory!, "cachedSigs"));
if (!cacheDir.Exists)
cacheDir.Create();
// Set up the SigScanner for our target module
TargetSigScanner scanner;
using (Timings.Start("SigScanner Init"))
{
scanner = new TargetSigScanner(
true, new FileInfo(Path.Combine(cacheDir.FullName, $"{this.StartInfo.GameVersion}.json")));
}
ServiceManager.InitializeProvidedServicesAndClientStructs(this, info, configuration); ServiceManager.InitializeProvidedServices(this, fs, configuration, scanner);
// Set up FFXIVClientStructs
this.SetupClientStructsResolver(cacheDir);
if (!configuration.IsResumeGameAfterPluginLoad) if (!configuration.IsResumeGameAfterPluginLoad)
{ {
NativeFunctions.SetEvent(mainThreadContinueEvent); NativeFunctions.SetEvent(mainThreadContinueEvent);
try ServiceManager.InitializeEarlyLoadableServices()
{ .ContinueWith(t =>
_ = ServiceManager.InitializeEarlyLoadableServices(); {
} if (t.IsCompletedSuccessfully)
catch (Exception e) return;
{
Log.Error(e, "Service initialization failure"); Log.Error(t.Exception!, "Service initialization failure");
} Util.Fatal(
"Dalamud failed to load all necessary services.\n\nThe game will continue, but you may not be able to use plugins.",
"Dalamud", false);
});
} }
else else
{ {
@ -93,13 +116,36 @@ internal sealed class Dalamud : IServiceType
} }
}); });
} }
this.DefaultExceptionFilter = NativeFunctions.SetUnhandledExceptionFilter(nint.Zero);
NativeFunctions.SetUnhandledExceptionFilter(this.DefaultExceptionFilter);
Log.Debug($"SE default exception filter at {this.DefaultExceptionFilter.ToInt64():X}");
var debugSig = "40 55 53 56 48 8D AC 24 ?? ?? ?? ?? B8 ?? ?? ?? ?? E8 ?? ?? ?? ?? 48 2B E0 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 85 ?? ?? ?? ?? 48 83 3D ?? ?? ?? ?? ??";
this.DebugExceptionFilter = Service<TargetSigScanner>.Get().ScanText(debugSig);
Log.Debug($"SE debug exception filter at {this.DebugExceptionFilter.ToInt64():X}");
} }
/// <summary>
/// Gets the start information for this Dalamud instance.
/// </summary>
internal DalamudStartInfo StartInfo { get; private set; }
/// <summary> /// <summary>
/// Gets location of stored assets. /// Gets location of stored assets.
/// </summary> /// </summary>
internal DirectoryInfo AssetDirectory => new(Service<DalamudStartInfo>.Get().AssetDirectory!); internal DirectoryInfo AssetDirectory => new(this.StartInfo.AssetDirectory!);
/// <summary>
/// Gets the in-game default exception filter.
/// </summary>
private nint DefaultExceptionFilter { get; }
/// <summary>
/// Gets the in-game debug exception filter.
/// </summary>
private nint DebugExceptionFilter { get; }
/// <summary> /// <summary>
/// Signal to the crash handler process that we should restart the game. /// Signal to the crash handler process that we should restart the game.
/// </summary> /// </summary>
@ -141,36 +187,38 @@ internal sealed class Dalamud : IServiceType
} }
/// <summary> /// <summary>
/// Dispose subsystems related to plugin handling. /// Replace the current exception handler with the default one.
/// </summary> /// </summary>
public void DisposePlugins() internal void UseDefaultExceptionHandler() =>
{ this.SetExceptionHandler(this.DefaultExceptionFilter);
// this must be done before unloading interface manager, in order to do rebuild
// the correct cascaded WndProc (IME -> RawDX11Scene -> Game). Otherwise the game
// will not receive any windows messages
Service<DalamudIME>.GetNullable()?.Dispose();
// this must be done before unloading plugins, or it can cause a race condition
// due to rendering happening on another thread, where a plugin might receive
// a render call after it has been disposed, which can crash if it attempts to
// use any resources that it freed in its own Dispose method
Service<InterfaceManager>.GetNullable()?.Dispose();
Service<DalamudInterface>.GetNullable()?.Dispose();
Service<PluginManager>.GetNullable()?.Dispose();
}
/// <summary> /// <summary>
/// Replace the built-in exception handler with a debug one. /// Replace the current exception handler with a debug one.
/// </summary> /// </summary>
internal void ReplaceExceptionHandler() internal void UseDebugExceptionHandler() =>
{ this.SetExceptionHandler(this.DebugExceptionFilter);
var releaseSig = "40 55 53 56 48 8D AC 24 ?? ?? ?? ?? B8 ?? ?? ?? ?? E8 ?? ?? ?? ?? 48 2B E0 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 85 ?? ?? ?? ?? 48 83 3D ?? ?? ?? ?? ??";
var releaseFilter = Service<SigScanner>.Get().ScanText(releaseSig);
Log.Debug($"SE debug filter at {releaseFilter.ToInt64():X}");
var oldFilter = NativeFunctions.SetUnhandledExceptionFilter(releaseFilter); /// <summary>
Log.Debug("Reset ExceptionFilter, old: {0}", oldFilter); /// Disable the current exception handler.
/// </summary>
internal void UseNoExceptionHandler() =>
this.SetExceptionHandler(nint.Zero);
/// <summary>
/// Helper function to set the exception handler.
/// </summary>
private void SetExceptionHandler(nint newFilter)
{
var oldFilter = NativeFunctions.SetUnhandledExceptionFilter(newFilter);
Log.Debug("Set ExceptionFilter to {0}, old: {1}", newFilter, oldFilter);
}
private void SetupClientStructsResolver(DirectoryInfo cacheDir)
{
using (Timings.Start("CS Resolver Init"))
{
FFXIVClientStructs.Interop.Resolver.GetInstance.SetupSearchSpace(Service<TargetSigScanner>.Get().SearchBase, new FileInfo(Path.Combine(cacheDir.FullName, $"{this.StartInfo.GameVersion}_cs.json")));
FFXIVClientStructs.Interop.Resolver.GetInstance.Resolve();
}
} }
} }

View file

@ -8,11 +8,12 @@
</PropertyGroup> </PropertyGroup>
<PropertyGroup Label="Feature"> <PropertyGroup Label="Feature">
<DalamudVersion>7.10.1.0</DalamudVersion> <DalamudVersion>9.0.0.21</DalamudVersion>
<Description>XIV Launcher addon framework</Description> <Description>XIV Launcher addon framework</Description>
<AssemblyVersion>$(DalamudVersion)</AssemblyVersion> <AssemblyVersion>$(DalamudVersion)</AssemblyVersion>
<Version>$(DalamudVersion)</Version> <Version>$(DalamudVersion)</Version>
<FileVersion>$(DalamudVersion)</FileVersion> <FileVersion>$(DalamudVersion)</FileVersion>
<PackageLicenseExpression>AGPL-3.0-or-later</PackageLicenseExpression>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Label="Output"> <PropertyGroup Label="Output">
@ -67,8 +68,12 @@
<PackageReference Include="goaaats.Reloaded.Hooks" Version="4.2.0-goat.4" /> <PackageReference Include="goaaats.Reloaded.Hooks" Version="4.2.0-goat.4" />
<PackageReference Include="goaaats.Reloaded.Assembler" Version="1.0.14-goat.2" /> <PackageReference Include="goaaats.Reloaded.Assembler" Version="1.0.14-goat.2" />
<PackageReference Include="JetBrains.Annotations" Version="2021.2.0" /> <PackageReference Include="JetBrains.Annotations" Version="2021.2.0" />
<PackageReference Include="Lumina" Version="3.10.2" /> <PackageReference Include="Lumina" Version="3.16.0" />
<PackageReference Include="Lumina.Excel" Version="6.4.0" /> <PackageReference Include="Lumina.Excel" Version="6.5.2" />
<PackageReference Include="Microsoft.Extensions.ObjectPool" Version="8.0.1" />
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.46-beta">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="MinSharp" Version="1.0.4" /> <PackageReference Include="MinSharp" Version="1.0.4" />
<PackageReference Include="MonoModReorg.RuntimeDetour" Version="23.1.2-prerelease.1" /> <PackageReference Include="MonoModReorg.RuntimeDetour" Version="23.1.2-prerelease.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
@ -76,6 +81,7 @@
<PackageReference Include="Serilog.Sinks.Async" Version="1.5.0" /> <PackageReference Include="Serilog.Sinks.Async" Version="1.5.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="4.0.1" /> <PackageReference Include="Serilog.Sinks.Console" Version="4.0.1" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" /> <PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageReference Include="sqlite-net-pcl" Version="1.8.116" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.333"> <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.333">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@ -85,9 +91,10 @@
<PackageReference Include="System.Reactive" Version="5.0.0" /> <PackageReference Include="System.Reactive" Version="5.0.0" />
<PackageReference Include="System.Reflection.MetadataLoadContext" Version="7.0.0" /> <PackageReference Include="System.Reflection.MetadataLoadContext" Version="7.0.0" />
<PackageReference Include="System.Resources.Extensions" Version="7.0.0" /> <PackageReference Include="System.Resources.Extensions" Version="7.0.0" />
<PackageReference Include="TerraFX.Interop.Windows" Version="10.0.22621.2" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Dalamud.Interface\Dalamud.Interface.csproj" /> <ProjectReference Include="..\Dalamud.Common\Dalamud.Common.csproj" />
<ProjectReference Include="..\lib\FFXIVClientStructs\FFXIVClientStructs\FFXIVClientStructs.csproj" /> <ProjectReference Include="..\lib\FFXIVClientStructs\FFXIVClientStructs\FFXIVClientStructs.csproj" />
<ProjectReference Include="..\lib\ImGuiScene\deps\ImGui.NET\src\ImGui.NET-472\ImGui.NET-472.csproj" /> <ProjectReference Include="..\lib\ImGuiScene\deps\ImGui.NET\src\ImGui.NET-472\ImGui.NET-472.csproj" />
<ProjectReference Include="..\lib\ImGuiScene\deps\SDL2-CS\SDL2-CS.csproj" /> <ProjectReference Include="..\lib\ImGuiScene\deps\SDL2-CS\SDL2-CS.csproj" />

153
Dalamud/DalamudAsset.cs Normal file
View file

@ -0,0 +1,153 @@
using Dalamud.Storage.Assets;
namespace Dalamud;
/// <summary>
/// Specifies an asset that has been shipped as Dalamud Asset.<br />
/// <strong>Any asset can cease to exist at any point, even if the enum value exists.</strong><br />
/// Either ship your own assets, or be prepared for errors.
/// </summary>
public enum DalamudAsset
{
/// <summary>
/// Nothing.
/// </summary>
[DalamudAsset(DalamudAssetPurpose.Empty, data: new byte[0])]
Unspecified = 0,
/// <summary>
/// <see cref="DalamudAssetPurpose.TextureFromRaw"/>: The fallback empty texture.
/// </summary>
[DalamudAsset(DalamudAssetPurpose.TextureFromRaw, data: new byte[] { 0, 0, 0, 0, 0, 0, 0, 0 })]
[DalamudAssetRawTexture(4, 8, 4, SharpDX.DXGI.Format.BC1_UNorm)]
Empty4X4 = 1000,
/// <summary>
/// <see cref="DalamudAssetPurpose.TextureFromPng"/>: The Dalamud logo.
/// </summary>
[DalamudAsset(DalamudAssetPurpose.TextureFromPng)]
[DalamudAssetPath("UIRes", "logo.png")]
Logo = 1001,
/// <summary>
/// <see cref="DalamudAssetPurpose.TextureFromPng"/>: The Dalamud logo, but smaller.
/// </summary>
[DalamudAsset(DalamudAssetPurpose.TextureFromPng)]
[DalamudAssetPath("UIRes", "tsmLogo.png")]
LogoSmall = 1002,
/// <summary>
/// <see cref="DalamudAssetPurpose.TextureFromPng"/>: The default plugin icon.
/// </summary>
[DalamudAsset(DalamudAssetPurpose.TextureFromPng)]
[DalamudAssetPath("UIRes", "defaultIcon.png")]
DefaultIcon = 1003,
/// <summary>
/// <see cref="DalamudAssetPurpose.TextureFromPng"/>: The disabled plugin icon.
/// </summary>
[DalamudAsset(DalamudAssetPurpose.TextureFromPng)]
[DalamudAssetPath("UIRes", "disabledIcon.png")]
DisabledIcon = 1004,
/// <summary>
/// <see cref="DalamudAssetPurpose.TextureFromPng"/>: The outdated installable plugin icon.
/// </summary>
[DalamudAsset(DalamudAssetPurpose.TextureFromPng)]
[DalamudAssetPath("UIRes", "outdatedInstallableIcon.png")]
OutdatedInstallableIcon = 1005,
/// <summary>
/// <see cref="DalamudAssetPurpose.TextureFromPng"/>: The plugin trouble icon overlay.
/// </summary>
[DalamudAsset(DalamudAssetPurpose.TextureFromPng)]
[DalamudAssetPath("UIRes", "troubleIcon.png")]
TroubleIcon = 1006,
/// <summary>
/// <see cref="DalamudAssetPurpose.TextureFromPng"/>: The plugin trouble icon overlay.
/// </summary>
[DalamudAsset(DalamudAssetPurpose.TextureFromPng)]
[DalamudAssetPath("UIRes", "devPluginIcon.png")]
DevPluginIcon = 1007,
/// <summary>
/// <see cref="DalamudAssetPurpose.TextureFromPng"/>: The plugin update icon overlay.
/// </summary>
[DalamudAsset(DalamudAssetPurpose.TextureFromPng)]
[DalamudAssetPath("UIRes", "updateIcon.png")]
UpdateIcon = 1008,
/// <summary>
/// <see cref="DalamudAssetPurpose.TextureFromPng"/>: The plugin installed icon overlay.
/// </summary>
[DalamudAsset(DalamudAssetPurpose.TextureFromPng)]
[DalamudAssetPath("UIRes", "installedIcon.png")]
InstalledIcon = 1009,
/// <summary>
/// <see cref="DalamudAssetPurpose.TextureFromPng"/>: The third party plugin icon overlay.
/// </summary>
[DalamudAsset(DalamudAssetPurpose.TextureFromPng)]
[DalamudAssetPath("UIRes", "thirdIcon.png")]
ThirdIcon = 1010,
/// <summary>
/// <see cref="DalamudAssetPurpose.TextureFromPng"/>: The installed third party plugin icon overlay.
/// </summary>
[DalamudAsset(DalamudAssetPurpose.TextureFromPng)]
[DalamudAssetPath("UIRes", "thirdInstalledIcon.png")]
ThirdInstalledIcon = 1011,
/// <summary>
/// <see cref="DalamudAssetPurpose.TextureFromPng"/>: The API bump explainer icon.
/// </summary>
[DalamudAsset(DalamudAssetPurpose.TextureFromPng)]
[DalamudAssetPath("UIRes", "changelogApiBump.png")]
ChangelogApiBumpIcon = 1012,
/// <summary>
/// <see cref="DalamudAssetPurpose.TextureFromPng"/>: The background shade for
/// <see cref="Interface.Internal.Windows.TitleScreenMenuWindow"/>.
/// </summary>
[DalamudAsset(DalamudAssetPurpose.TextureFromPng)]
[DalamudAssetPath("UIRes", "tsmShade.png")]
TitleScreenMenuShade = 1013,
/// <summary>
/// <see cref="DalamudAssetPurpose.Font"/>: Noto Sans CJK JP Medium.
/// </summary>
[DalamudAsset(DalamudAssetPurpose.Font)]
[DalamudAssetPath("UIRes", "NotoSansCJKjp-Regular.otf")]
[DalamudAssetPath("UIRes", "NotoSansCJKjp-Medium.otf")]
NotoSansJpMedium = 2000,
/// <summary>
/// <see cref="DalamudAssetPurpose.Font"/>: Noto Sans CJK KR Regular.
/// </summary>
[DalamudAsset(DalamudAssetPurpose.Font)]
[DalamudAssetPath("UIRes", "NotoSansCJKkr-Regular.otf")]
[DalamudAssetPath("UIRes", "NotoSansKR-Regular.otf")]
NotoSansKrRegular = 2001,
/// <summary>
/// <see cref="DalamudAssetPurpose.Font"/>: Inconsolata Regular.
/// </summary>
[DalamudAsset(DalamudAssetPurpose.Font)]
[DalamudAssetPath("UIRes", "Inconsolata-Regular.ttf")]
InconsolataRegular = 2002,
/// <summary>
/// <see cref="DalamudAssetPurpose.Font"/>: FontAwesome Free Solid.
/// </summary>
[DalamudAsset(DalamudAssetPurpose.Font)]
[DalamudAssetPath("UIRes", "FontAwesomeFreeSolid.otf")]
FontAwesomeFreeSolid = 2003,
/// <summary>
/// <see cref="DalamudAssetPurpose.Font"/>: Game symbol fonts being used as webfonts at Lodestone.
/// </summary>
[DalamudAsset(DalamudAssetPurpose.Font, required: false)]
// [DalamudAssetOnlineSource("https://img.finalfantasyxiv.com/lds/pc/global/fonts/FFXIV_Lodestone_SSF.ttf")]
LodestoneGameSymbol = 2004,
}

View file

@ -1,27 +1,20 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Diagnostics; using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO; using System.IO;
using System.Threading; using System.Threading;
using Dalamud.Interface.Internal;
using Dalamud.IoC; using Dalamud.IoC;
using Dalamud.IoC.Internal; using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Dalamud.Utility; using Dalamud.Utility;
using Dalamud.Utility.Timing; using Dalamud.Utility.Timing;
using ImGuiScene;
using JetBrains.Annotations; using JetBrains.Annotations;
using Lumina; using Lumina;
using Lumina.Data; using Lumina.Data;
using Lumina.Data.Files;
using Lumina.Data.Parsing.Tex.Buffers;
using Lumina.Excel; using Lumina.Excel;
using Newtonsoft.Json; using Newtonsoft.Json;
using Serilog; using Serilog;
using SharpDX.DXGI;
namespace Dalamud.Data; namespace Dalamud.Data;
@ -34,39 +27,20 @@ namespace Dalamud.Data;
#pragma warning disable SA1015 #pragma warning disable SA1015
[ResolveVia<IDataManager>] [ResolveVia<IDataManager>]
#pragma warning restore SA1015 #pragma warning restore SA1015
public sealed class DataManager : IDisposable, IServiceType, IDataManager internal sealed class DataManager : IInternalDisposableService, IDataManager
{ {
private const string IconFileFormat = "ui/icon/{0:D3}000/{1}{2:D6}.tex";
private const string HighResolutionIconFileFormat = "ui/icon/{0:D3}000/{1}{2:D6}_hr1.tex";
private readonly Thread luminaResourceThread; private readonly Thread luminaResourceThread;
private readonly CancellationTokenSource luminaCancellationTokenSource; private readonly CancellationTokenSource luminaCancellationTokenSource;
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
private DataManager(DalamudStartInfo dalamudStartInfo, Dalamud dalamud) private DataManager(Dalamud dalamud)
{ {
this.Language = dalamudStartInfo.Language; this.Language = (ClientLanguage)dalamud.StartInfo.Language;
// Set up default values so plugins do not null-reference when data is being loaded.
this.ClientOpCodes = this.ServerOpCodes = new ReadOnlyDictionary<string, ushort>(new Dictionary<string, ushort>());
var baseDir = dalamud.AssetDirectory.FullName;
try try
{ {
Log.Verbose("Starting data load..."); Log.Verbose("Starting data load...");
var zoneOpCodeDict = JsonConvert.DeserializeObject<Dictionary<string, ushort>>(
File.ReadAllText(Path.Combine(baseDir, "UIRes", "serveropcode.json")))!;
this.ServerOpCodes = new ReadOnlyDictionary<string, ushort>(zoneOpCodeDict);
Log.Verbose("Loaded {0} ServerOpCodes.", zoneOpCodeDict.Count);
var clientOpCodeDict = JsonConvert.DeserializeObject<Dictionary<string, ushort>>(
File.ReadAllText(Path.Combine(baseDir, "UIRes", "clientopcode.json")))!;
this.ClientOpCodes = new ReadOnlyDictionary<string, ushort>(clientOpCodeDict);
Log.Verbose("Loaded {0} ClientOpCodes.", clientOpCodeDict.Count);
using (Timings.Start("Lumina Init")) using (Timings.Start("Lumina Init"))
{ {
var luminaOptions = new LuminaOptions var luminaOptions = new LuminaOptions
@ -93,17 +67,20 @@ public sealed class DataManager : IDisposable, IServiceType, IDataManager
Log.Information("Lumina is ready: {0}", this.GameData.DataPath); Log.Information("Lumina is ready: {0}", this.GameData.DataPath);
try if (!dalamud.StartInfo.TroubleshootingPackData.IsNullOrEmpty())
{ {
var tsInfo = try
JsonConvert.DeserializeObject<LauncherTroubleshootingInfo>( {
dalamudStartInfo.TroubleshootingPackData); var tsInfo =
this.HasModifiedGameDataFiles = JsonConvert.DeserializeObject<LauncherTroubleshootingInfo>(
tsInfo?.IndexIntegrity is LauncherTroubleshootingInfo.IndexIntegrityResult.Failed or LauncherTroubleshootingInfo.IndexIntegrityResult.Exception; dalamud.StartInfo.TroubleshootingPackData);
} this.HasModifiedGameDataFiles =
catch tsInfo?.IndexIntegrity is LauncherTroubleshootingInfo.IndexIntegrityResult.Failed or LauncherTroubleshootingInfo.IndexIntegrityResult.Exception;
{ }
// ignored catch
{
// ignored
}
} }
} }
@ -137,25 +114,20 @@ public sealed class DataManager : IDisposable, IServiceType, IDataManager
/// <inheritdoc/> /// <inheritdoc/>
public ClientLanguage Language { get; private set; } public ClientLanguage Language { get; private set; }
/// <inheritdoc/>
public ReadOnlyDictionary<string, ushort> ServerOpCodes { get; private set; }
/// <inheritdoc/>
[UsedImplicitly]
public ReadOnlyDictionary<string, ushort> ClientOpCodes { get; private set; }
/// <inheritdoc/> /// <inheritdoc/>
public GameData GameData { get; private set; } public GameData GameData { get; private set; }
/// <inheritdoc/> /// <inheritdoc/>
public ExcelModule Excel => this.GameData.Excel; public ExcelModule Excel => this.GameData.Excel;
/// <inheritdoc/>
public bool IsDataReady { get; private set; }
/// <inheritdoc/> /// <inheritdoc/>
public bool HasModifiedGameDataFiles { get; private set; } public bool HasModifiedGameDataFiles { get; private set; }
/// <summary>
/// Gets a value indicating whether Game Data is ready to be read.
/// </summary>
internal bool IsDataReady { get; private set; }
#region Lumina Wrappers #region Lumina Wrappers
/// <inheritdoc/> /// <inheritdoc/>
@ -183,162 +155,10 @@ public sealed class DataManager : IDisposable, IServiceType, IDataManager
public bool FileExists(string path) public bool FileExists(string path)
=> this.GameData.FileExists(path); => this.GameData.FileExists(path);
/// <summary>
/// Get a <see cref="TexFile"/> containing the icon with the given ID.
/// </summary>
/// <param name="iconId">The icon ID.</param>
/// <returns>The <see cref="TexFile"/> containing the icon.</returns>
[Obsolete("Use ITextureProvider instead")]
public TexFile? GetIcon(uint iconId)
=> this.GetIcon(this.Language, iconId, false);
/// <inheritdoc/>
[Obsolete("Use ITextureProvider instead")]
public TexFile? GetIcon(uint iconId, bool highResolution)
=> this.GetIcon(this.Language, iconId, highResolution);
/// <inheritdoc/>
[Obsolete("Use ITextureProvider instead")]
public TexFile? GetIcon(bool isHq, uint iconId)
{
var type = isHq ? "hq/" : string.Empty;
return this.GetIcon(type, iconId);
}
/// <summary>
/// Get a <see cref="TexFile"/> containing the icon with the given ID, of the given language.
/// </summary>
/// <param name="iconLanguage">The requested language.</param>
/// <param name="iconId">The icon ID.</param>
/// <returns>The <see cref="TexFile"/> containing the icon.</returns>
[Obsolete("Use ITextureProvider instead")]
public TexFile? GetIcon(ClientLanguage iconLanguage, uint iconId)
=> this.GetIcon(iconLanguage, iconId, false);
/// <inheritdoc/>
[Obsolete("Use ITextureProvider instead")]
public TexFile? GetIcon(ClientLanguage iconLanguage, uint iconId, bool highResolution)
{
var type = iconLanguage switch
{
ClientLanguage.Japanese => "ja/",
ClientLanguage.English => "en/",
ClientLanguage.German => "de/",
ClientLanguage.French => "fr/",
_ => throw new ArgumentOutOfRangeException(nameof(iconLanguage), $"Unknown Language: {iconLanguage}"),
};
return this.GetIcon(type, iconId, highResolution);
}
/// <summary>
/// Get a <see cref="TexFile"/> containing the icon with the given ID, of the given type.
/// </summary>
/// <param name="type">The type of the icon (e.g. 'hq' to get the HQ variant of an item icon).</param>
/// <param name="iconId">The icon ID.</param>
/// <returns>The <see cref="TexFile"/> containing the icon.</returns>
[Obsolete("Use ITextureProvider instead")]
public TexFile? GetIcon(string? type, uint iconId)
=> this.GetIcon(type, iconId, false);
/// <inheritdoc/>
[Obsolete("Use ITextureProvider instead")]
public TexFile? GetIcon(string? type, uint iconId, bool highResolution)
{
var format = highResolution ? HighResolutionIconFileFormat : IconFileFormat;
type ??= string.Empty;
if (type.Length > 0 && !type.EndsWith("/"))
type += "/";
var filePath = string.Format(format, iconId / 1000, type, iconId);
var file = this.GetFile<TexFile>(filePath);
if (type == string.Empty || file != default)
return file;
// Couldn't get specific type, try for generic version.
filePath = string.Format(format, iconId / 1000, string.Empty, iconId);
file = this.GetFile<TexFile>(filePath);
return file;
}
/// <inheritdoc/>
[Obsolete("Use ITextureProvider instead")]
public TexFile? GetHqIcon(uint iconId)
=> this.GetIcon(true, iconId);
/// <inheritdoc/>
[Obsolete("Use ITextureProvider instead")]
[return: NotNullIfNotNull(nameof(tex))]
public TextureWrap? GetImGuiTexture(TexFile? tex)
{
if (tex is null)
return null;
var im = Service<InterfaceManager>.Get();
var buffer = tex.TextureBuffer;
var bpp = 1 << (((int)tex.Header.Format & (int)TexFile.TextureFormat.BppMask) >>
(int)TexFile.TextureFormat.BppShift);
var (dxgiFormat, conversion) = TexFile.GetDxgiFormatFromTextureFormat(tex.Header.Format, false);
if (conversion != TexFile.DxgiFormatConversion.NoConversion || !im.SupportsDxgiFormat((Format)dxgiFormat))
{
dxgiFormat = (int)Format.B8G8R8A8_UNorm;
buffer = buffer.Filter(0, 0, TexFile.TextureFormat.B8G8R8A8);
bpp = 32;
}
var pitch = buffer is BlockCompressionTextureBuffer
? Math.Max(1, (buffer.Width + 3) / 4) * 2 * bpp
: ((buffer.Width * bpp) + 7) / 8;
return im.LoadImageFromDxgiFormat(buffer.RawData, pitch, buffer.Width, buffer.Height, (Format)dxgiFormat);
}
/// <inheritdoc/>
[Obsolete("Use ITextureProvider instead")]
public TextureWrap? GetImGuiTexture(string path)
=> this.GetImGuiTexture(this.GetFile<TexFile>(path));
/// <summary>
/// Get a <see cref="TextureWrap"/> containing the icon with the given ID.
/// </summary>
/// <param name="iconId">The icon ID.</param>
/// <returns>The <see cref="TextureWrap"/> containing the icon.</returns>
/// TODO(v9): remove in api9 in favor of GetImGuiTextureIcon(uint iconId, bool highResolution)
[Obsolete("Use ITextureProvider instead")]
public TextureWrap? GetImGuiTextureIcon(uint iconId)
=> this.GetImGuiTexture(this.GetIcon(iconId, false));
/// <inheritdoc/>
[Obsolete("Use ITextureProvider instead")]
public TextureWrap? GetImGuiTextureIcon(uint iconId, bool highResolution)
=> this.GetImGuiTexture(this.GetIcon(iconId, highResolution));
/// <inheritdoc/>
[Obsolete("Use ITextureProvider instead")]
public TextureWrap? GetImGuiTextureIcon(bool isHq, uint iconId)
=> this.GetImGuiTexture(this.GetIcon(isHq, iconId));
/// <inheritdoc/>
[Obsolete("Use ITextureProvider instead")]
public TextureWrap? GetImGuiTextureIcon(ClientLanguage iconLanguage, uint iconId)
=> this.GetImGuiTexture(this.GetIcon(iconLanguage, iconId));
/// <inheritdoc/>
[Obsolete("Use ITextureProvider instead")]
public TextureWrap? GetImGuiTextureIcon(string type, uint iconId)
=> this.GetImGuiTexture(this.GetIcon(type, iconId));
/// <inheritdoc/>
[Obsolete("Use ITextureProvider instead")]
public TextureWrap? GetImGuiTextureHqIcon(uint iconId)
=> this.GetImGuiTexture(this.GetHqIcon(iconId));
#endregion #endregion
/// <inheritdoc/> /// <inheritdoc/>
void IDisposable.Dispose() void IInternalDisposableService.DisposeService()
{ {
this.luminaCancellationTokenSource.Cancel(); this.luminaCancellationTokenSource.Cancel();
} }

View file

@ -1,4 +1,3 @@
using System;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Net; using System.Net;
@ -6,10 +5,12 @@ using System.Runtime.InteropServices;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Dalamud.Common;
using Dalamud.Configuration.Internal; using Dalamud.Configuration.Internal;
using Dalamud.Logging.Internal; using Dalamud.Logging.Internal;
using Dalamud.Logging.Retention; using Dalamud.Logging.Retention;
using Dalamud.Plugin.Internal; using Dalamud.Plugin.Internal;
using Dalamud.Storage;
using Dalamud.Support; using Dalamud.Support;
using Dalamud.Utility; using Dalamud.Utility;
using Newtonsoft.Json; using Newtonsoft.Json;
@ -162,7 +163,10 @@ public static class EntryPoint
SerilogEventSink.Instance.LogLine += SerilogOnLogLine; SerilogEventSink.Instance.LogLine += SerilogOnLogLine;
// Load configuration first to get some early persistent state, like log level // Load configuration first to get some early persistent state, like log level
var configuration = DalamudConfiguration.Load(info.ConfigurationPath!); #pragma warning disable CS0618 // Type or member is obsolete
var fs = new ReliableFileStorage(Path.GetDirectoryName(info.ConfigurationPath)!);
#pragma warning restore CS0618 // Type or member is obsolete
var configuration = DalamudConfiguration.Load(info.ConfigurationPath!, fs);
// Set the appropriate logging level from the configuration // Set the appropriate logging level from the configuration
if (!configuration.LogSynchronously) if (!configuration.LogSynchronously)
@ -170,7 +174,8 @@ public static class EntryPoint
LogLevelSwitch.MinimumLevel = configuration.LogLevel; LogLevelSwitch.MinimumLevel = configuration.LogLevel;
// Log any unhandled exception. // Log any unhandled exception.
AppDomain.CurrentDomain.UnhandledException += OnUnhandledException; if (!info.NoExceptionHandlers)
AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
TaskScheduler.UnobservedTaskException += OnUnobservedTaskException; TaskScheduler.UnobservedTaskException += OnUnobservedTaskException;
var unloadFailed = false; var unloadFailed = false;
@ -186,15 +191,18 @@ public static class EntryPoint
Log.Information(new string('-', 80)); Log.Information(new string('-', 80));
Log.Information("Initializing a session.."); Log.Information("Initializing a session..");
if (string.IsNullOrEmpty(info.WorkingDirectory))
throw new Exception("Working directory was invalid");
Reloaded.Hooks.Tools.Utilities.FasmBasePath = new DirectoryInfo(info.WorkingDirectory); Reloaded.Hooks.Tools.Utilities.FasmBasePath = new DirectoryInfo(info.WorkingDirectory);
// This is due to GitHub not supporting TLS 1.0, so we enable all TLS versions globally // This is due to GitHub not supporting TLS 1.0, so we enable all TLS versions globally
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12 | SecurityProtocolType.Tls; ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12 | SecurityProtocolType.Tls;
if (!Util.IsLinux()) if (!Util.IsWine())
InitSymbolHandler(info); InitSymbolHandler(info);
var dalamud = new Dalamud(info, configuration, mainThreadContinueEvent); var dalamud = new Dalamud(info, fs, configuration, mainThreadContinueEvent);
Log.Information("This is Dalamud - Core: {GitHash}, CS: {CsGitHash} [{CsVersion}]", Util.GetGitHash(), Util.GetGitHashClientStructs(), FFXIVClientStructs.Interop.Resolver.Version); Log.Information("This is Dalamud - Core: {GitHash}, CS: {CsGitHash} [{CsVersion}]", Util.GetGitHash(), Util.GetGitHashClientStructs(), FFXIVClientStructs.Interop.Resolver.Version);
dalamud.WaitForUnload(); dalamud.WaitForUnload();
@ -216,7 +224,8 @@ public static class EntryPoint
finally finally
{ {
TaskScheduler.UnobservedTaskException -= OnUnobservedTaskException; TaskScheduler.UnobservedTaskException -= OnUnobservedTaskException;
AppDomain.CurrentDomain.UnhandledException -= OnUnhandledException; if (!info.NoExceptionHandlers)
AppDomain.CurrentDomain.UnhandledException -= OnUnhandledException;
Log.Information("Session has ended."); Log.Information("Session has ended.");
Log.CloseAndFlush(); Log.CloseAndFlush();

View file

@ -0,0 +1,107 @@
using System.Runtime.CompilerServices;
using System.Threading;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
namespace Dalamud.Game.Addon;
/// <summary>Argument pool for Addon Lifecycle services.</summary>
[ServiceManager.EarlyLoadedService]
internal sealed class AddonLifecyclePooledArgs : IServiceType
{
private readonly AddonSetupArgs?[] addonSetupArgPool = new AddonSetupArgs?[64];
private readonly AddonFinalizeArgs?[] addonFinalizeArgPool = new AddonFinalizeArgs?[64];
private readonly AddonDrawArgs?[] addonDrawArgPool = new AddonDrawArgs?[64];
private readonly AddonUpdateArgs?[] addonUpdateArgPool = new AddonUpdateArgs?[64];
private readonly AddonRefreshArgs?[] addonRefreshArgPool = new AddonRefreshArgs?[64];
private readonly AddonRequestedUpdateArgs?[] addonRequestedUpdateArgPool = new AddonRequestedUpdateArgs?[64];
private readonly AddonReceiveEventArgs?[] addonReceiveEventArgPool = new AddonReceiveEventArgs?[64];
[ServiceManager.ServiceConstructor]
private AddonLifecyclePooledArgs()
{
}
/// <summary>Rents an instance of an argument.</summary>
/// <param name="arg">The rented instance.</param>
/// <returns>The returner.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public PooledEntry<AddonSetupArgs> Rent(out AddonSetupArgs arg) => new(out arg, this.addonSetupArgPool);
/// <summary>Rents an instance of an argument.</summary>
/// <param name="arg">The rented instance.</param>
/// <returns>The returner.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public PooledEntry<AddonFinalizeArgs> Rent(out AddonFinalizeArgs arg) => new(out arg, this.addonFinalizeArgPool);
/// <summary>Rents an instance of an argument.</summary>
/// <param name="arg">The rented instance.</param>
/// <returns>The returner.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public PooledEntry<AddonDrawArgs> Rent(out AddonDrawArgs arg) => new(out arg, this.addonDrawArgPool);
/// <summary>Rents an instance of an argument.</summary>
/// <param name="arg">The rented instance.</param>
/// <returns>The returner.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public PooledEntry<AddonUpdateArgs> Rent(out AddonUpdateArgs arg) => new(out arg, this.addonUpdateArgPool);
/// <summary>Rents an instance of an argument.</summary>
/// <param name="arg">The rented instance.</param>
/// <returns>The returner.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public PooledEntry<AddonRefreshArgs> Rent(out AddonRefreshArgs arg) => new(out arg, this.addonRefreshArgPool);
/// <summary>Rents an instance of an argument.</summary>
/// <param name="arg">The rented instance.</param>
/// <returns>The returner.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public PooledEntry<AddonRequestedUpdateArgs> Rent(out AddonRequestedUpdateArgs arg) =>
new(out arg, this.addonRequestedUpdateArgPool);
/// <summary>Rents an instance of an argument.</summary>
/// <param name="arg">The rented instance.</param>
/// <returns>The returner.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public PooledEntry<AddonReceiveEventArgs> Rent(out AddonReceiveEventArgs arg) =>
new(out arg, this.addonReceiveEventArgPool);
/// <summary>Returns the object to the pool on dispose.</summary>
/// <typeparam name="T">The type.</typeparam>
public readonly ref struct PooledEntry<T>
where T : AddonArgs, new()
{
private readonly Span<T> pool;
private readonly T obj;
/// <summary>Initializes a new instance of the <see cref="PooledEntry{T}"/> struct.</summary>
/// <param name="arg">An instance of the argument.</param>
/// <param name="pool">The pool to rent from and return to.</param>
public PooledEntry(out T arg, Span<T> pool)
{
this.pool = pool;
foreach (ref var item in pool)
{
if (Interlocked.Exchange(ref item, null) is { } v)
{
this.obj = arg = v;
return;
}
}
this.obj = arg = new();
}
/// <summary>Returns the item to the pool.</summary>
public void Dispose()
{
var tmp = this.obj;
foreach (ref var item in this.pool)
{
if (Interlocked.Exchange(ref item, tmp) is not { } tmp2)
return;
tmp = tmp2;
}
}
}
}

View file

@ -0,0 +1,97 @@
namespace Dalamud.Game.Addon.Events;
/// <summary>
/// Reimplementation of CursorType.
/// </summary>
public enum AddonCursorType
{
/// <summary>
/// Arrow.
/// </summary>
Arrow,
/// <summary>
/// Boot.
/// </summary>
Boot,
/// <summary>
/// Search.
/// </summary>
Search,
/// <summary>
/// Chat Pointer.
/// </summary>
ChatPointer,
/// <summary>
/// Interact.
/// </summary>
Interact,
/// <summary>
/// Attack.
/// </summary>
Attack,
/// <summary>
/// Hand.
/// </summary>
Hand,
/// <summary>
/// Resizeable Left-Right.
/// </summary>
ResizeWE,
/// <summary>
/// Resizeable Up-Down.
/// </summary>
ResizeNS,
/// <summary>
/// Resizeable.
/// </summary>
ResizeNWSR,
/// <summary>
/// Resizeable 4-way.
/// </summary>
ResizeNESW,
/// <summary>
/// Clickable.
/// </summary>
Clickable,
/// <summary>
/// Text Input.
/// </summary>
TextInput,
/// <summary>
/// Text Click.
/// </summary>
TextClick,
/// <summary>
/// Grab.
/// </summary>
Grab,
/// <summary>
/// Chat Bubble.
/// </summary>
ChatBubble,
/// <summary>
/// No Access.
/// </summary>
NoAccess,
/// <summary>
/// Hidden.
/// </summary>
Hidden,
}

View file

@ -0,0 +1,59 @@
using Dalamud.Memory;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace Dalamud.Game.Addon.Events;
/// <summary>
/// This class represents a registered event that a plugin registers with a native ui node.
/// Contains all necessary information to track and clean up events automatically.
/// </summary>
internal unsafe class AddonEventEntry
{
/// <summary>
/// Name of an invalid addon.
/// </summary>
public const string InvalidAddonName = "NullAddon";
private string? addonName;
/// <summary>
/// Gets the pointer to the addons AtkUnitBase.
/// </summary>
required public nint Addon { get; init; }
/// <summary>
/// Gets the name of the addon this args referrers to.
/// </summary>
public string AddonName => this.Addon == nint.Zero ? InvalidAddonName : this.addonName ??= MemoryHelper.ReadString((nint)((AtkUnitBase*)this.Addon)->Name, 0x20);
/// <summary>
/// Gets the pointer to the event source.
/// </summary>
required public nint Node { get; init; }
/// <summary>
/// Gets the handler that gets called when this event is triggered.
/// </summary>
required public IAddonEventManager.AddonEventHandler Handler { get; init; }
/// <summary>
/// Gets the unique id for this event.
/// </summary>
required public uint ParamKey { get; init; }
/// <summary>
/// Gets the event type for this event.
/// </summary>
required public AddonEventType EventType { get; init; }
/// <summary>
/// Gets the event handle for this event.
/// </summary>
required internal IAddonEventHandle Handle { get; init; }
/// <summary>
/// Gets the formatted log string for this AddonEventEntry.
/// </summary>
internal string LogString => $"ParamKey: {this.ParamKey}, Addon: {this.AddonName}, Event: {this.EventType}, GUID: {this.Handle.EventGuid}";
}

View file

@ -0,0 +1,19 @@
namespace Dalamud.Game.Addon.Events;
/// <summary>
/// Class that represents a addon event handle.
/// </summary>
public class AddonEventHandle : IAddonEventHandle
{
/// <inheritdoc/>
public uint ParamKey { get; init; }
/// <inheritdoc/>
public string AddonName { get; init; } = "NullAddon";
/// <inheritdoc/>
public AddonEventType EventType { get; init; }
/// <inheritdoc/>
public Guid EventGuid { get; init; }
}

View file

@ -0,0 +1,97 @@
using System.Runtime.InteropServices;
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace Dalamud.Game.Addon.Events;
/// <summary>
/// Event listener class for managing custom events.
/// </summary>
// Custom event handler tech provided by Pohky, implemented by MidoriKami
internal unsafe class AddonEventListener : IDisposable
{
private ReceiveEventDelegate? receiveEventDelegate;
private AtkEventListener* eventListener;
/// <summary>
/// Initializes a new instance of the <see cref="AddonEventListener"/> class.
/// </summary>
/// <param name="eventHandler">The managed handler to send events to.</param>
public AddonEventListener(ReceiveEventDelegate eventHandler)
{
this.receiveEventDelegate = eventHandler;
this.eventListener = (AtkEventListener*)Marshal.AllocHGlobal(sizeof(AtkEventListener));
this.eventListener->vtbl = (void*)Marshal.AllocHGlobal(sizeof(void*) * 3);
this.eventListener->vfunc[0] = (delegate* unmanaged<void>)&NullSub;
this.eventListener->vfunc[1] = (delegate* unmanaged<void>)&NullSub;
this.eventListener->vfunc[2] = (void*)Marshal.GetFunctionPointerForDelegate(this.receiveEventDelegate);
}
/// <summary>
/// Delegate for receiving custom events.
/// </summary>
/// <param name="self">Pointer to the event listener.</param>
/// <param name="eventType">Event type.</param>
/// <param name="eventParam">Unique Id for this event.</param>
/// <param name="eventData">Event Data.</param>
/// <param name="unknown">Unknown Parameter.</param>
public delegate void ReceiveEventDelegate(AtkEventListener* self, AtkEventType eventType, uint eventParam, AtkEvent* eventData, nint unknown);
/// <summary>
/// Gets the address of this listener.
/// </summary>
public nint Address => (nint)this.eventListener;
/// <inheritdoc />
public void Dispose()
{
if (this.eventListener is null) return;
Marshal.FreeHGlobal((nint)this.eventListener->vtbl);
Marshal.FreeHGlobal((nint)this.eventListener);
this.eventListener = null;
this.receiveEventDelegate = null;
}
/// <summary>
/// Register an event to this event handler.
/// </summary>
/// <param name="addon">Addon that triggers this event.</param>
/// <param name="node">Node to attach event to.</param>
/// <param name="eventType">Event type to trigger this event.</param>
/// <param name="param">Unique id for this event.</param>
public void RegisterEvent(AtkUnitBase* addon, AtkResNode* node, AtkEventType eventType, uint param)
{
if (node is null) return;
Service<Framework>.Get().RunOnFrameworkThread(() =>
{
node->AddEvent(eventType, param, this.eventListener, (AtkResNode*)addon, false);
});
}
/// <summary>
/// Unregister an event from this event handler.
/// </summary>
/// <param name="node">Node to remove the event from.</param>
/// <param name="eventType">Event type that this event is for.</param>
/// <param name="param">Unique id for this event.</param>
public void UnregisterEvent(AtkResNode* node, AtkEventType eventType, uint param)
{
if (node is null) return;
Service<Framework>.Get().RunOnFrameworkThread(() =>
{
node->RemoveEvent(eventType, param, this.eventListener, false);
});
}
[UnmanagedCallersOnly]
private static void NullSub()
{
/* do nothing */
}
}

View file

@ -0,0 +1,262 @@
using System.Collections.Concurrent;
using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Hooking;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Logging.Internal;
using Dalamud.Plugin.Internal.Types;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace Dalamud.Game.Addon.Events;
/// <summary>
/// Service provider for addon event management.
/// </summary>
[InterfaceVersion("1.0")]
[ServiceManager.EarlyLoadedService]
internal unsafe class AddonEventManager : IInternalDisposableService
{
/// <summary>
/// PluginName for Dalamud Internal use.
/// </summary>
public static readonly Guid DalamudInternalKey = Guid.NewGuid();
private static readonly ModuleLog Log = new("AddonEventManager");
[ServiceManager.ServiceDependency]
private readonly AddonLifecycle addonLifecycle = Service<AddonLifecycle>.Get();
private readonly AddonLifecycleEventListener finalizeEventListener;
private readonly AddonEventManagerAddressResolver address;
private readonly Hook<UpdateCursorDelegate> onUpdateCursor;
private readonly ConcurrentDictionary<Guid, PluginEventController> pluginEventControllers;
private AddonCursorType? cursorOverride;
[ServiceManager.ServiceConstructor]
private AddonEventManager(TargetSigScanner sigScanner)
{
this.address = new AddonEventManagerAddressResolver();
this.address.Setup(sigScanner);
this.pluginEventControllers = new ConcurrentDictionary<Guid, PluginEventController>();
this.pluginEventControllers.TryAdd(DalamudInternalKey, new PluginEventController());
this.cursorOverride = null;
this.onUpdateCursor = Hook<UpdateCursorDelegate>.FromAddress(this.address.UpdateCursor, this.UpdateCursorDetour);
this.finalizeEventListener = new AddonLifecycleEventListener(AddonEvent.PreFinalize, string.Empty, this.OnAddonFinalize);
this.addonLifecycle.RegisterListener(this.finalizeEventListener);
this.onUpdateCursor.Enable();
}
private delegate nint UpdateCursorDelegate(RaptureAtkModule* module);
/// <inheritdoc/>
void IInternalDisposableService.DisposeService()
{
this.onUpdateCursor.Dispose();
foreach (var (_, pluginEventController) in this.pluginEventControllers)
{
pluginEventController.Dispose();
}
this.addonLifecycle.UnregisterListener(this.finalizeEventListener);
}
/// <summary>
/// Registers an event handler for the specified addon, node, and type.
/// </summary>
/// <param name="pluginId">Unique ID for this plugin.</param>
/// <param name="atkUnitBase">The parent addon for this event.</param>
/// <param name="atkResNode">The node that will trigger this event.</param>
/// <param name="eventType">The event type for this event.</param>
/// <param name="eventHandler">The handler to call when event is triggered.</param>
/// <returns>IAddonEventHandle used to remove the event.</returns>
internal IAddonEventHandle? AddEvent(Guid pluginId, IntPtr atkUnitBase, IntPtr atkResNode, AddonEventType eventType, IAddonEventManager.AddonEventHandler eventHandler)
{
if (this.pluginEventControllers.TryGetValue(pluginId, out var controller))
{
return controller.AddEvent(atkUnitBase, atkResNode, eventType, eventHandler);
}
else
{
Log.Verbose($"Unable to locate controller for {pluginId}. No event was added.");
}
return null;
}
/// <summary>
/// Unregisters an event handler with the specified event id and event type.
/// </summary>
/// <param name="pluginId">Unique ID for this plugin.</param>
/// <param name="eventHandle">The Unique Id for this event.</param>
internal void RemoveEvent(Guid pluginId, IAddonEventHandle eventHandle)
{
if (this.pluginEventControllers.TryGetValue(pluginId, out var controller))
{
controller.RemoveEvent(eventHandle);
}
else
{
Log.Verbose($"Unable to locate controller for {pluginId}. No event was removed.");
}
}
/// <summary>
/// Force the game cursor to be the specified cursor.
/// </summary>
/// <param name="cursor">Which cursor to use.</param>
internal void SetCursor(AddonCursorType cursor) => this.cursorOverride = cursor;
/// <summary>
/// Un-forces the game cursor.
/// </summary>
internal void ResetCursor() => this.cursorOverride = null;
/// <summary>
/// Adds a new managed event controller if one doesn't already exist for this pluginId.
/// </summary>
/// <param name="pluginId">Unique ID for this plugin.</param>
internal void AddPluginEventController(Guid pluginId)
{
this.pluginEventControllers.GetOrAdd(
pluginId,
key =>
{
Log.Verbose($"Creating new PluginEventController for: {key}");
return new PluginEventController();
});
}
/// <summary>
/// Removes an existing managed event controller for the specified plugin.
/// </summary>
/// <param name="pluginId">Unique ID for this plugin.</param>
internal void RemovePluginEventController(Guid pluginId)
{
if (this.pluginEventControllers.TryRemove(pluginId, out var controller))
{
Log.Verbose($"Removing PluginEventController for: {pluginId}");
controller.Dispose();
}
}
/// <summary>
/// When an addon finalizes, check it for any registered events, and unregister them.
/// </summary>
/// <param name="eventType">Event type that triggered this call.</param>
/// <param name="addonInfo">Addon that triggered this call.</param>
private void OnAddonFinalize(AddonEvent eventType, AddonArgs addonInfo)
{
// It shouldn't be possible for this event to be anything other than PreFinalize.
if (eventType != AddonEvent.PreFinalize) return;
foreach (var pluginList in this.pluginEventControllers)
{
pluginList.Value.RemoveForAddon(addonInfo.AddonName);
}
}
private nint UpdateCursorDetour(RaptureAtkModule* module)
{
try
{
var atkStage = AtkStage.GetSingleton();
if (this.cursorOverride is not null && atkStage is not null)
{
var cursor = (AddonCursorType)atkStage->AtkCursor.Type;
if (cursor != this.cursorOverride)
{
AtkStage.GetSingleton()->AtkCursor.SetCursorType((AtkCursor.CursorType)this.cursorOverride, 1);
}
return nint.Zero;
}
}
catch (Exception e)
{
Log.Error(e, "Exception in UpdateCursorDetour.");
}
return this.onUpdateCursor!.Original(module);
}
}
/// <summary>
/// Plugin-scoped version of a AddonEventManager service.
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.ScopedService]
#pragma warning disable SA1015
[ResolveVia<IAddonEventManager>]
#pragma warning restore SA1015
internal class AddonEventManagerPluginScoped : IInternalDisposableService, IAddonEventManager
{
[ServiceManager.ServiceDependency]
private readonly AddonEventManager eventManagerService = Service<AddonEventManager>.Get();
private readonly LocalPlugin plugin;
private bool isForcingCursor;
/// <summary>
/// Initializes a new instance of the <see cref="AddonEventManagerPluginScoped"/> class.
/// </summary>
/// <param name="plugin">Plugin info for the plugin that requested this service.</param>
public AddonEventManagerPluginScoped(LocalPlugin plugin)
{
this.plugin = plugin;
this.eventManagerService.AddPluginEventController(plugin.Manifest.WorkingPluginId);
}
/// <inheritdoc/>
void IInternalDisposableService.DisposeService()
{
// if multiple plugins force cursors and dispose without un-forcing them then all forces will be cleared.
if (this.isForcingCursor)
{
this.eventManagerService.ResetCursor();
}
this.eventManagerService.RemovePluginEventController(this.plugin.Manifest.WorkingPluginId);
}
/// <inheritdoc/>
public IAddonEventHandle? AddEvent(IntPtr atkUnitBase, IntPtr atkResNode, AddonEventType eventType, IAddonEventManager.AddonEventHandler eventHandler)
=> this.eventManagerService.AddEvent(this.plugin.Manifest.WorkingPluginId, atkUnitBase, atkResNode, eventType, eventHandler);
/// <inheritdoc/>
public void RemoveEvent(IAddonEventHandle eventHandle)
=> this.eventManagerService.RemoveEvent(this.plugin.Manifest.WorkingPluginId, eventHandle);
/// <inheritdoc/>
public void SetCursor(AddonCursorType cursor)
{
this.isForcingCursor = true;
this.eventManagerService.SetCursor(cursor);
}
/// <inheritdoc/>
public void ResetCursor()
{
this.isForcingCursor = false;
this.eventManagerService.ResetCursor();
}
}

View file

@ -0,0 +1,21 @@
namespace Dalamud.Game.Addon.Events;
/// <summary>
/// AddonEventManager memory address resolver.
/// </summary>
internal class AddonEventManagerAddressResolver : BaseAddressResolver
{
/// <summary>
/// Gets the address of the AtkModule UpdateCursor method.
/// </summary>
public nint UpdateCursor { get; private set; }
/// <summary>
/// Scan for and setup any configured address pointers.
/// </summary>
/// <param name="scanner">The signature scanner to facilitate setup.</param>
protected override void Setup64Bit(ISigScanner scanner)
{
this.UpdateCursor = scanner.ScanText("48 89 74 24 ?? 48 89 7C 24 ?? 41 56 48 83 EC 20 4C 8B F1 E8 ?? ?? ?? ?? 49 8B CE");
}
}

View file

@ -0,0 +1,158 @@
namespace Dalamud.Game.Addon.Events;
/// <summary>
/// Reimplementation of AtkEventType.
/// </summary>
public enum AddonEventType : byte
{
/// <summary>
/// Mouse Down.
/// </summary>
MouseDown = 3,
/// <summary>
/// Mouse Up.
/// </summary>
MouseUp = 4,
/// <summary>
/// Mouse Move.
/// </summary>
MouseMove = 5,
/// <summary>
/// Mouse Over.
/// </summary>
MouseOver = 6,
/// <summary>
/// Mouse Out.
/// </summary>
MouseOut = 7,
/// <summary>
/// Mouse Click.
/// </summary>
MouseClick = 9,
/// <summary>
/// Input Received.
/// </summary>
InputReceived = 12,
/// <summary>
/// Focus Start.
/// </summary>
FocusStart = 18,
/// <summary>
/// Focus Stop.
/// </summary>
FocusStop = 19,
/// <summary>
/// Button Press, sent on MouseDown on Button.
/// </summary>
ButtonPress = 23,
/// <summary>
/// Button Release, sent on MouseUp and MouseOut.
/// </summary>
ButtonRelease = 24,
/// <summary>
/// Button Click, sent on MouseUp and MouseClick on button.
/// </summary>
ButtonClick = 25,
/// <summary>
/// List Item RollOver.
/// </summary>
ListItemRollOver = 33,
/// <summary>
/// List Item Roll Out.
/// </summary>
ListItemRollOut = 34,
/// <summary>
/// List Item Toggle.
/// </summary>
ListItemToggle = 35,
/// <summary>
/// Drag Drop Begin.
/// Sent on MouseDown over a draggable icon (will NOT send for a locked icon).
/// </summary>
DragDropBegin = 47,
/// <summary>
/// Drag Drop Insert.
/// Sent when dropping an icon into a hotbar/inventory slot or similar.
/// </summary>
DragDropInsert = 50,
/// <summary>
/// Drag Drop Roll Over.
/// </summary>
DragDropRollOver = 52,
/// <summary>
/// Drag Drop Roll Out.
/// </summary>
DragDropRollOut = 53,
/// <summary>
/// Drag Drop Discard.
/// Sent when dropping an icon into empty screenspace, eg to remove an action from a hotBar.
/// </summary>
DragDropDiscard = 54,
/// <summary>
/// Drag Drop Unknown.
/// </summary>
[Obsolete("Use DragDropDiscard")]
DragDropUnk54 = 54,
/// <summary>
/// Drag Drop Cancel.
/// Sent on MouseUp if the cursor has not moved since DragDropBegin, OR on MouseDown over a locked icon.
/// </summary>
DragDropCancel = 55,
/// <summary>
/// Drag Drop Unknown.
/// </summary>
[Obsolete("Use DragDropCancel")]
DragDropUnk55 = 55,
/// <summary>
/// Icon Text Roll Over.
/// </summary>
IconTextRollOver = 56,
/// <summary>
/// Icon Text Roll Out.
/// </summary>
IconTextRollOut = 57,
/// <summary>
/// Icon Text Click.
/// </summary>
IconTextClick = 58,
/// <summary>
/// Window Roll Over.
/// </summary>
WindowRollOver = 67,
/// <summary>
/// Window Roll Out.
/// </summary>
WindowRollOut = 68,
/// <summary>
/// Window Change Scale.
/// </summary>
WindowChangeScale = 69,
}

View file

@ -0,0 +1,27 @@
namespace Dalamud.Game.Addon.Events;
/// <summary>
/// Interface representing the data used for managing AddonEvents.
/// </summary>
public interface IAddonEventHandle
{
/// <summary>
/// Gets the param key associated with this event.
/// </summary>
public uint ParamKey { get; init; }
/// <summary>
/// Gets the name of the addon that this event was attached to.
/// </summary>
public string AddonName { get; init; }
/// <summary>
/// Gets the event type associated with this handle.
/// </summary>
public AddonEventType EventType { get; init; }
/// <summary>
/// Gets the unique ID for this handle.
/// </summary>
public Guid EventGuid { get; init; }
}

View file

@ -0,0 +1,201 @@
using System.Collections.Generic;
using System.Linq;
using Dalamud.Game.Gui;
using Dalamud.Logging.Internal;
using Dalamud.Memory;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace Dalamud.Game.Addon.Events;
/// <summary>
/// Class to manage creating and cleaning up events per-plugin.
/// </summary>
internal unsafe class PluginEventController : IDisposable
{
private static readonly ModuleLog Log = new("AddonEventManager");
/// <summary>
/// Initializes a new instance of the <see cref="PluginEventController"/> class.
/// </summary>
public PluginEventController()
{
this.EventListener = new AddonEventListener(this.PluginEventListHandler);
}
private AddonEventListener EventListener { get; init; }
private List<AddonEventEntry> Events { get; } = new();
/// <summary>
/// Adds a tracked event.
/// </summary>
/// <param name="atkUnitBase">The Parent addon for the event.</param>
/// <param name="atkResNode">The Node for the event.</param>
/// <param name="atkEventType">The Event Type.</param>
/// <param name="handler">The delegate to call when invoking this event.</param>
/// <returns>IAddonEventHandle used to remove the event.</returns>
public IAddonEventHandle AddEvent(nint atkUnitBase, nint atkResNode, AddonEventType atkEventType, IAddonEventManager.AddonEventHandler handler)
{
var node = (AtkResNode*)atkResNode;
var addon = (AtkUnitBase*)atkUnitBase;
var eventType = (AtkEventType)atkEventType;
var eventId = this.GetNextParamKey();
var eventGuid = Guid.NewGuid();
var eventHandle = new AddonEventHandle
{
AddonName = MemoryHelper.ReadStringNullTerminated((nint)addon->Name),
ParamKey = eventId,
EventType = atkEventType,
EventGuid = eventGuid,
};
var eventEntry = new AddonEventEntry
{
Addon = atkUnitBase,
Handler = handler,
Node = atkResNode,
EventType = atkEventType,
ParamKey = eventId,
Handle = eventHandle,
};
Log.Verbose($"Adding Event. {eventEntry.LogString}");
this.EventListener.RegisterEvent(addon, node, eventType, eventId);
this.Events.Add(eventEntry);
return eventHandle;
}
/// <summary>
/// Removes a tracked event, also attempts to un-attach the event from native.
/// </summary>
/// <param name="handle">Unique ID of the event to remove.</param>
public void RemoveEvent(IAddonEventHandle handle)
{
if (this.Events.FirstOrDefault(registeredEvent => registeredEvent.Handle == handle) is not { } targetEvent) return;
Log.Verbose($"Removing Event. {targetEvent.LogString}");
this.TryRemoveEventFromNative(targetEvent);
this.Events.Remove(targetEvent);
}
/// <summary>
/// Removes all events attached to the specified addon.
/// </summary>
/// <param name="addonName">Addon name to remove events from.</param>
public void RemoveForAddon(string addonName)
{
if (this.Events.Where(entry => entry.AddonName == addonName).ToList() is { Count: not 0 } events)
{
Log.Verbose($"Addon: {addonName} is Finalizing, removing {events.Count} events.");
foreach (var registeredEvent in events)
{
this.RemoveEvent(registeredEvent.Handle);
}
}
}
/// <inheritdoc/>
public void Dispose()
{
foreach (var registeredEvent in this.Events.ToList())
{
this.RemoveEvent(registeredEvent.Handle);
}
this.EventListener.Dispose();
}
private uint GetNextParamKey()
{
for (var i = 0u; i < uint.MaxValue; ++i)
{
if (this.Events.All(registeredEvent => registeredEvent.ParamKey != i)) return i;
}
throw new OverflowException($"uint.MaxValue number of ParamKeys used for this event controller.");
}
/// <summary>
/// Attempts to remove a tracked event from native UI.
/// This method performs several safety checks to only remove events from a still active addon.
/// If any of these checks fail, it likely means the native UI already cleaned up the event, and we don't have to worry about them.
/// </summary>
/// <param name="eventEntry">Event entry to remove.</param>
private void TryRemoveEventFromNative(AddonEventEntry eventEntry)
{
// Is the eventEntry addon valid?
if (eventEntry.AddonName is AddonEventEntry.InvalidAddonName) return;
// Is an addon with the same name active?
var currentAddonPointer = Service<GameGui>.Get().GetAddonByName(eventEntry.AddonName);
if (currentAddonPointer == nint.Zero) return;
// Is our stored addon pointer the same as the active addon pointer?
if (currentAddonPointer != eventEntry.Addon) return;
// Does this addon contain the node this event is for? (by address)
var atkUnitBase = (AtkUnitBase*)currentAddonPointer;
var nodeFound = false;
foreach (var index in Enumerable.Range(0, atkUnitBase->UldManager.NodeListCount))
{
var node = atkUnitBase->UldManager.NodeList[index];
// If this node matches our node, then we know our node is still valid.
if (node is not null && (nint)node == eventEntry.Node)
{
nodeFound = true;
}
}
// If we didn't find the node, we can't remove the event.
if (!nodeFound) return;
// Does the node have a registered event matching the parameters we have?
var atkResNode = (AtkResNode*)eventEntry.Node;
var eventType = (AtkEventType)eventEntry.EventType;
var currentEvent = atkResNode->AtkEventManager.Event;
var eventFound = false;
while (currentEvent is not null)
{
var paramKeyMatches = currentEvent->Param == eventEntry.ParamKey;
var eventListenerAddressMatches = (nint)currentEvent->Listener == this.EventListener.Address;
var eventTypeMatches = currentEvent->Type == eventType;
if (paramKeyMatches && eventListenerAddressMatches && eventTypeMatches)
{
eventFound = true;
break;
}
// Move to the next event.
currentEvent = currentEvent->NextEvent;
}
// If we didn't find the event, we can't remove the event.
if (!eventFound) return;
// We have a valid addon, valid node, valid event, and valid key.
this.EventListener.UnregisterEvent(atkResNode, eventType, eventEntry.ParamKey);
}
private void PluginEventListHandler(AtkEventListener* self, AtkEventType eventType, uint eventParam, AtkEvent* eventData, IntPtr unknown)
{
try
{
if (eventData is null) return;
if (this.Events.FirstOrDefault(handler => handler.ParamKey == eventParam) is not { } eventInfo) return;
// We stored the AtkUnitBase* in EventData->Node, and EventData->Target contains the node that triggered the event.
eventInfo.Handler.Invoke((AddonEventType)eventType, (nint)eventData->Node, (nint)eventData->Target);
}
catch (Exception exception)
{
Log.Error(exception, "Exception in PluginEventList custom event invoke.");
}
}
}

View file

@ -0,0 +1,85 @@
using Dalamud.Memory;
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
/// <summary>
/// Base class for AddonLifecycle AddonArgTypes.
/// </summary>
public abstract unsafe class AddonArgs
{
/// <summary>
/// Constant string representing the name of an addon that is invalid.
/// </summary>
public const string InvalidAddon = "NullAddon";
private string? addonName;
private IntPtr addon;
/// <summary>
/// Gets the name of the addon this args referrers to.
/// </summary>
public string AddonName => this.GetAddonName();
/// <summary>
/// Gets the pointer to the addons AtkUnitBase.
/// </summary>
public nint Addon
{
get => this.AddonInternal;
init => this.AddonInternal = value;
}
/// <summary>
/// Gets the type of these args.
/// </summary>
public abstract AddonArgsType Type { get; }
/// <summary>
/// Gets or sets the pointer to the addons AtkUnitBase.
/// </summary>
internal nint AddonInternal
{
get => this.addon;
set
{
this.addon = value;
// Note: always clear addonName on updating the addon being pointed.
// Same address may point to a different addon.
this.addonName = null;
}
}
/// <summary>
/// Checks if addon name matches the given span of char.
/// </summary>
/// <param name="name">The name to check.</param>
/// <returns>Whether it is the case.</returns>
internal bool IsAddon(ReadOnlySpan<char> name)
{
if (this.Addon == nint.Zero) return false;
if (name.Length is 0 or > 0x20)
return false;
var addonPointer = (AtkUnitBase*)this.Addon;
if (addonPointer->Name is null) return false;
return MemoryHelper.EqualsZeroTerminatedString(name, (nint)addonPointer->Name, null, 0x20);
}
/// <summary>
/// Helper method for ensuring the name of the addon is valid.
/// </summary>
/// <returns>The name of the addon for this object. <see cref="InvalidAddon"/> when invalid.</returns>
private string GetAddonName()
{
if (this.Addon == nint.Zero) return InvalidAddon;
var addonPointer = (AtkUnitBase*)this.Addon;
if (addonPointer->Name is null) return InvalidAddon;
return this.addonName ??= MemoryHelper.ReadString((nint)addonPointer->Name, 0x20);
}
}

View file

@ -0,0 +1,24 @@
namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
/// <summary>
/// Addon argument data for Draw events.
/// </summary>
public class AddonDrawArgs : AddonArgs, ICloneable
{
/// <summary>
/// Initializes a new instance of the <see cref="AddonDrawArgs"/> class.
/// </summary>
[Obsolete("Not intended for public construction.", false)]
public AddonDrawArgs()
{
}
/// <inheritdoc/>
public override AddonArgsType Type => AddonArgsType.Draw;
/// <inheritdoc cref="ICloneable.Clone"/>
public AddonDrawArgs Clone() => (AddonDrawArgs)this.MemberwiseClone();
/// <inheritdoc cref="Clone"/>
object ICloneable.Clone() => this.Clone();
}

View file

@ -0,0 +1,24 @@
namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
/// <summary>
/// Addon argument data for ReceiveEvent events.
/// </summary>
public class AddonFinalizeArgs : AddonArgs, ICloneable
{
/// <summary>
/// Initializes a new instance of the <see cref="AddonFinalizeArgs"/> class.
/// </summary>
[Obsolete("Not intended for public construction.", false)]
public AddonFinalizeArgs()
{
}
/// <inheritdoc/>
public override AddonArgsType Type => AddonArgsType.Finalize;
/// <inheritdoc cref="ICloneable.Clone"/>
public AddonFinalizeArgs Clone() => (AddonFinalizeArgs)this.MemberwiseClone();
/// <inheritdoc cref="Clone"/>
object ICloneable.Clone() => this.Clone();
}

View file

@ -0,0 +1,44 @@
namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
/// <summary>
/// Addon argument data for ReceiveEvent events.
/// </summary>
public class AddonReceiveEventArgs : AddonArgs, ICloneable
{
/// <summary>
/// Initializes a new instance of the <see cref="AddonReceiveEventArgs"/> class.
/// </summary>
[Obsolete("Not intended for public construction.", false)]
public AddonReceiveEventArgs()
{
}
/// <inheritdoc/>
public override AddonArgsType Type => AddonArgsType.ReceiveEvent;
/// <summary>
/// Gets or sets the AtkEventType for this event message.
/// </summary>
public byte AtkEventType { get; set; }
/// <summary>
/// Gets or sets the event id for this event message.
/// </summary>
public int EventParam { get; set; }
/// <summary>
/// Gets or sets the pointer to an AtkEvent for this event message.
/// </summary>
public nint AtkEvent { get; set; }
/// <summary>
/// Gets or sets the pointer to a block of data for this event message.
/// </summary>
public nint Data { get; set; }
/// <inheritdoc cref="ICloneable.Clone"/>
public AddonReceiveEventArgs Clone() => (AddonReceiveEventArgs)this.MemberwiseClone();
/// <inheritdoc cref="Clone"/>
object ICloneable.Clone() => this.Clone();
}

View file

@ -0,0 +1,41 @@
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
/// <summary>
/// Addon argument data for Refresh events.
/// </summary>
public class AddonRefreshArgs : AddonArgs, ICloneable
{
/// <summary>
/// Initializes a new instance of the <see cref="AddonRefreshArgs"/> class.
/// </summary>
[Obsolete("Not intended for public construction.", false)]
public AddonRefreshArgs()
{
}
/// <inheritdoc/>
public override AddonArgsType Type => AddonArgsType.Refresh;
/// <summary>
/// Gets or sets the number of AtkValues.
/// </summary>
public uint AtkValueCount { get; set; }
/// <summary>
/// Gets or sets the address of the AtkValue array.
/// </summary>
public nint AtkValues { get; set; }
/// <summary>
/// Gets the AtkValues in the form of a span.
/// </summary>
public unsafe Span<AtkValue> AtkValueSpan => new(this.AtkValues.ToPointer(), (int)this.AtkValueCount);
/// <inheritdoc cref="ICloneable.Clone"/>
public AddonRefreshArgs Clone() => (AddonRefreshArgs)this.MemberwiseClone();
/// <inheritdoc cref="Clone"/>
object ICloneable.Clone() => this.Clone();
}

View file

@ -0,0 +1,34 @@
namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
/// <summary>
/// Addon argument data for OnRequestedUpdate events.
/// </summary>
public class AddonRequestedUpdateArgs : AddonArgs, ICloneable
{
/// <summary>
/// Initializes a new instance of the <see cref="AddonRequestedUpdateArgs"/> class.
/// </summary>
[Obsolete("Not intended for public construction.", false)]
public AddonRequestedUpdateArgs()
{
}
/// <inheritdoc/>
public override AddonArgsType Type => AddonArgsType.RequestedUpdate;
/// <summary>
/// Gets or sets the NumberArrayData** for this event.
/// </summary>
public nint NumberArrayData { get; set; }
/// <summary>
/// Gets or sets the StringArrayData** for this event.
/// </summary>
public nint StringArrayData { get; set; }
/// <inheritdoc cref="ICloneable.Clone"/>
public AddonRequestedUpdateArgs Clone() => (AddonRequestedUpdateArgs)this.MemberwiseClone();
/// <inheritdoc cref="Clone"/>
object ICloneable.Clone() => this.Clone();
}

View file

@ -0,0 +1,41 @@
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
/// <summary>
/// Addon argument data for Setup events.
/// </summary>
public class AddonSetupArgs : AddonArgs, ICloneable
{
/// <summary>
/// Initializes a new instance of the <see cref="AddonSetupArgs"/> class.
/// </summary>
[Obsolete("Not intended for public construction.", false)]
public AddonSetupArgs()
{
}
/// <inheritdoc/>
public override AddonArgsType Type => AddonArgsType.Setup;
/// <summary>
/// Gets or sets the number of AtkValues.
/// </summary>
public uint AtkValueCount { get; set; }
/// <summary>
/// Gets or sets the address of the AtkValue array.
/// </summary>
public nint AtkValues { get; set; }
/// <summary>
/// Gets the AtkValues in the form of a span.
/// </summary>
public unsafe Span<AtkValue> AtkValueSpan => new(this.AtkValues.ToPointer(), (int)this.AtkValueCount);
/// <inheritdoc cref="ICloneable.Clone"/>
public AddonSetupArgs Clone() => (AddonSetupArgs)this.MemberwiseClone();
/// <inheritdoc cref="Clone"/>
object ICloneable.Clone() => this.Clone();
}

View file

@ -0,0 +1,38 @@
namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
/// <summary>
/// Addon argument data for Update events.
/// </summary>
public class AddonUpdateArgs : AddonArgs, ICloneable
{
/// <summary>
/// Initializes a new instance of the <see cref="AddonUpdateArgs"/> class.
/// </summary>
[Obsolete("Not intended for public construction.", false)]
public AddonUpdateArgs()
{
}
/// <inheritdoc/>
public override AddonArgsType Type => AddonArgsType.Update;
/// <summary>
/// Gets the time since the last update.
/// </summary>
public float TimeDelta
{
get => this.TimeDeltaInternal;
init => this.TimeDeltaInternal = value;
}
/// <summary>
/// Gets or sets the time since the last update.
/// </summary>
internal float TimeDeltaInternal { get; set; }
/// <inheritdoc cref="ICloneable.Clone"/>
public AddonUpdateArgs Clone() => (AddonUpdateArgs)this.MemberwiseClone();
/// <inheritdoc cref="Clone"/>
object ICloneable.Clone() => this.Clone();
}

View file

@ -0,0 +1,42 @@
namespace Dalamud.Game.Addon.Lifecycle;
/// <summary>
/// Enumeration for available AddonLifecycle arg data.
/// </summary>
public enum AddonArgsType
{
/// <summary>
/// Contains argument data for Setup.
/// </summary>
Setup,
/// <summary>
/// Contains argument data for Update.
/// </summary>
Update,
/// <summary>
/// Contains argument data for Draw.
/// </summary>
Draw,
/// <summary>
/// Contains argument data for Finalize.
/// </summary>
Finalize,
/// <summary>
/// Contains argument data for RequestedUpdate.
/// </summary>
RequestedUpdate,
/// <summary>
/// Contains argument data for Refresh.
/// </summary>
Refresh,
/// <summary>
/// Contains argument data for ReceiveEvent.
/// </summary>
ReceiveEvent,
}

View file

@ -0,0 +1,72 @@
namespace Dalamud.Game.Addon.Lifecycle;
/// <summary>
/// Enumeration for available AddonLifecycle events.
/// </summary>
public enum AddonEvent
{
/// <summary>
/// Event that is fired before an addon begins it's setup process.
/// </summary>
PreSetup,
/// <summary>
/// Event that is fired after an addon has completed it's setup process.
/// </summary>
PostSetup,
/// <summary>
/// Event that is fired before an addon begins update.
/// </summary>
PreUpdate,
/// <summary>
/// Event that is fired after an addon has completed update.
/// </summary>
PostUpdate,
/// <summary>
/// Event that is fired before an addon begins draw.
/// </summary>
PreDraw,
/// <summary>
/// Event that is fired after an addon has completed draw.
/// </summary>
PostDraw,
/// <summary>
/// Event that is fired before an addon is finalized.
/// </summary>
PreFinalize,
/// <summary>
/// Event that is fired before an addon begins a requested update.
/// </summary>
PreRequestedUpdate,
/// <summary>
/// Event that is fired after an addon finishes a requested update.
/// </summary>
PostRequestedUpdate,
/// <summary>
/// Event that is fired before an addon begins a refresh.
/// </summary>
PreRefresh,
/// <summary>
/// Event that is fired after an addon has finished a refresh.
/// </summary>
PostRefresh,
/// <summary>
/// Event that is fired before an addon begins processing an event.
/// </summary>
PreReceiveEvent,
/// <summary>
/// Event that is fired after an addon has processed an event.
/// </summary>
PostReceiveEvent,
}

View file

@ -0,0 +1,468 @@
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Hooking;
using Dalamud.Hooking.Internal;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Logging.Internal;
using Dalamud.Memory;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace Dalamud.Game.Addon.Lifecycle;
/// <summary>
/// This class provides events for in-game addon lifecycles.
/// </summary>
[InterfaceVersion("1.0")]
[ServiceManager.EarlyLoadedService]
internal unsafe class AddonLifecycle : IInternalDisposableService
{
private static readonly ModuleLog Log = new("AddonLifecycle");
[ServiceManager.ServiceDependency]
private readonly Framework framework = Service<Framework>.Get();
[ServiceManager.ServiceDependency]
private readonly AddonLifecyclePooledArgs argsPool = Service<AddonLifecyclePooledArgs>.Get();
private readonly nint disallowedReceiveEventAddress;
private readonly AddonLifecycleAddressResolver address;
private readonly CallHook<AddonSetupDelegate> onAddonSetupHook;
private readonly CallHook<AddonSetupDelegate> onAddonSetup2Hook;
private readonly Hook<AddonFinalizeDelegate> onAddonFinalizeHook;
private readonly CallHook<AddonDrawDelegate> onAddonDrawHook;
private readonly CallHook<AddonUpdateDelegate> onAddonUpdateHook;
private readonly Hook<AddonOnRefreshDelegate> onAddonRefreshHook;
private readonly CallHook<AddonOnRequestedUpdateDelegate> onAddonRequestedUpdateHook;
[ServiceManager.ServiceConstructor]
private AddonLifecycle(TargetSigScanner sigScanner)
{
this.address = new AddonLifecycleAddressResolver();
this.address.Setup(sigScanner);
// We want value of the function pointer at vFunc[2]
this.disallowedReceiveEventAddress = ((nint*)this.address.AtkEventListener)![2];
this.onAddonSetupHook = new CallHook<AddonSetupDelegate>(this.address.AddonSetup, this.OnAddonSetup);
this.onAddonSetup2Hook = new CallHook<AddonSetupDelegate>(this.address.AddonSetup2, this.OnAddonSetup);
this.onAddonFinalizeHook = Hook<AddonFinalizeDelegate>.FromAddress(this.address.AddonFinalize, this.OnAddonFinalize);
this.onAddonDrawHook = new CallHook<AddonDrawDelegate>(this.address.AddonDraw, this.OnAddonDraw);
this.onAddonUpdateHook = new CallHook<AddonUpdateDelegate>(this.address.AddonUpdate, this.OnAddonUpdate);
this.onAddonRefreshHook = Hook<AddonOnRefreshDelegate>.FromAddress(this.address.AddonOnRefresh, this.OnAddonRefresh);
this.onAddonRequestedUpdateHook = new CallHook<AddonOnRequestedUpdateDelegate>(this.address.AddonOnRequestedUpdate, this.OnRequestedUpdate);
this.onAddonSetupHook.Enable();
this.onAddonSetup2Hook.Enable();
this.onAddonFinalizeHook.Enable();
this.onAddonDrawHook.Enable();
this.onAddonUpdateHook.Enable();
this.onAddonRefreshHook.Enable();
this.onAddonRequestedUpdateHook.Enable();
}
private delegate void AddonSetupDelegate(AtkUnitBase* addon, uint valueCount, AtkValue* values);
private delegate void AddonFinalizeDelegate(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase);
private delegate void AddonDrawDelegate(AtkUnitBase* addon);
private delegate void AddonUpdateDelegate(AtkUnitBase* addon, float delta);
private delegate void AddonOnRequestedUpdateDelegate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData);
private delegate byte AddonOnRefreshDelegate(AtkUnitManager* unitManager, AtkUnitBase* addon, uint valueCount, AtkValue* values);
/// <summary>
/// Gets a list of all AddonLifecycle ReceiveEvent Listener Hooks.
/// </summary>
internal List<AddonLifecycleReceiveEventListener> ReceiveEventListeners { get; } = new();
/// <summary>
/// Gets a list of all AddonLifecycle Event Listeners.
/// </summary>
internal List<AddonLifecycleEventListener> EventListeners { get; } = new();
/// <inheritdoc/>
void IInternalDisposableService.DisposeService()
{
this.onAddonSetupHook.Dispose();
this.onAddonSetup2Hook.Dispose();
this.onAddonFinalizeHook.Dispose();
this.onAddonDrawHook.Dispose();
this.onAddonUpdateHook.Dispose();
this.onAddonRefreshHook.Dispose();
this.onAddonRequestedUpdateHook.Dispose();
foreach (var receiveEventListener in this.ReceiveEventListeners)
{
receiveEventListener.Dispose();
}
}
/// <summary>
/// Register a listener for the target event and addon.
/// </summary>
/// <param name="listener">The listener to register.</param>
internal void RegisterListener(AddonLifecycleEventListener listener)
{
this.framework.RunOnTick(() =>
{
this.EventListeners.Add(listener);
// If we want receive event messages have an already active addon, enable the receive event hook.
// If the addon isn't active yet, we'll grab the hook when it sets up.
if (listener is { EventType: AddonEvent.PreReceiveEvent or AddonEvent.PostReceiveEvent })
{
if (this.ReceiveEventListeners.FirstOrDefault(listeners => listeners.AddonNames.Contains(listener.AddonName)) is { } receiveEventListener)
{
receiveEventListener.Hook?.Enable();
}
}
});
}
/// <summary>
/// Unregisters the listener from events.
/// </summary>
/// <param name="listener">The listener to unregister.</param>
internal void UnregisterListener(AddonLifecycleEventListener listener)
{
this.framework.RunOnTick(() =>
{
this.EventListeners.Remove(listener);
// If we are disabling an ReceiveEvent listener, check if we should disable the hook.
if (listener is { EventType: AddonEvent.PreReceiveEvent or AddonEvent.PostReceiveEvent })
{
// Get the ReceiveEvent Listener for this addon
if (this.ReceiveEventListeners.FirstOrDefault(listeners => listeners.AddonNames.Contains(listener.AddonName)) is { } receiveEventListener)
{
// If there are no other listeners listening for this event, disable the hook.
if (!this.EventListeners.Any(listeners => listeners.AddonName.Contains(listener.AddonName) && listener.EventType is AddonEvent.PreReceiveEvent or AddonEvent.PostReceiveEvent))
{
receiveEventListener.Hook?.Disable();
}
}
}
});
}
/// <summary>
/// Invoke listeners for the specified event type.
/// </summary>
/// <param name="eventType">Event Type.</param>
/// <param name="args">AddonArgs.</param>
/// <param name="blame">What to blame on errors.</param>
internal void InvokeListenersSafely(AddonEvent eventType, AddonArgs args, [CallerMemberName] string blame = "")
{
// Do not use linq; this is a high-traffic function, and more heap allocations avoided, the better.
foreach (var listener in this.EventListeners)
{
if (listener.EventType != eventType)
continue;
// Match on string.empty for listeners that want events for all addons.
if (!string.IsNullOrWhiteSpace(listener.AddonName) && !args.IsAddon(listener.AddonName))
continue;
try
{
listener.FunctionDelegate.Invoke(eventType, args);
}
catch (Exception e)
{
Log.Error(e, $"Exception in {blame} during {eventType} invoke.");
}
}
}
private void RegisterReceiveEventHook(AtkUnitBase* addon)
{
// Hook the addon's ReceiveEvent function here, but only enable the hook if we have an active listener.
// Disallows hooking the core internal event handler.
var addonName = MemoryHelper.ReadStringNullTerminated((nint)addon->Name);
var receiveEventAddress = (nint)addon->VTable->ReceiveEvent;
if (receiveEventAddress != this.disallowedReceiveEventAddress)
{
// If we have a ReceiveEvent listener already made for this hook address, add this addon's name to that handler.
if (this.ReceiveEventListeners.FirstOrDefault(listener => listener.HookAddress == receiveEventAddress) is { } existingListener)
{
if (!existingListener.AddonNames.Contains(addonName))
{
existingListener.AddonNames.Add(addonName);
}
}
// Else, we have an addon that we don't have the ReceiveEvent for yet, make it.
else
{
this.ReceiveEventListeners.Add(new AddonLifecycleReceiveEventListener(this, addonName, receiveEventAddress));
}
// If we have an active listener for this addon already, we need to activate this hook.
if (this.EventListeners.Any(listener => (listener.EventType is AddonEvent.PostReceiveEvent or AddonEvent.PreReceiveEvent) && listener.AddonName == addonName))
{
if (this.ReceiveEventListeners.FirstOrDefault(listener => listener.AddonNames.Contains(addonName)) is { } receiveEventListener)
{
receiveEventListener.Hook?.Enable();
}
}
}
}
private void UnregisterReceiveEventHook(string addonName)
{
// Remove this addons ReceiveEvent Registration
if (this.ReceiveEventListeners.FirstOrDefault(listener => listener.AddonNames.Contains(addonName)) is { } eventListener)
{
eventListener.AddonNames.Remove(addonName);
// If there are no more listeners let's remove and dispose.
if (eventListener.AddonNames.Count is 0)
{
this.ReceiveEventListeners.Remove(eventListener);
eventListener.Dispose();
}
}
}
private void OnAddonSetup(AtkUnitBase* addon, uint valueCount, AtkValue* values)
{
try
{
this.RegisterReceiveEventHook(addon);
}
catch (Exception e)
{
Log.Error(e, "Exception in OnAddonSetup ReceiveEvent Registration.");
}
using var returner = this.argsPool.Rent(out AddonSetupArgs arg);
arg.AddonInternal = (nint)addon;
arg.AtkValueCount = valueCount;
arg.AtkValues = (nint)values;
this.InvokeListenersSafely(AddonEvent.PreSetup, arg);
valueCount = arg.AtkValueCount;
values = (AtkValue*)arg.AtkValues;
try
{
addon->OnSetup(valueCount, values);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original AddonSetup. This may be a bug in the game or another plugin hooking this method.");
}
this.InvokeListenersSafely(AddonEvent.PostSetup, arg);
}
private void OnAddonFinalize(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase)
{
try
{
var addonName = MemoryHelper.ReadStringNullTerminated((nint)atkUnitBase[0]->Name);
this.UnregisterReceiveEventHook(addonName);
}
catch (Exception e)
{
Log.Error(e, "Exception in OnAddonFinalize ReceiveEvent Removal.");
}
using var returner = this.argsPool.Rent(out AddonFinalizeArgs arg);
arg.AddonInternal = (nint)atkUnitBase[0];
this.InvokeListenersSafely(AddonEvent.PreFinalize, arg);
try
{
this.onAddonFinalizeHook.Original(unitManager, atkUnitBase);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original AddonFinalize. This may be a bug in the game or another plugin hooking this method.");
}
}
private void OnAddonDraw(AtkUnitBase* addon)
{
using var returner = this.argsPool.Rent(out AddonDrawArgs arg);
arg.AddonInternal = (nint)addon;
this.InvokeListenersSafely(AddonEvent.PreDraw, arg);
try
{
addon->Draw();
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original AddonDraw. This may be a bug in the game or another plugin hooking this method.");
}
this.InvokeListenersSafely(AddonEvent.PostDraw, arg);
}
private void OnAddonUpdate(AtkUnitBase* addon, float delta)
{
using var returner = this.argsPool.Rent(out AddonUpdateArgs arg);
arg.AddonInternal = (nint)addon;
arg.TimeDeltaInternal = delta;
this.InvokeListenersSafely(AddonEvent.PreUpdate, arg);
try
{
addon->Update(delta);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original AddonUpdate. This may be a bug in the game or another plugin hooking this method.");
}
this.InvokeListenersSafely(AddonEvent.PostUpdate, arg);
}
private byte OnAddonRefresh(AtkUnitManager* atkUnitManager, AtkUnitBase* addon, uint valueCount, AtkValue* values)
{
byte result = 0;
using var returner = this.argsPool.Rent(out AddonRefreshArgs arg);
arg.AddonInternal = (nint)addon;
arg.AtkValueCount = valueCount;
arg.AtkValues = (nint)values;
this.InvokeListenersSafely(AddonEvent.PreRefresh, arg);
valueCount = arg.AtkValueCount;
values = (AtkValue*)arg.AtkValues;
try
{
result = this.onAddonRefreshHook.Original(atkUnitManager, addon, valueCount, values);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original AddonRefresh. This may be a bug in the game or another plugin hooking this method.");
}
this.InvokeListenersSafely(AddonEvent.PostRefresh, arg);
return result;
}
private void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData)
{
using var returner = this.argsPool.Rent(out AddonRequestedUpdateArgs arg);
arg.AddonInternal = (nint)addon;
arg.NumberArrayData = (nint)numberArrayData;
arg.StringArrayData = (nint)stringArrayData;
this.InvokeListenersSafely(AddonEvent.PreRequestedUpdate, arg);
numberArrayData = (NumberArrayData**)arg.NumberArrayData;
stringArrayData = (StringArrayData**)arg.StringArrayData;
try
{
addon->OnUpdate(numberArrayData, stringArrayData);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original AddonRequestedUpdate. This may be a bug in the game or another plugin hooking this method.");
}
this.InvokeListenersSafely(AddonEvent.PostRequestedUpdate, arg);
}
}
/// <summary>
/// Plugin-scoped version of a AddonLifecycle service.
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.ScopedService]
#pragma warning disable SA1015
[ResolveVia<IAddonLifecycle>]
#pragma warning restore SA1015
internal class AddonLifecyclePluginScoped : IInternalDisposableService, IAddonLifecycle
{
[ServiceManager.ServiceDependency]
private readonly AddonLifecycle addonLifecycleService = Service<AddonLifecycle>.Get();
private readonly List<AddonLifecycleEventListener> eventListeners = new();
/// <inheritdoc/>
void IInternalDisposableService.DisposeService()
{
foreach (var listener in this.eventListeners)
{
this.addonLifecycleService.UnregisterListener(listener);
}
}
/// <inheritdoc/>
public void RegisterListener(AddonEvent eventType, IEnumerable<string> addonNames, IAddonLifecycle.AddonEventDelegate handler)
{
foreach (var addonName in addonNames)
{
this.RegisterListener(eventType, addonName, handler);
}
}
/// <inheritdoc/>
public void RegisterListener(AddonEvent eventType, string addonName, IAddonLifecycle.AddonEventDelegate handler)
{
var listener = new AddonLifecycleEventListener(eventType, addonName, handler);
this.eventListeners.Add(listener);
this.addonLifecycleService.RegisterListener(listener);
}
/// <inheritdoc/>
public void RegisterListener(AddonEvent eventType, IAddonLifecycle.AddonEventDelegate handler)
{
this.RegisterListener(eventType, string.Empty, handler);
}
/// <inheritdoc/>
public void UnregisterListener(AddonEvent eventType, IEnumerable<string> addonNames, IAddonLifecycle.AddonEventDelegate? handler = null)
{
foreach (var addonName in addonNames)
{
this.UnregisterListener(eventType, addonName, handler);
}
}
/// <inheritdoc/>
public void UnregisterListener(AddonEvent eventType, string addonName, IAddonLifecycle.AddonEventDelegate? handler = null)
{
this.eventListeners.RemoveAll(entry =>
{
if (entry.EventType != eventType) return false;
if (entry.AddonName != addonName) return false;
if (handler is not null && entry.FunctionDelegate != handler) return false;
this.addonLifecycleService.UnregisterListener(entry);
return true;
});
}
/// <inheritdoc/>
public void UnregisterListener(AddonEvent eventType, IAddonLifecycle.AddonEventDelegate? handler = null)
{
this.UnregisterListener(eventType, string.Empty, handler);
}
/// <inheritdoc/>
public void UnregisterListener(params IAddonLifecycle.AddonEventDelegate[] handlers)
{
foreach (var handler in handlers)
{
this.eventListeners.RemoveAll(entry =>
{
if (entry.FunctionDelegate != handler) return false;
this.addonLifecycleService.UnregisterListener(entry);
return true;
});
}
}
}

View file

@ -0,0 +1,68 @@
namespace Dalamud.Game.Addon.Lifecycle;
/// <summary>
/// AddonLifecycleService memory address resolver.
/// </summary>
internal class AddonLifecycleAddressResolver : BaseAddressResolver
{
/// <summary>
/// Gets the address of the addon setup hook invoked by the AtkUnitManager.
/// There are two callsites for this vFunc, we need to hook both of them to catch both normal UI and special UI cases like dialogue.
/// This is called for a majority of all addon OnSetup's.
/// </summary>
public nint AddonSetup { get; private set; }
/// <summary>
/// Gets the address of the other addon setup hook invoked by the AtkUnitManager.
/// There are two callsites for this vFunc, we need to hook both of them to catch both normal UI and special UI cases like dialogue.
/// This seems to be called rarely for specific addons.
/// </summary>
public nint AddonSetup2 { get; private set; }
/// <summary>
/// Gets the address of the addon finalize hook invoked by the AtkUnitManager.
/// </summary>
public nint AddonFinalize { get; private set; }
/// <summary>
/// Gets the address of the addon draw hook invoked by virtual function call.
/// </summary>
public nint AddonDraw { get; private set; }
/// <summary>
/// Gets the address of the addon update hook invoked by virtual function call.
/// </summary>
public nint AddonUpdate { get; private set; }
/// <summary>
/// Gets the address of the addon onRequestedUpdate hook invoked by virtual function call.
/// </summary>
public nint AddonOnRequestedUpdate { get; private set; }
/// <summary>
/// Gets the address of AtkUnitManager_vf10 which triggers addon onRefresh.
/// </summary>
public nint AddonOnRefresh { get; private set; }
/// <summary>
/// Gets the address of AtkEventListener base vTable.
/// This is used to ensure that we do not hook ReceiveEvents that resolve back to the internal handler.
/// </summary>
public nint AtkEventListener { get; private set; }
/// <summary>
/// Scan for and setup any configured address pointers.
/// </summary>
/// <param name="sig">The signature scanner to facilitate setup.</param>
protected override void Setup64Bit(ISigScanner sig)
{
this.AddonSetup = sig.ScanText("FF 90 ?? ?? ?? ?? 48 8B 93 ?? ?? ?? ?? 80 8B");
this.AddonSetup2 = sig.ScanText("FF 90 ?? ?? ?? ?? 48 8B 03 48 8B CB 80 8B");
this.AddonFinalize = sig.ScanText("E8 ?? ?? ?? ?? 48 8B 7C 24 ?? 41 8B C6");
this.AddonDraw = sig.ScanText("FF 90 ?? ?? ?? ?? 83 EB 01 79 C1");
this.AddonUpdate = sig.ScanText("FF 90 ?? ?? ?? ?? 40 88 AF");
this.AddonOnRequestedUpdate = sig.ScanText("FF 90 98 01 00 00 48 8B 5C 24 30 48 83 C4 20");
this.AddonOnRefresh = sig.ScanText("48 89 5C 24 08 57 48 83 EC 20 41 8B F8 48 8B DA");
this.AtkEventListener = sig.GetStaticAddressFromSig("4C 8D 3D ?? ?? ?? ?? 49 8D 8E");
}
}

View file

@ -0,0 +1,38 @@
using Dalamud.Plugin.Services;
namespace Dalamud.Game.Addon.Lifecycle;
/// <summary>
/// This class is a helper for tracking and invoking listener delegates.
/// </summary>
internal class AddonLifecycleEventListener
{
/// <summary>
/// Initializes a new instance of the <see cref="AddonLifecycleEventListener"/> class.
/// </summary>
/// <param name="eventType">Event type to listen for.</param>
/// <param name="addonName">Addon name to listen for.</param>
/// <param name="functionDelegate">Delegate to invoke.</param>
internal AddonLifecycleEventListener(AddonEvent eventType, string addonName, IAddonLifecycle.AddonEventDelegate functionDelegate)
{
this.EventType = eventType;
this.AddonName = addonName;
this.FunctionDelegate = functionDelegate;
}
/// <summary>
/// Gets the name of the addon this listener is looking for.
/// string.Empty if it wants to be called for any addon.
/// </summary>
public string AddonName { get; init; }
/// <summary>
/// Gets the event type this listener is looking for.
/// </summary>
public AddonEvent EventType { get; init; }
/// <summary>
/// Gets the delegate this listener invokes.
/// </summary>
public IAddonLifecycle.AddonEventDelegate FunctionDelegate { get; init; }
}

View file

@ -0,0 +1,104 @@
using System.Collections.Generic;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Hooking;
using Dalamud.Logging.Internal;
using Dalamud.Memory;
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace Dalamud.Game.Addon.Lifecycle;
/// <summary>
/// This class is a helper for tracking and invoking listener delegates for Addon_OnReceiveEvent.
/// Multiple addons may use the same ReceiveEvent function, this helper makes sure that those addon events are handled properly.
/// </summary>
internal unsafe class AddonLifecycleReceiveEventListener : IDisposable
{
private static readonly ModuleLog Log = new("AddonLifecycle");
[ServiceManager.ServiceDependency]
private readonly AddonLifecyclePooledArgs argsPool = Service<AddonLifecyclePooledArgs>.Get();
/// <summary>
/// Initializes a new instance of the <see cref="AddonLifecycleReceiveEventListener"/> class.
/// </summary>
/// <param name="service">AddonLifecycle service instance.</param>
/// <param name="addonName">Initial Addon Requesting this listener.</param>
/// <param name="receiveEventAddress">Address of Addon's ReceiveEvent function.</param>
internal AddonLifecycleReceiveEventListener(AddonLifecycle service, string addonName, nint receiveEventAddress)
{
this.AddonLifecycle = service;
this.AddonNames = new List<string> { addonName };
this.Hook = Hook<AddonReceiveEventDelegate>.FromAddress(receiveEventAddress, this.OnReceiveEvent);
}
/// <summary>
/// Addon Receive Event Function delegate.
/// </summary>
/// <param name="addon">Addon Pointer.</param>
/// <param name="eventType">Event Type.</param>
/// <param name="eventParam">Unique Event ID.</param>
/// <param name="atkEvent">Event Data.</param>
/// <param name="a5">Unknown.</param>
public delegate void AddonReceiveEventDelegate(AtkUnitBase* addon, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, nint a5);
/// <summary>
/// Gets the list of addons that use this receive event hook.
/// </summary>
public List<string> AddonNames { get; init; }
/// <summary>
/// Gets the address of the registered hook.
/// </summary>
public nint HookAddress => this.Hook?.Address ?? nint.Zero;
/// <summary>
/// Gets the contained hook for these addons.
/// </summary>
public Hook<AddonReceiveEventDelegate>? Hook { get; init; }
/// <summary>
/// Gets or sets the Reference to AddonLifecycle service instance.
/// </summary>
private AddonLifecycle AddonLifecycle { get; set; }
/// <inheritdoc/>
public void Dispose()
{
this.Hook?.Dispose();
}
private void OnReceiveEvent(AtkUnitBase* addon, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, nint data)
{
// Check that we didn't get here through a call to another addons handler.
var addonName = MemoryHelper.ReadString((nint)addon->Name, 0x20);
if (!this.AddonNames.Contains(addonName))
{
this.Hook!.Original(addon, eventType, eventParam, atkEvent, data);
return;
}
using var returner = this.argsPool.Rent(out AddonReceiveEventArgs arg);
arg.AddonInternal = (nint)addon;
arg.AtkEventType = (byte)eventType;
arg.EventParam = eventParam;
arg.AtkEvent = (IntPtr)atkEvent;
arg.Data = data;
this.AddonLifecycle.InvokeListenersSafely(AddonEvent.PreReceiveEvent, arg);
eventType = (AtkEventType)arg.AtkEventType;
eventParam = arg.EventParam;
atkEvent = (AtkEvent*)arg.AtkEvent;
data = arg.Data;
try
{
this.Hook!.Original(addon, eventType, eventParam, atkEvent, data);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original AddonReceiveEvent. This may be a bug in the game or another plugin hooking this method.");
}
this.AddonLifecycle.InvokeListenersSafely(AddonEvent.PostReceiveEvent, arg);
}
}

View file

@ -18,24 +18,15 @@ public abstract class BaseAddressResolver
public static Dictionary<string, List<(string ClassName, IntPtr Address)>> DebugScannedValues { get; } = new(); public static Dictionary<string, List<(string ClassName, IntPtr Address)>> DebugScannedValues { get; } = new();
/// <summary> /// <summary>
/// Gets or sets a value indicating whether the resolver has successfully run <see cref="Setup32Bit(SigScanner)"/> or <see cref="Setup64Bit(SigScanner)"/>. /// Gets or sets a value indicating whether the resolver has successfully run <see cref="Setup32Bit(ISigScanner)"/> or <see cref="Setup64Bit(ISigScanner)"/>.
/// </summary> /// </summary>
protected bool IsResolved { get; set; } protected bool IsResolved { get; set; }
/// <summary>
/// Setup the resolver, calling the appropriate method based on the process architecture,
/// using the default SigScanner.
///
/// For plugins. Not intended to be called from Dalamud Service{T} constructors.
/// </summary>
[UsedImplicitly]
public void Setup() => this.Setup(Service<SigScanner>.Get());
/// <summary> /// <summary>
/// Setup the resolver, calling the appropriate method based on the process architecture. /// Setup the resolver, calling the appropriate method based on the process architecture.
/// </summary> /// </summary>
/// <param name="scanner">The SigScanner instance.</param> /// <param name="scanner">The SigScanner instance.</param>
public void Setup(SigScanner scanner) public void Setup(ISigScanner scanner)
{ {
// Because C# don't allow to call virtual function while in ctor // Because C# don't allow to call virtual function while in ctor
// we have to do this shit :\ // we have to do this shit :\
@ -92,7 +83,7 @@ public abstract class BaseAddressResolver
/// Setup the resolver by finding any necessary memory addresses. /// Setup the resolver by finding any necessary memory addresses.
/// </summary> /// </summary>
/// <param name="scanner">The SigScanner instance.</param> /// <param name="scanner">The SigScanner instance.</param>
protected virtual void Setup32Bit(SigScanner scanner) protected virtual void Setup32Bit(ISigScanner scanner)
{ {
throw new NotSupportedException("32 bit version is not supported."); throw new NotSupportedException("32 bit version is not supported.");
} }
@ -101,7 +92,7 @@ public abstract class BaseAddressResolver
/// Setup the resolver by finding any necessary memory addresses. /// Setup the resolver by finding any necessary memory addresses.
/// </summary> /// </summary>
/// <param name="scanner">The SigScanner instance.</param> /// <param name="scanner">The SigScanner instance.</param>
protected virtual void Setup64Bit(SigScanner scanner) protected virtual void Setup64Bit(ISigScanner scanner)
{ {
throw new NotSupportedException("64 bit version is not supported."); throw new NotSupportedException("64 bit version is not supported.");
} }
@ -110,7 +101,7 @@ public abstract class BaseAddressResolver
/// Setup the resolver by finding any necessary memory addresses. /// Setup the resolver by finding any necessary memory addresses.
/// </summary> /// </summary>
/// <param name="scanner">The SigScanner instance.</param> /// <param name="scanner">The SigScanner instance.</param>
protected virtual void SetupInternal(SigScanner scanner) protected virtual void SetupInternal(ISigScanner scanner)
{ {
// Do nothing // Do nothing
} }

View file

@ -1,8 +1,8 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using CheapLoc; using CheapLoc;
@ -11,24 +11,22 @@ using Dalamud.Game.Gui;
using Dalamud.Game.Text; using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Interface.ImGuiNotification.Internal;
using Dalamud.Interface.Internal; using Dalamud.Interface.Internal;
using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Internal.Notifications;
using Dalamud.Interface.Internal.Windows; using Dalamud.Interface.Internal.Windows;
using Dalamud.IoC; using Dalamud.Interface.Internal.Windows.PluginInstaller;
using Dalamud.IoC.Internal; using Dalamud.Logging.Internal;
using Dalamud.Plugin.Internal; using Dalamud.Plugin.Internal;
using Dalamud.Utility; using Dalamud.Utility;
using Serilog;
namespace Dalamud.Game; namespace Dalamud.Game;
/// <summary> /// <summary>
/// Chat events and public helper functions. /// Chat events and public helper functions.
/// </summary> /// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService] [ServiceManager.BlockingEarlyLoadedService]
public class ChatHandlers : IServiceType internal class ChatHandlers : IServiceType
{ {
// private static readonly Dictionary<string, string> UnicodeToDiscordEmojiDict = new() // private static readonly Dictionary<string, string> UnicodeToDiscordEmojiDict = new()
// { // {
@ -64,6 +62,8 @@ public class ChatHandlers : IServiceType
// { XivChatType.Echo, Color.Gray }, // { XivChatType.Echo, Color.Gray },
// }; // };
private static readonly ModuleLog Log = new("CHATHANDLER");
private readonly Regex rmtRegex = new( private readonly Regex rmtRegex = new(
@"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", @"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); RegexOptions.Compiled | RegexOptions.IgnoreCase);
@ -106,11 +106,15 @@ public class ChatHandlers : IServiceType
private readonly DalamudLinkPayload openInstallerWindowLink; private readonly DalamudLinkPayload openInstallerWindowLink;
[ServiceManager.ServiceDependency]
private readonly Dalamud dalamud = Service<Dalamud>.Get();
[ServiceManager.ServiceDependency] [ServiceManager.ServiceDependency]
private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.Get(); private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.Get();
private bool hasSeenLoadingMsg; private bool hasSeenLoadingMsg;
private bool startedAutoUpdatingPlugins; private bool startedAutoUpdatingPlugins;
private CancellationTokenSource deferredAutoUpdateCts = new();
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
private ChatHandlers(ChatGui chatGui) private ChatHandlers(ChatGui chatGui)
@ -120,7 +124,7 @@ public class ChatHandlers : IServiceType
this.openInstallerWindowLink = chatGui.AddChatLinkHandler("Dalamud", 1001, (i, m) => this.openInstallerWindowLink = chatGui.AddChatLinkHandler("Dalamud", 1001, (i, m) =>
{ {
Service<DalamudInterface>.GetNullable()?.OpenPluginInstaller(); Service<DalamudInterface>.GetNullable()?.OpenPluginInstallerTo(PluginInstallerWindow.PluginInstallerOpenKind.InstalledPlugins);
}); });
} }
@ -134,22 +138,6 @@ public class ChatHandlers : IServiceType
/// </summary> /// </summary>
public bool IsAutoUpdateComplete { get; private set; } public bool IsAutoUpdateComplete { get; private set; }
/// <summary>
/// Convert a TextPayload to SeString and wrap in italics payloads.
/// </summary>
/// <param name="text">Text to convert.</param>
/// <returns>SeString payload of italicized text.</returns>
public static SeString MakeItalics(string text)
=> MakeItalics(new TextPayload(text));
/// <summary>
/// Convert a TextPayload to SeString and wrap in italics payloads.
/// </summary>
/// <param name="text">Text to convert.</param>
/// <returns>SeString payload of italicized text.</returns>
public static SeString MakeItalics(TextPayload text)
=> new(EmphasisItalicPayload.ItalicsOn, text, EmphasisItalicPayload.ItalicsOff);
private void OnCheckMessageHandled(XivChatType type, uint senderid, ref SeString sender, ref SeString message, ref bool isHandled) private void OnCheckMessageHandled(XivChatType type, uint senderid, ref SeString sender, ref SeString message, ref bool isHandled)
{ {
var textVal = message.TextValue; var textVal = message.TextValue;
@ -178,21 +166,23 @@ public class ChatHandlers : IServiceType
private void OnChatMessage(XivChatType type, uint senderId, ref SeString sender, ref SeString message, ref bool isHandled) private void OnChatMessage(XivChatType type, uint senderId, ref SeString sender, ref SeString message, ref bool isHandled)
{ {
var startInfo = Service<DalamudStartInfo>.Get();
var clientState = Service<ClientState.ClientState>.GetNullable(); var clientState = Service<ClientState.ClientState>.GetNullable();
if (clientState == null) if (clientState == null)
return; return;
if (type == XivChatType.Notice && !this.hasSeenLoadingMsg) if (type == XivChatType.Notice)
this.PrintWelcomeMessage(); {
if (!this.hasSeenLoadingMsg)
this.PrintWelcomeMessage();
if (!this.startedAutoUpdatingPlugins)
this.AutoUpdatePluginsWithRetry();
}
// For injections while logged in // For injections while logged in
if (clientState.LocalPlayer != null && clientState.TerritoryType == 0 && !this.hasSeenLoadingMsg) if (clientState.LocalPlayer != null && clientState.TerritoryType == 0 && !this.hasSeenLoadingMsg)
this.PrintWelcomeMessage(); this.PrintWelcomeMessage();
if (!this.startedAutoUpdatingPlugins)
this.AutoUpdatePlugins();
#if !DEBUG && false #if !DEBUG && false
if (!this.hasSeenLoadingMsg) if (!this.hasSeenLoadingMsg)
return; return;
@ -200,7 +190,7 @@ public class ChatHandlers : IServiceType
if (type == XivChatType.RetainerSale) if (type == XivChatType.RetainerSale)
{ {
foreach (var regex in this.retainerSaleRegexes[startInfo.Language]) foreach (var regex in this.retainerSaleRegexes[(ClientLanguage)this.dalamud.StartInfo.Language])
{ {
var matchInfo = regex.Match(message.TextValue); var matchInfo = regex.Match(message.TextValue);
@ -258,22 +248,21 @@ public class ChatHandlers : IServiceType
{ {
foreach (var plugin in pluginManager.InstalledPlugins.OrderBy(plugin => plugin.Name).Where(x => x.IsLoaded)) foreach (var plugin in pluginManager.InstalledPlugins.OrderBy(plugin => plugin.Name).Where(x => x.IsLoaded))
{ {
chatGui.Print(string.Format(Loc.Localize("DalamudPluginLoaded", " 》 {0} v{1} loaded."), plugin.Name, plugin.Manifest.AssemblyVersion)); chatGui.Print(string.Format(Loc.Localize("DalamudPluginLoaded", " 》 {0} v{1} loaded."), plugin.Name, plugin.EffectiveVersion));
} }
} }
if (string.IsNullOrEmpty(this.configuration.LastVersion) || !assemblyVersion.StartsWith(this.configuration.LastVersion)) if (string.IsNullOrEmpty(this.configuration.LastVersion) || !assemblyVersion.StartsWith(this.configuration.LastVersion))
{ {
chatGui.PrintChat(new XivChatEntry chatGui.Print(new XivChatEntry
{ {
Message = Loc.Localize("DalamudUpdated", "Dalamud has been updated successfully! Please check the discord for a full changelog."), Message = Loc.Localize("DalamudUpdated", "Dalamud has been updated successfully! Please check the discord for a full changelog."),
Type = XivChatType.Notice, Type = XivChatType.Notice,
}); });
if (string.IsNullOrEmpty(this.configuration.LastChangelogMajorMinor) || (!ChangelogWindow.WarrantsChangelogForMajorMinor.StartsWith(this.configuration.LastChangelogMajorMinor) && assemblyVersion.StartsWith(ChangelogWindow.WarrantsChangelogForMajorMinor))) if (ChangelogWindow.WarrantsChangelog())
{ {
dalamudInterface.OpenChangelogWindow(); dalamudInterface.OpenChangelogWindow();
this.configuration.LastChangelogMajorMinor = ChangelogWindow.WarrantsChangelogForMajorMinor;
} }
this.configuration.LastVersion = assemblyVersion; this.configuration.LastVersion = assemblyVersion;
@ -283,24 +272,42 @@ public class ChatHandlers : IServiceType
this.hasSeenLoadingMsg = true; this.hasSeenLoadingMsg = true;
} }
private void AutoUpdatePlugins() private void AutoUpdatePluginsWithRetry()
{
var firstAttempt = this.AutoUpdatePlugins();
if (!firstAttempt)
{
Task.Run(() =>
{
Task.Delay(30_000, this.deferredAutoUpdateCts.Token);
this.AutoUpdatePlugins();
});
}
}
private bool AutoUpdatePlugins()
{ {
var chatGui = Service<ChatGui>.GetNullable(); var chatGui = Service<ChatGui>.GetNullable();
var pluginManager = Service<PluginManager>.GetNullable(); var pluginManager = Service<PluginManager>.GetNullable();
var notifications = Service<NotificationManager>.GetNullable(); var notifications = Service<NotificationManager>.GetNullable();
if (chatGui == null || pluginManager == null || notifications == null) if (chatGui == null || pluginManager == null || notifications == null)
return; {
Log.Warning("Aborting auto-update because a required service was not loaded.");
return false;
}
if (!pluginManager.ReposReady || !pluginManager.InstalledPlugins.Any() || !pluginManager.AvailablePlugins.Any()) if (!pluginManager.ReposReady || !pluginManager.InstalledPlugins.Any() || !pluginManager.AvailablePlugins.Any())
{ {
// Plugins aren't ready yet. // Plugins aren't ready yet.
// TODO: We should retry. This sucks, because it means we won't ever get here again until another notice. // TODO: We should retry. This sucks, because it means we won't ever get here again until another notice.
return; Log.Warning("Aborting auto-update because plugins weren't loaded or ready.");
return false;
} }
this.startedAutoUpdatingPlugins = true; this.startedAutoUpdatingPlugins = true;
Log.Debug("Beginning plugin auto-update process...");
Task.Run(() => pluginManager.UpdatePluginsAsync(true, !this.configuration.AutoUpdatePlugins, true)).ContinueWith(task => Task.Run(() => pluginManager.UpdatePluginsAsync(true, !this.configuration.AutoUpdatePlugins, true)).ContinueWith(task =>
{ {
this.IsAutoUpdateComplete = true; this.IsAutoUpdateComplete = true;
@ -321,7 +328,7 @@ public class ChatHandlers : IServiceType
} }
else else
{ {
chatGui.PrintChat(new XivChatEntry chatGui.Print(new XivChatEntry
{ {
Message = new SeString(new List<Payload>() Message = new SeString(new List<Payload>()
{ {
@ -339,5 +346,7 @@ public class ChatHandlers : IServiceType
} }
} }
}); });
return true;
} }
} }

View file

@ -18,7 +18,7 @@ namespace Dalamud.Game.ClientState.Aetherytes;
#pragma warning disable SA1015 #pragma warning disable SA1015
[ResolveVia<IAetheryteList>] [ResolveVia<IAetheryteList>]
#pragma warning restore SA1015 #pragma warning restore SA1015
public sealed unsafe partial class AetheryteList : IServiceType, IAetheryteList internal sealed unsafe partial class AetheryteList : IServiceType, IAetheryteList
{ {
[ServiceManager.ServiceDependency] [ServiceManager.ServiceDependency]
private readonly ClientState clientState = Service<ClientState>.Get(); private readonly ClientState clientState = Service<ClientState>.Get();
@ -78,7 +78,7 @@ public sealed unsafe partial class AetheryteList : IServiceType, IAetheryteList
/// <summary> /// <summary>
/// This collection represents the list of available Aetherytes in the Teleport window. /// This collection represents the list of available Aetherytes in the Teleport window.
/// </summary> /// </summary>
public sealed partial class AetheryteList internal sealed partial class AetheryteList
{ {
/// <inheritdoc/> /// <inheritdoc/>
public int Count => this.Length; public int Count => this.Length;

View file

@ -20,7 +20,7 @@ namespace Dalamud.Game.ClientState.Buddy;
#pragma warning disable SA1015 #pragma warning disable SA1015
[ResolveVia<IBuddyList>] [ResolveVia<IBuddyList>]
#pragma warning restore SA1015 #pragma warning restore SA1015
public sealed partial class BuddyList : IServiceType, IBuddyList internal sealed partial class BuddyList : IServiceType, IBuddyList
{ {
private const uint InvalidObjectID = 0xE0000000; private const uint InvalidObjectID = 0xE0000000;
@ -55,18 +55,6 @@ public sealed partial class BuddyList : IServiceType, IBuddyList
} }
} }
/// <summary>
/// Gets a value indicating whether the local player's companion is present.
/// </summary>
[Obsolete("Use CompanionBuddy != null", false)]
public bool CompanionBuddyPresent => this.CompanionBuddy != null;
/// <summary>
/// Gets a value indicating whether the local player's pet is present.
/// </summary>
[Obsolete("Use PetBuddy != null", false)]
public bool PetBuddyPresent => this.PetBuddy != null;
/// <inheritdoc/> /// <inheritdoc/>
public BuddyMember? CompanionBuddy public BuddyMember? CompanionBuddy
{ {
@ -147,7 +135,7 @@ public sealed partial class BuddyList : IServiceType, IBuddyList
/// <summary> /// <summary>
/// This collection represents the buddies present in your squadron or trust party. /// This collection represents the buddies present in your squadron or trust party.
/// </summary> /// </summary>
public sealed partial class BuddyList internal sealed partial class BuddyList
{ {
/// <inheritdoc/> /// <inheritdoc/>
int IReadOnlyCollection<BuddyMember>.Count => this.Length; int IReadOnlyCollection<BuddyMember>.Count => this.Length;

View file

@ -1,4 +1,3 @@
using System;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Dalamud.Data; using Dalamud.Data;
@ -9,24 +8,25 @@ using Dalamud.Game.Network.Internal;
using Dalamud.Hooking; using Dalamud.Hooking;
using Dalamud.IoC; using Dalamud.IoC;
using Dalamud.IoC.Internal; using Dalamud.IoC.Internal;
using Dalamud.Logging.Internal;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Dalamud.Utility; using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game;
using Serilog; using Lumina.Excel.GeneratedSheets;
using Action = System.Action;
namespace Dalamud.Game.ClientState; namespace Dalamud.Game.ClientState;
/// <summary> /// <summary>
/// This class represents the state of the game client at the time of access. /// This class represents the state of the game client at the time of access.
/// </summary> /// </summary>
[PluginInterface]
[InterfaceVersion("1.0")] [InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService] [ServiceManager.BlockingEarlyLoadedService]
#pragma warning disable SA1015 internal sealed class ClientState : IInternalDisposableService, IClientState
[ResolveVia<IClientState>]
#pragma warning restore SA1015
public sealed class ClientState : IDisposable, IServiceType, IClientState
{ {
private static readonly ModuleLog Log = new("ClientState");
private readonly GameLifecycle lifecycle; private readonly GameLifecycle lifecycle;
private readonly ClientStateAddressResolver address; private readonly ClientStateAddressResolver address;
private readonly Hook<SetupTerritoryTypeDelegate> setupTerritoryTypeHook; private readonly Hook<SetupTerritoryTypeDelegate> setupTerritoryTypeHook;
@ -38,10 +38,10 @@ public sealed class ClientState : IDisposable, IServiceType, IClientState
private readonly NetworkHandlers networkHandlers = Service<NetworkHandlers>.Get(); private readonly NetworkHandlers networkHandlers = Service<NetworkHandlers>.Get();
private bool lastConditionNone = true; private bool lastConditionNone = true;
private bool lastFramePvP = false; private bool lastFramePvP;
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
private ClientState(SigScanner sigScanner, DalamudStartInfo startInfo, GameLifecycle lifecycle) private ClientState(TargetSigScanner sigScanner, Dalamud dalamud, GameLifecycle lifecycle)
{ {
this.lifecycle = lifecycle; this.lifecycle = lifecycle;
this.address = new ClientStateAddressResolver(); this.address = new ClientStateAddressResolver();
@ -49,7 +49,7 @@ public sealed class ClientState : IDisposable, IServiceType, IClientState
Log.Verbose("===== C L I E N T S T A T E ====="); Log.Verbose("===== C L I E N T S T A T E =====");
this.ClientLanguage = startInfo.Language; this.ClientLanguage = (ClientLanguage)dalamud.StartInfo.Language;
Log.Verbose($"SetupTerritoryType address 0x{this.address.SetupTerritoryType.ToInt64():X}"); Log.Verbose($"SetupTerritoryType address 0x{this.address.SetupTerritoryType.ToInt64():X}");
@ -58,28 +58,30 @@ public sealed class ClientState : IDisposable, IServiceType, IClientState
this.framework.Update += this.FrameworkOnOnUpdateEvent; this.framework.Update += this.FrameworkOnOnUpdateEvent;
this.networkHandlers.CfPop += this.NetworkHandlersOnCfPop; this.networkHandlers.CfPop += this.NetworkHandlersOnCfPop;
this.setupTerritoryTypeHook.Enable();
} }
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] [UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate IntPtr SetupTerritoryTypeDelegate(IntPtr manager, ushort terriType); private delegate IntPtr SetupTerritoryTypeDelegate(IntPtr manager, ushort terriType);
/// <inheritdoc/> /// <inheritdoc/>
public event EventHandler<ushort> TerritoryChanged; public event Action<ushort>? TerritoryChanged;
/// <inheritdoc/> /// <inheritdoc/>
public event EventHandler Login; public event Action? Login;
/// <inheritdoc/> /// <inheritdoc/>
public event EventHandler Logout; public event Action? Logout;
/// <inheritdoc/> /// <inheritdoc/>
public event Action EnterPvP; public event Action? EnterPvP;
/// <inheritdoc/> /// <inheritdoc/>
public event Action LeavePvP; public event Action? LeavePvP;
/// <inheritdoc/> /// <inheritdoc/>
public event EventHandler<Lumina.Excel.GeneratedSheets.ContentFinderCondition> CfPop; public event Action<ContentFinderCondition>? CfPop;
/// <inheritdoc/> /// <inheritdoc/>
public ClientLanguage ClientLanguage { get; } public ClientLanguage ClientLanguage { get; }
@ -102,6 +104,9 @@ public sealed class ClientState : IDisposable, IServiceType, IClientState
/// <inheritdoc/> /// <inheritdoc/>
public bool IsPvPExcludingDen { get; private set; } public bool IsPvPExcludingDen { get; private set; }
/// <inheritdoc />
public bool IsGPosing => GameMain.IsInGPose();
/// <summary> /// <summary>
/// Gets client state address resolver. /// Gets client state address resolver.
/// </summary> /// </summary>
@ -110,35 +115,29 @@ public sealed class ClientState : IDisposable, IServiceType, IClientState
/// <summary> /// <summary>
/// Dispose of managed and unmanaged resources. /// Dispose of managed and unmanaged resources.
/// </summary> /// </summary>
void IDisposable.Dispose() void IInternalDisposableService.DisposeService()
{ {
this.setupTerritoryTypeHook.Dispose(); this.setupTerritoryTypeHook.Dispose();
this.framework.Update -= this.FrameworkOnOnUpdateEvent; this.framework.Update -= this.FrameworkOnOnUpdateEvent;
this.networkHandlers.CfPop -= this.NetworkHandlersOnCfPop; this.networkHandlers.CfPop -= this.NetworkHandlersOnCfPop;
} }
[ServiceManager.CallWhenServicesReady]
private void ContinueConstruction()
{
this.setupTerritoryTypeHook.Enable();
}
private IntPtr SetupTerritoryTypeDetour(IntPtr manager, ushort terriType) private IntPtr SetupTerritoryTypeDetour(IntPtr manager, ushort terriType)
{ {
this.TerritoryType = terriType; this.TerritoryType = terriType;
this.TerritoryChanged?.InvokeSafely(this, terriType); this.TerritoryChanged?.InvokeSafely(terriType);
Log.Debug("TerritoryType changed: {0}", terriType); Log.Debug("TerritoryType changed: {0}", terriType);
return this.setupTerritoryTypeHook.Original(manager, terriType); return this.setupTerritoryTypeHook.Original(manager, terriType);
} }
private void NetworkHandlersOnCfPop(object sender, Lumina.Excel.GeneratedSheets.ContentFinderCondition e) private void NetworkHandlersOnCfPop(ContentFinderCondition e)
{ {
this.CfPop?.InvokeSafely(this, e); this.CfPop?.InvokeSafely(e);
} }
private void FrameworkOnOnUpdateEvent(Framework framework1) private void FrameworkOnOnUpdateEvent(IFramework framework1)
{ {
var condition = Service<Conditions.Condition>.GetNullable(); var condition = Service<Conditions.Condition>.GetNullable();
var gameGui = Service<GameGui>.GetNullable(); var gameGui = Service<GameGui>.GetNullable();
@ -147,12 +146,12 @@ public sealed class ClientState : IDisposable, IServiceType, IClientState
if (condition == null || gameGui == null || data == null) if (condition == null || gameGui == null || data == null)
return; return;
if (condition.Any() && this.lastConditionNone == true && this.LocalPlayer != null) if (condition.Any() && this.lastConditionNone && this.LocalPlayer != null)
{ {
Log.Debug("Is login"); Log.Debug("Is login");
this.lastConditionNone = false; this.lastConditionNone = false;
this.IsLoggedIn = true; this.IsLoggedIn = true;
this.Login?.InvokeSafely(this, null); this.Login?.InvokeSafely();
gameGui.ResetUiHideState(); gameGui.ResetUiHideState();
this.lifecycle.ResetLogout(); this.lifecycle.ResetLogout();
@ -163,7 +162,7 @@ public sealed class ClientState : IDisposable, IServiceType, IClientState
Log.Debug("Is logout"); Log.Debug("Is logout");
this.lastConditionNone = true; this.lastConditionNone = true;
this.IsLoggedIn = false; this.IsLoggedIn = false;
this.Logout?.InvokeSafely(this, null); this.Logout?.InvokeSafely();
gameGui.ResetUiHideState(); gameGui.ResetUiHideState();
this.lifecycle.SetLogout(); this.lifecycle.SetLogout();
@ -187,3 +186,103 @@ public sealed class ClientState : IDisposable, IServiceType, IClientState
} }
} }
} }
/// <summary>
/// Plugin-scoped version of a GameConfig service.
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.ScopedService]
#pragma warning disable SA1015
[ResolveVia<IClientState>]
#pragma warning restore SA1015
internal class ClientStatePluginScoped : IInternalDisposableService, IClientState
{
[ServiceManager.ServiceDependency]
private readonly ClientState clientStateService = Service<ClientState>.Get();
/// <summary>
/// Initializes a new instance of the <see cref="ClientStatePluginScoped"/> class.
/// </summary>
internal ClientStatePluginScoped()
{
this.clientStateService.TerritoryChanged += this.TerritoryChangedForward;
this.clientStateService.Login += this.LoginForward;
this.clientStateService.Logout += this.LogoutForward;
this.clientStateService.EnterPvP += this.EnterPvPForward;
this.clientStateService.LeavePvP += this.ExitPvPForward;
this.clientStateService.CfPop += this.ContentFinderPopForward;
}
/// <inheritdoc/>
public event Action<ushort>? TerritoryChanged;
/// <inheritdoc/>
public event Action? Login;
/// <inheritdoc/>
public event Action? Logout;
/// <inheritdoc/>
public event Action? EnterPvP;
/// <inheritdoc/>
public event Action? LeavePvP;
/// <inheritdoc/>
public event Action<ContentFinderCondition>? CfPop;
/// <inheritdoc/>
public ClientLanguage ClientLanguage => this.clientStateService.ClientLanguage;
/// <inheritdoc/>
public ushort TerritoryType => this.clientStateService.TerritoryType;
/// <inheritdoc/>
public PlayerCharacter? LocalPlayer => this.clientStateService.LocalPlayer;
/// <inheritdoc/>
public ulong LocalContentId => this.clientStateService.LocalContentId;
/// <inheritdoc/>
public bool IsLoggedIn => this.clientStateService.IsLoggedIn;
/// <inheritdoc/>
public bool IsPvP => this.clientStateService.IsPvP;
/// <inheritdoc/>
public bool IsPvPExcludingDen => this.clientStateService.IsPvPExcludingDen;
/// <inheritdoc/>
public bool IsGPosing => this.clientStateService.IsGPosing;
/// <inheritdoc/>
void IInternalDisposableService.DisposeService()
{
this.clientStateService.TerritoryChanged -= this.TerritoryChangedForward;
this.clientStateService.Login -= this.LoginForward;
this.clientStateService.Logout -= this.LogoutForward;
this.clientStateService.EnterPvP -= this.EnterPvPForward;
this.clientStateService.LeavePvP -= this.ExitPvPForward;
this.clientStateService.CfPop -= this.ContentFinderPopForward;
this.TerritoryChanged = null;
this.Login = null;
this.Logout = null;
this.EnterPvP = null;
this.LeavePvP = null;
this.CfPop = null;
}
private void TerritoryChangedForward(ushort territoryId) => this.TerritoryChanged?.Invoke(territoryId);
private void LoginForward() => this.Login?.Invoke();
private void LogoutForward() => this.Logout?.Invoke();
private void EnterPvPForward() => this.EnterPvP?.Invoke();
private void ExitPvPForward() => this.LeavePvP?.Invoke();
private void ContentFinderPopForward(ContentFinderCondition cfc) => this.CfPop?.Invoke(cfc);
}

View file

@ -5,7 +5,7 @@ namespace Dalamud.Game.ClientState;
/// <summary> /// <summary>
/// Client state memory address resolver. /// Client state memory address resolver.
/// </summary> /// </summary>
public sealed class ClientStateAddressResolver : BaseAddressResolver internal sealed class ClientStateAddressResolver : BaseAddressResolver
{ {
// Static offsets // Static offsets
@ -79,7 +79,7 @@ public sealed class ClientStateAddressResolver : BaseAddressResolver
/// Scan for and setup any configured address pointers. /// Scan for and setup any configured address pointers.
/// </summary> /// </summary>
/// <param name="sig">The signature scanner to facilitate setup.</param> /// <param name="sig">The signature scanner to facilitate setup.</param>
protected override void Setup64Bit(SigScanner sig) protected override void Setup64Bit(ISigScanner sig)
{ {
this.ObjectTable = sig.GetStaticAddressFromSig("48 8D 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 44 0F B6 83 ?? ?? ?? ?? C6 83 ?? ?? ?? ?? ??"); this.ObjectTable = sig.GetStaticAddressFromSig("48 8D 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 44 0F B6 83 ?? ?? ?? ?? C6 83 ?? ?? ?? ?? ??");

View file

@ -1,7 +1,6 @@
using System;
using Dalamud.IoC; using Dalamud.IoC;
using Dalamud.IoC.Internal; using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services;
using Serilog; using Serilog;
namespace Dalamud.Game.ClientState.Conditions; namespace Dalamud.Game.ClientState.Conditions;
@ -9,47 +8,48 @@ namespace Dalamud.Game.ClientState.Conditions;
/// <summary> /// <summary>
/// Provides access to conditions (generally player state). You can check whether a player is in combat, mounted, etc. /// Provides access to conditions (generally player state). You can check whether a player is in combat, mounted, etc.
/// </summary> /// </summary>
[PluginInterface]
[InterfaceVersion("1.0")] [InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService] [ServiceManager.BlockingEarlyLoadedService]
public sealed partial class Condition : IServiceType internal sealed partial class Condition : IInternalDisposableService, ICondition
{ {
/// <summary> /// <summary>
/// The current max number of conditions. You can get this just by looking at the condition sheet and how many rows it has. /// Gets the current max number of conditions. You can get this just by looking at the condition sheet and how many rows it has.
/// </summary> /// </summary>
public const int MaxConditionEntries = 104; internal const int MaxConditionEntries = 104;
[ServiceManager.ServiceDependency]
private readonly Framework framework = Service<Framework>.Get();
private readonly bool[] cache = new bool[MaxConditionEntries]; private readonly bool[] cache = new bool[MaxConditionEntries];
private bool isDisposed;
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
private Condition(ClientState clientState) private Condition(ClientState clientState)
{ {
var resolver = clientState.AddressResolver; var resolver = clientState.AddressResolver;
this.Address = resolver.ConditionFlags; this.Address = resolver.ConditionFlags;
// Initialization
for (var i = 0; i < MaxConditionEntries; i++)
this.cache[i] = this[i];
this.framework.Update += this.FrameworkUpdate;
} }
/// <summary>Finalizes an instance of the <see cref="Condition" /> class.</summary>
~Condition() => this.Dispose(false);
/// <summary> /// <inheritdoc/>
/// A delegate type used with the <see cref="ConditionChange"/> event. public event ICondition.ConditionChangeDelegate? ConditionChange;
/// </summary>
/// <param name="flag">The changed condition.</param>
/// <param name="value">The value the condition is set to.</param>
public delegate void ConditionChangeDelegate(ConditionFlag flag, bool value);
/// <summary> /// <inheritdoc/>
/// Event that gets fired when a condition is set. public int MaxEntries => MaxConditionEntries;
/// Should only get fired for actual changes, so the previous value will always be !value.
/// </summary>
public event ConditionChangeDelegate? ConditionChange;
/// <summary> /// <inheritdoc/>
/// Gets the condition array base pointer.
/// </summary>
public IntPtr Address { get; private set; } public IntPtr Address { get; private set; }
/// <summary> /// <inheritdoc/>
/// Check the value of a specific condition/state flag.
/// </summary>
/// <param name="flag">The condition flag to check.</param>
public unsafe bool this[int flag] public unsafe bool this[int flag]
{ {
get get
@ -61,14 +61,14 @@ public sealed partial class Condition : IServiceType
} }
} }
/// <inheritdoc cref="this[int]"/> /// <inheritdoc/>
public unsafe bool this[ConditionFlag flag] public bool this[ConditionFlag flag]
=> this[(int)flag]; => this[(int)flag];
/// <summary> /// <inheritdoc/>
/// Check if any condition flags are set. void IInternalDisposableService.DisposeService() => this.Dispose(true);
/// </summary>
/// <returns>Whether any single flag is set.</returns> /// <inheritdoc/>
public bool Any() public bool Any()
{ {
for (var i = 0; i < MaxConditionEntries; i++) for (var i = 0; i < MaxConditionEntries; i++)
@ -81,18 +81,36 @@ public sealed partial class Condition : IServiceType
return false; return false;
} }
[ServiceManager.CallWhenServicesReady] /// <inheritdoc/>
private void ContinueConstruction(Framework framework) public bool Any(params ConditionFlag[] flags)
{ {
// Initialization foreach (var flag in flags)
for (var i = 0; i < MaxConditionEntries; i++) {
this.cache[i] = this[i]; // this[i] performs range checking, so no need to check here
if (this[flag])
{
return true;
}
}
framework.Update += this.FrameworkUpdate; return false;
} }
private void FrameworkUpdate(Framework framework) private void Dispose(bool disposing)
{
if (this.isDisposed)
return;
if (disposing)
{
this.framework.Update -= this.FrameworkUpdate;
}
this.isDisposed = true;
}
private void FrameworkUpdate(IFramework unused)
{ {
for (var i = 0; i < MaxConditionEntries; i++) for (var i = 0; i < MaxConditionEntries; i++)
{ {
@ -116,39 +134,52 @@ public sealed partial class Condition : IServiceType
} }
/// <summary> /// <summary>
/// Provides access to conditions (generally player state). You can check whether a player is in combat, mounted, etc. /// Plugin-scoped version of a Condition service.
/// </summary> /// </summary>
public sealed partial class Condition : IDisposable [PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.ScopedService]
#pragma warning disable SA1015
[ResolveVia<ICondition>]
#pragma warning restore SA1015
internal class ConditionPluginScoped : IInternalDisposableService, ICondition
{ {
private bool isDisposed; [ServiceManager.ServiceDependency]
private readonly Condition conditionService = Service<Condition>.Get();
/// <summary> /// <summary>
/// Finalizes an instance of the <see cref="Condition" /> class. /// Initializes a new instance of the <see cref="ConditionPluginScoped"/> class.
/// </summary> /// </summary>
~Condition() internal ConditionPluginScoped()
{ {
this.Dispose(false); this.conditionService.ConditionChange += this.ConditionChangedForward;
}
/// <inheritdoc/>
public event ICondition.ConditionChangeDelegate? ConditionChange;
/// <inheritdoc/>
public int MaxEntries => this.conditionService.MaxEntries;
/// <inheritdoc/>
public IntPtr Address => this.conditionService.Address;
/// <inheritdoc/>
public bool this[int flag] => this.conditionService[flag];
/// <inheritdoc/>
void IInternalDisposableService.DisposeService()
{
this.conditionService.ConditionChange -= this.ConditionChangedForward;
this.ConditionChange = null;
} }
/// <summary> /// <inheritdoc/>
/// Disposes this instance, alongside its hooks. public bool Any() => this.conditionService.Any();
/// </summary>
void IDisposable.Dispose()
{
GC.SuppressFinalize(this);
this.Dispose(true);
}
private void Dispose(bool disposing) /// <inheritdoc/>
{ public bool Any(params ConditionFlag[] flags) => this.conditionService.Any(flags);
if (this.isDisposed)
return;
if (disposing) private void ConditionChangedForward(ConditionFlag flag, bool value) => this.ConditionChange?.Invoke(flag, value);
{
Service<Framework>.Get().Update -= this.FrameworkUpdate;
}
this.isDisposed = true;
}
} }

View file

@ -18,7 +18,7 @@ namespace Dalamud.Game.ClientState.Fates;
#pragma warning disable SA1015 #pragma warning disable SA1015
[ResolveVia<IFateTable>] [ResolveVia<IFateTable>]
#pragma warning restore SA1015 #pragma warning restore SA1015
public sealed partial class FateTable : IServiceType, IFateTable internal sealed partial class FateTable : IServiceType, IFateTable
{ {
private readonly ClientStateAddressResolver address; private readonly ClientStateAddressResolver address;
@ -110,7 +110,7 @@ public sealed partial class FateTable : IServiceType, IFateTable
/// <summary> /// <summary>
/// This collection represents the currently available Fate events. /// This collection represents the currently available Fate events.
/// </summary> /// </summary>
public sealed partial class FateTable internal sealed partial class FateTable
{ {
/// <inheritdoc/> /// <inheritdoc/>
int IReadOnlyCollection<Fate>.Count => this.Length; int IReadOnlyCollection<Fate>.Count => this.Length;

View file

@ -21,7 +21,7 @@ namespace Dalamud.Game.ClientState.GamePad;
#pragma warning disable SA1015 #pragma warning disable SA1015
[ResolveVia<IGamepadState>] [ResolveVia<IGamepadState>]
#pragma warning restore SA1015 #pragma warning restore SA1015
public unsafe class GamepadState : IDisposable, IServiceType, IGamepadState internal unsafe class GamepadState : IInternalDisposableService, IGamepadState
{ {
private readonly Hook<ControllerPoll>? gamepadPoll; private readonly Hook<ControllerPoll>? gamepadPoll;
@ -38,6 +38,7 @@ public unsafe class GamepadState : IDisposable, IServiceType, IGamepadState
var resolver = clientState.AddressResolver; var resolver = clientState.AddressResolver;
Log.Verbose($"GamepadPoll address 0x{resolver.GamepadPoll.ToInt64():X}"); Log.Verbose($"GamepadPoll address 0x{resolver.GamepadPoll.ToInt64():X}");
this.gamepadPoll = Hook<ControllerPoll>.FromAddress(resolver.GamepadPoll, this.GamepadPollDetour); this.gamepadPoll = Hook<ControllerPoll>.FromAddress(resolver.GamepadPoll, this.GamepadPollDetour);
this.gamepadPoll?.Enable();
} }
private delegate int ControllerPoll(IntPtr controllerInput); private delegate int ControllerPoll(IntPtr controllerInput);
@ -55,54 +56,6 @@ public unsafe class GamepadState : IDisposable, IServiceType, IGamepadState
public Vector2 RightStick => public Vector2 RightStick =>
new(this.rightStickX, this.rightStickY); new(this.rightStickX, this.rightStickY);
/// <summary>
/// Gets the state of the left analogue stick in the left direction between 0 (not tilted) and 1 (max tilt).
/// </summary>
[Obsolete("Use IGamepadState.LeftStick.X", false)]
public float LeftStickLeft => this.leftStickX < 0 ? -this.leftStickX / 100f : 0;
/// <summary>
/// Gets the state of the left analogue stick in the right direction between 0 (not tilted) and 1 (max tilt).
/// </summary>
[Obsolete("Use IGamepadState.LeftStick.X", false)]
public float LeftStickRight => this.leftStickX > 0 ? this.leftStickX / 100f : 0;
/// <summary>
/// Gets the state of the left analogue stick in the up direction between 0 (not tilted) and 1 (max tilt).
/// </summary>
[Obsolete("Use IGamepadState.LeftStick.Y", false)]
public float LeftStickUp => this.leftStickY > 0 ? this.leftStickY / 100f : 0;
/// <summary>
/// Gets the state of the left analogue stick in the down direction between 0 (not tilted) and 1 (max tilt).
/// </summary>
[Obsolete("Use IGamepadState.LeftStick.Y", false)]
public float LeftStickDown => this.leftStickY < 0 ? -this.leftStickY / 100f : 0;
/// <summary>
/// Gets the state of the right analogue stick in the left direction between 0 (not tilted) and 1 (max tilt).
/// </summary>
[Obsolete("Use IGamepadState.RightStick.X", false)]
public float RightStickLeft => this.rightStickX < 0 ? -this.rightStickX / 100f : 0;
/// <summary>
/// Gets the state of the right analogue stick in the right direction between 0 (not tilted) and 1 (max tilt).
/// </summary>
[Obsolete("Use IGamepadState.RightStick.X", false)]
public float RightStickRight => this.rightStickX > 0 ? this.rightStickX / 100f : 0;
/// <summary>
/// Gets the state of the right analogue stick in the up direction between 0 (not tilted) and 1 (max tilt).
/// </summary>
[Obsolete("Use IGamepadState.RightStick.Y", false)]
public float RightStickUp => this.rightStickY > 0 ? this.rightStickY / 100f : 0;
/// <summary>
/// Gets the state of the right analogue stick in the down direction between 0 (not tilted) and 1 (max tilt).
/// </summary>
[Obsolete("Use IGamepadState.RightStick.Y", false)]
public float RightStickDown => this.rightStickY < 0 ? -this.rightStickY / 100f : 0;
/// <summary> /// <summary>
/// Gets buttons pressed bitmask, set once when the button is pressed. See <see cref="GamepadButtons"/> for the mapping. /// Gets buttons pressed bitmask, set once when the button is pressed. See <see cref="GamepadButtons"/> for the mapping.
/// ///
@ -156,18 +109,12 @@ public unsafe class GamepadState : IDisposable, IServiceType, IGamepadState
/// <summary> /// <summary>
/// Disposes this instance, alongside its hooks. /// Disposes this instance, alongside its hooks.
/// </summary> /// </summary>
void IDisposable.Dispose() void IInternalDisposableService.DisposeService()
{ {
this.Dispose(true); this.Dispose(true);
GC.SuppressFinalize(this); GC.SuppressFinalize(this);
} }
[ServiceManager.CallWhenServicesReady]
private void ContinueConstruction()
{
this.gamepadPoll?.Enable();
}
private int GamepadPollDetour(IntPtr gamepadInput) private int GamepadPollDetour(IntPtr gamepadInput)
{ {
var original = this.gamepadPoll!.Original(gamepadInput); var original = this.gamepadPoll!.Original(gamepadInput);

View file

@ -19,7 +19,7 @@ namespace Dalamud.Game.ClientState.JobGauge;
#pragma warning disable SA1015 #pragma warning disable SA1015
[ResolveVia<IJobGauges>] [ResolveVia<IJobGauges>]
#pragma warning restore SA1015 #pragma warning restore SA1015
public class JobGauges : IServiceType, IJobGauges internal class JobGauges : IServiceType, IJobGauges
{ {
private Dictionary<Type, JobGaugeBase> cache = new(); private Dictionary<Type, JobGaugeBase> cache = new();

View file

@ -1,9 +1,11 @@
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Dalamud.IoC; using Dalamud.IoC;
using Dalamud.IoC.Internal; using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services;
using Serilog; using Serilog;
namespace Dalamud.Game.ClientState.Keys; namespace Dalamud.Game.ClientState.Keys;
@ -23,7 +25,10 @@ namespace Dalamud.Game.ClientState.Keys;
[PluginInterface] [PluginInterface]
[InterfaceVersion("1.0")] [InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService] [ServiceManager.BlockingEarlyLoadedService]
public class KeyState : IServiceType #pragma warning disable SA1015
[ResolveVia<IKeyState>]
#pragma warning restore SA1015
internal class KeyState : IServiceType, IKeyState
{ {
// The array is accessed in a way that this limit doesn't appear to exist // The array is accessed in a way that this limit doesn't appear to exist
// but there is other state data past this point, and keys beyond here aren't // but there is other state data past this point, and keys beyond here aren't
@ -31,10 +36,10 @@ public class KeyState : IServiceType
private const int MaxKeyCode = 0xF0; private const int MaxKeyCode = 0xF0;
private readonly IntPtr bufferBase; private readonly IntPtr bufferBase;
private readonly IntPtr indexBase; private readonly IntPtr indexBase;
private VirtualKey[] validVirtualKeyCache = null; private VirtualKey[]? validVirtualKeyCache;
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
private KeyState(SigScanner sigScanner, ClientState clientState) private KeyState(TargetSigScanner sigScanner, ClientState clientState)
{ {
var moduleBaseAddress = sigScanner.Module.BaseAddress; var moduleBaseAddress = sigScanner.Module.BaseAddress;
var addressResolver = clientState.AddressResolver; var addressResolver = clientState.AddressResolver;
@ -44,46 +49,29 @@ public class KeyState : IServiceType
Log.Verbose($"Keyboard state buffer address 0x{this.bufferBase.ToInt64():X}"); Log.Verbose($"Keyboard state buffer address 0x{this.bufferBase.ToInt64():X}");
} }
/// <summary> /// <inheritdoc/>
/// Get or set the key-pressed state for a given vkCode. public bool this[int vkCode]
/// </summary>
/// <param name="vkCode">The virtual key to change.</param>
/// <returns>Whether the specified key is currently pressed.</returns>
/// <exception cref="ArgumentException">If the vkCode is not valid. Refer to <see cref="IsVirtualKeyValid(int)"/> or <see cref="GetValidVirtualKeys"/>.</exception>
/// <exception cref="ArgumentOutOfRangeException">If the set value is non-zero.</exception>
public unsafe bool this[int vkCode]
{ {
get => this.GetRawValue(vkCode) != 0; get => this.GetRawValue(vkCode) != 0;
set => this.SetRawValue(vkCode, value ? 1 : 0); set => this.SetRawValue(vkCode, value ? 1 : 0);
} }
/// <inheritdoc cref="this[int]"/> /// <inheritdoc/>
public bool this[VirtualKey vkCode] public bool this[VirtualKey vkCode]
{ {
get => this[(int)vkCode]; get => this[(int)vkCode];
set => this[(int)vkCode] = value; set => this[(int)vkCode] = value;
} }
/// <summary> /// <inheritdoc/>
/// Gets the value in the index array.
/// </summary>
/// <param name="vkCode">The virtual key to change.</param>
/// <returns>The raw value stored in the index array.</returns>
/// <exception cref="ArgumentException">If the vkCode is not valid. Refer to <see cref="IsVirtualKeyValid(int)"/> or <see cref="GetValidVirtualKeys"/>.</exception>
public int GetRawValue(int vkCode) public int GetRawValue(int vkCode)
=> this.GetRefValue(vkCode); => this.GetRefValue(vkCode);
/// <inheritdoc cref="GetRawValue(int)"/> /// <inheritdoc/>
public int GetRawValue(VirtualKey vkCode) public int GetRawValue(VirtualKey vkCode)
=> this.GetRawValue((int)vkCode); => this.GetRawValue((int)vkCode);
/// <summary> /// <inheritdoc/>
/// Sets the value in the index array.
/// </summary>
/// <param name="vkCode">The virtual key to change.</param>
/// <param name="value">The raw value to set in the index array.</param>
/// <exception cref="ArgumentException">If the vkCode is not valid. Refer to <see cref="IsVirtualKeyValid(int)"/> or <see cref="GetValidVirtualKeys"/>.</exception>
/// <exception cref="ArgumentOutOfRangeException">If the set value is non-zero.</exception>
public void SetRawValue(int vkCode, int value) public void SetRawValue(int vkCode, int value)
{ {
if (value != 0) if (value != 0)
@ -92,32 +80,23 @@ public class KeyState : IServiceType
this.GetRefValue(vkCode) = value; this.GetRefValue(vkCode) = value;
} }
/// <inheritdoc cref="SetRawValue(int, int)"/> /// <inheritdoc/>
public void SetRawValue(VirtualKey vkCode, int value) public void SetRawValue(VirtualKey vkCode, int value)
=> this.SetRawValue((int)vkCode, value); => this.SetRawValue((int)vkCode, value);
/// <summary> /// <inheritdoc/>
/// Gets a value indicating whether the given VirtualKey code is regarded as valid input by the game.
/// </summary>
/// <param name="vkCode">Virtual key code.</param>
/// <returns>If the code is valid.</returns>
public bool IsVirtualKeyValid(int vkCode) public bool IsVirtualKeyValid(int vkCode)
=> this.ConvertVirtualKey(vkCode) != 0; => this.ConvertVirtualKey(vkCode) != 0;
/// <inheritdoc cref="IsVirtualKeyValid(int)"/> /// <inheritdoc/>
public bool IsVirtualKeyValid(VirtualKey vkCode) public bool IsVirtualKeyValid(VirtualKey vkCode)
=> this.IsVirtualKeyValid((int)vkCode); => this.IsVirtualKeyValid((int)vkCode);
/// <summary> /// <inheritdoc/>
/// Gets an array of virtual keys the game considers valid input. public IEnumerable<VirtualKey> GetValidVirtualKeys()
/// </summary> => this.validVirtualKeyCache ??= Enum.GetValues<VirtualKey>().Where(this.IsVirtualKeyValid).ToArray();
/// <returns>An array of valid virtual keys.</returns>
public VirtualKey[] GetValidVirtualKeys()
=> this.validVirtualKeyCache ??= Enum.GetValues<VirtualKey>().Where(vk => this.IsVirtualKeyValid(vk)).ToArray();
/// <summary> /// <inheritdoc/>
/// Clears the pressed state for all keys.
/// </summary>
public void ClearAll() public void ClearAll()
{ {
foreach (var vk in this.GetValidVirtualKeys()) foreach (var vk in this.GetValidVirtualKeys())

View file

@ -21,9 +21,9 @@ namespace Dalamud.Game.ClientState.Objects;
#pragma warning disable SA1015 #pragma warning disable SA1015
[ResolveVia<IObjectTable>] [ResolveVia<IObjectTable>]
#pragma warning restore SA1015 #pragma warning restore SA1015
public sealed partial class ObjectTable : IServiceType, IObjectTable internal sealed partial class ObjectTable : IServiceType, IObjectTable
{ {
private const int ObjectTableLength = 596; private const int ObjectTableLength = 599;
private readonly ClientStateAddressResolver address; private readonly ClientStateAddressResolver address;
@ -109,7 +109,7 @@ public sealed partial class ObjectTable : IServiceType, IObjectTable
/// <summary> /// <summary>
/// This collection represents the currently spawned FFXIV game objects. /// This collection represents the currently spawned FFXIV game objects.
/// </summary> /// </summary>
public sealed partial class ObjectTable internal sealed partial class ObjectTable
{ {
/// <inheritdoc/> /// <inheritdoc/>
int IReadOnlyCollection<GameObject>.Count => this.Length; int IReadOnlyCollection<GameObject>.Count => this.Length;

View file

@ -1,5 +1,3 @@
using System;
using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Game.ClientState.Objects.Enums;
namespace Dalamud.Game.ClientState.Objects.Types; namespace Dalamud.Game.ClientState.Objects.Types;
@ -25,5 +23,5 @@ public unsafe class BattleNpc : BattleChara
public BattleNpcSubKind BattleNpcKind => (BattleNpcSubKind)this.Struct->Character.GameObject.SubKind; public BattleNpcSubKind BattleNpcKind => (BattleNpcSubKind)this.Struct->Character.GameObject.SubKind;
/// <inheritdoc/> /// <inheritdoc/>
public override ulong TargetObjectId => this.Struct->Character.TargetObjectID; public override ulong TargetObjectId => this.Struct->Character.TargetId;
} }

View file

@ -1,5 +1,3 @@
using System;
using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Game.ClientState.Resolvers; using Dalamud.Game.ClientState.Resolvers;
@ -33,5 +31,5 @@ public unsafe class PlayerCharacter : BattleChara
/// <summary> /// <summary>
/// Gets the target actor ID of the PlayerCharacter. /// Gets the target actor ID of the PlayerCharacter.
/// </summary> /// </summary>
public override ulong TargetObjectId => this.Struct->Character.PlayerTargetObjectID; public override ulong TargetObjectId => this.Struct->Character.LookTargetId;
} }

View file

@ -16,7 +16,7 @@ namespace Dalamud.Game.ClientState.Objects;
#pragma warning disable SA1015 #pragma warning disable SA1015
[ResolveVia<ITargetManager>] [ResolveVia<ITargetManager>]
#pragma warning restore SA1015 #pragma warning restore SA1015
public sealed unsafe class TargetManager : IServiceType, ITargetManager internal sealed unsafe class TargetManager : IServiceType, ITargetManager
{ {
[ServiceManager.ServiceDependency] [ServiceManager.ServiceDependency]
private readonly ClientState clientState = Service<ClientState>.Get(); private readonly ClientState clientState = Service<ClientState>.Get();
@ -39,136 +39,50 @@ public sealed unsafe class TargetManager : IServiceType, ITargetManager
public GameObject? Target public GameObject? Target
{ {
get => this.objectTable.CreateObjectReference((IntPtr)Struct->Target); get => this.objectTable.CreateObjectReference((IntPtr)Struct->Target);
set => this.SetTarget(value); set => Struct->Target = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address;
} }
/// <inheritdoc/> /// <inheritdoc/>
public GameObject? MouseOverTarget public GameObject? MouseOverTarget
{ {
get => this.objectTable.CreateObjectReference((IntPtr)Struct->MouseOverTarget); get => this.objectTable.CreateObjectReference((IntPtr)Struct->MouseOverTarget);
set => this.SetMouseOverTarget(value); set => Struct->MouseOverTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address;
} }
/// <inheritdoc/> /// <inheritdoc/>
public GameObject? FocusTarget public GameObject? FocusTarget
{ {
get => this.objectTable.CreateObjectReference((IntPtr)Struct->FocusTarget); get => this.objectTable.CreateObjectReference((IntPtr)Struct->FocusTarget);
set => this.SetFocusTarget(value); set => Struct->FocusTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address;
} }
/// <inheritdoc/> /// <inheritdoc/>
public GameObject? PreviousTarget public GameObject? PreviousTarget
{ {
get => this.objectTable.CreateObjectReference((IntPtr)Struct->PreviousTarget); get => this.objectTable.CreateObjectReference((IntPtr)Struct->PreviousTarget);
set => this.SetPreviousTarget(value); set => Struct->PreviousTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address;
} }
/// <inheritdoc/> /// <inheritdoc/>
public GameObject? SoftTarget public GameObject? SoftTarget
{ {
get => this.objectTable.CreateObjectReference((IntPtr)Struct->SoftTarget); get => this.objectTable.CreateObjectReference((IntPtr)Struct->SoftTarget);
set => this.SetSoftTarget(value); set => Struct->SoftTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address;
}
/// <inheritdoc/>
public GameObject? GPoseTarget
{
get => this.objectTable.CreateObjectReference((IntPtr)Struct->GPoseTarget);
set => Struct->GPoseTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address;
}
/// <inheritdoc/>
public GameObject? MouseOverNameplateTarget
{
get => this.objectTable.CreateObjectReference((IntPtr)Struct->MouseOverNameplateTarget);
set => Struct->MouseOverNameplateTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address;
} }
private FFXIVClientStructs.FFXIV.Client.Game.Control.TargetSystem* Struct => (FFXIVClientStructs.FFXIV.Client.Game.Control.TargetSystem*)this.Address; private FFXIVClientStructs.FFXIV.Client.Game.Control.TargetSystem* Struct => (FFXIVClientStructs.FFXIV.Client.Game.Control.TargetSystem*)this.Address;
/// <summary>
/// Sets the current target.
/// </summary>
/// <param name="actor">Actor to target.</param>
[Obsolete("Use Target Property", false)]
public void SetTarget(GameObject? actor) => this.SetTarget(actor?.Address ?? IntPtr.Zero);
/// <summary>
/// Sets the mouseover target.
/// </summary>
/// <param name="actor">Actor to target.</param>
[Obsolete("Use MouseOverTarget Property", false)]
public void SetMouseOverTarget(GameObject? actor) => this.SetMouseOverTarget(actor?.Address ?? IntPtr.Zero);
/// <summary>
/// Sets the focus target.
/// </summary>
/// <param name="actor">Actor to target.</param>
[Obsolete("Use FocusTarget Property", false)]
public void SetFocusTarget(GameObject? actor) => this.SetFocusTarget(actor?.Address ?? IntPtr.Zero);
/// <summary>
/// Sets the previous target.
/// </summary>
/// <param name="actor">Actor to target.</param>
[Obsolete("Use PreviousTarget Property", false)]
public void SetPreviousTarget(GameObject? actor) => this.SetTarget(actor?.Address ?? IntPtr.Zero);
/// <summary>
/// Sets the soft target.
/// </summary>
/// <param name="actor">Actor to target.</param>
[Obsolete("Use SoftTarget Property", false)]
public void SetSoftTarget(GameObject? actor) => this.SetTarget(actor?.Address ?? IntPtr.Zero);
/// <summary>
/// Sets the current target.
/// </summary>
/// <param name="actorAddress">Actor (address) to target.</param>
[Obsolete("Use Target Property", false)]
public void SetTarget(IntPtr actorAddress) => Struct->Target = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)actorAddress;
/// <summary>
/// Sets the mouseover target.
/// </summary>
/// <param name="actorAddress">Actor (address) to target.</param>
[Obsolete("Use MouseOverTarget Property", false)]
public void SetMouseOverTarget(IntPtr actorAddress) => Struct->MouseOverTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)actorAddress;
/// <summary>
/// Sets the focus target.
/// </summary>
/// <param name="actorAddress">Actor (address) to target.</param>
[Obsolete("Use FocusTarget Property", false)]
public void SetFocusTarget(IntPtr actorAddress) => Struct->FocusTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)actorAddress;
/// <summary>
/// Sets the previous target.
/// </summary>
/// <param name="actorAddress">Actor (address) to target.</param>
[Obsolete("Use PreviousTarget Property", false)]
public void SetPreviousTarget(IntPtr actorAddress) => Struct->PreviousTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)actorAddress;
/// <summary>
/// Sets the soft target.
/// </summary>
/// <param name="actorAddress">Actor (address) to target.</param>
[Obsolete("Use SoftTarget Property", false)]
public void SetSoftTarget(IntPtr actorAddress) => Struct->SoftTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)actorAddress;
/// <summary>
/// Clears the current target.
/// </summary>
[Obsolete("Use Target Property", false)]
public void ClearTarget() => this.SetTarget(IntPtr.Zero);
/// <summary>
/// Clears the mouseover target.
/// </summary>
[Obsolete("Use MouseOverTarget Property", false)]
public void ClearMouseOverTarget() => this.SetMouseOverTarget(IntPtr.Zero);
/// <summary>
/// Clears the focus target.
/// </summary>
[Obsolete("Use FocusTarget Property", false)]
public void ClearFocusTarget() => this.SetFocusTarget(IntPtr.Zero);
/// <summary>
/// Clears the previous target.
/// </summary>
[Obsolete("Use PreviousTarget Property", false)]
public void ClearPreviousTarget() => this.SetPreviousTarget(IntPtr.Zero);
/// <summary>
/// Clears the soft target.
/// </summary>
[Obsolete("Use SoftTarget Property", false)]
public void ClearSoftTarget() => this.SetSoftTarget(IntPtr.Zero);
} }

View file

@ -1,6 +1,7 @@
using System; using System;
using Dalamud.Game.ClientState.Statuses; using Dalamud.Game.ClientState.Statuses;
using Dalamud.Utility;
namespace Dalamud.Game.ClientState.Objects.Types; namespace Dalamud.Game.ClientState.Objects.Types;
@ -57,8 +58,22 @@ public unsafe class BattleChara : Character
/// <summary> /// <summary>
/// Gets the total casting time of the spell being cast by the chara. /// Gets the total casting time of the spell being cast by the chara.
/// </summary> /// </summary>
/// <remarks>
/// This can only be a portion of the total cast for some actions.
/// Use AdjustedTotalCastTime if you always need the total cast time.
/// </remarks>
[Api10ToDo("Rename so it is not confused with AdjustedTotalCastTime")]
public float TotalCastTime => this.Struct->GetCastInfo->TotalCastTime; public float TotalCastTime => this.Struct->GetCastInfo->TotalCastTime;
/// <summary>
/// Gets the <see cref="TotalCastTime"/> plus any adjustments from the game, such as Action offset 2B. Used for display purposes.
/// </summary>
/// <remarks>
/// This is the actual total cast time for all actions.
/// </remarks>
[Api10ToDo("Rename so it is not confused with TotalCastTime")]
public float AdjustedTotalCastTime => this.Struct->GetCastInfo->AdjustedTotalCastTime;
/// <summary> /// <summary>
/// Gets the underlying structure. /// Gets the underlying structure.
/// </summary> /// </summary>

View file

@ -1,5 +1,3 @@
using System;
using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Game.ClientState.Resolvers; using Dalamud.Game.ClientState.Resolvers;
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
@ -63,6 +61,11 @@ public unsafe class Character : GameObject
/// </summary> /// </summary>
public uint MaxCp => this.Struct->CharacterData.MaxCraftingPoints; public uint MaxCp => this.Struct->CharacterData.MaxCraftingPoints;
/// <summary>
/// Gets the shield percentage of this Chara.
/// </summary>
public byte ShieldPercentage => this.Struct->CharacterData.ShieldValue;
/// <summary> /// <summary>
/// Gets the ClassJob of this Chara. /// Gets the ClassJob of this Chara.
/// </summary> /// </summary>
@ -87,7 +90,7 @@ public unsafe class Character : GameObject
/// <summary> /// <summary>
/// Gets the target object ID of the character. /// Gets the target object ID of the character.
/// </summary> /// </summary>
public override ulong TargetObjectId => this.Struct->TargetObjectID; public override ulong TargetObjectId => this.Struct->TargetId;
/// <summary> /// <summary>
/// Gets the name ID of the character. /// Gets the name ID of the character.
@ -115,5 +118,6 @@ public unsafe class Character : GameObject
/// <summary> /// <summary>
/// Gets the underlying structure. /// Gets the underlying structure.
/// </summary> /// </summary>
protected internal new FFXIVClientStructs.FFXIV.Client.Game.Character.Character* Struct => (FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)this.Address; protected internal new FFXIVClientStructs.FFXIV.Client.Game.Character.Character* Struct =>
(FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)this.Address;
} }

View file

@ -19,7 +19,7 @@ namespace Dalamud.Game.ClientState.Party;
#pragma warning disable SA1015 #pragma warning disable SA1015
[ResolveVia<IPartyList>] [ResolveVia<IPartyList>]
#pragma warning restore SA1015 #pragma warning restore SA1015
public sealed unsafe partial class PartyList : IServiceType, IPartyList internal sealed unsafe partial class PartyList : IServiceType, IPartyList
{ {
private const int GroupLength = 8; private const int GroupLength = 8;
private const int AllianceLength = 20; private const int AllianceLength = 20;
@ -130,7 +130,7 @@ public sealed unsafe partial class PartyList : IServiceType, IPartyList
/// <summary> /// <summary>
/// This collection represents the party members present in your party or alliance. /// This collection represents the party members present in your party or alliance.
/// </summary> /// </summary>
public sealed partial class PartyList internal sealed partial class PartyList
{ {
/// <inheritdoc/> /// <inheritdoc/>
int IReadOnlyCollection<PartyMember>.Count => this.Length; int IReadOnlyCollection<PartyMember>.Count => this.Length;

View file

@ -10,8 +10,6 @@ namespace Dalamud.Game.ClientState.Statuses;
/// </summary> /// </summary>
public sealed unsafe partial class StatusList public sealed unsafe partial class StatusList
{ {
private const int StatusListLength = 30;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="StatusList"/> class. /// Initializes a new instance of the <see cref="StatusList"/> class.
/// </summary> /// </summary>
@ -38,7 +36,7 @@ public sealed unsafe partial class StatusList
/// <summary> /// <summary>
/// Gets the amount of status effect slots the actor has. /// Gets the amount of status effect slots the actor has.
/// </summary> /// </summary>
public int Length => StatusListLength; public int Length => Struct->NumValidStatuses;
private static int StatusSize { get; } = Marshal.SizeOf<FFXIVClientStructs.FFXIV.Client.Game.Status>(); private static int StatusSize { get; } = Marshal.SizeOf<FFXIVClientStructs.FFXIV.Client.Game.Status>();
@ -53,7 +51,7 @@ public sealed unsafe partial class StatusList
{ {
get get
{ {
if (index < 0 || index > StatusListLength) if (index < 0 || index > this.Length)
return null; return null;
var addr = this.GetStatusAddress(index); var addr = this.GetStatusAddress(index);
@ -107,7 +105,7 @@ public sealed unsafe partial class StatusList
/// <returns>The memory address of the party member.</returns> /// <returns>The memory address of the party member.</returns>
public IntPtr GetStatusAddress(int index) public IntPtr GetStatusAddress(int index)
{ {
if (index < 0 || index >= StatusListLength) if (index < 0 || index >= this.Length)
return IntPtr.Zero; return IntPtr.Zero;
return (IntPtr)(this.Struct->Status + (index * StatusSize)); return (IntPtr)(this.Struct->Status + (index * StatusSize));
@ -134,7 +132,7 @@ public sealed partial class StatusList : IReadOnlyCollection<Status>, ICollectio
/// <inheritdoc/> /// <inheritdoc/>
public IEnumerator<Status> GetEnumerator() public IEnumerator<Status> GetEnumerator()
{ {
for (var i = 0; i < StatusListLength; i++) for (var i = 0; i < this.Length; i++)
{ {
var status = this[i]; var status = this[i];

View file

@ -15,7 +15,6 @@ public sealed class CommandInfo
public CommandInfo(HandlerDelegate handler) public CommandInfo(HandlerDelegate handler)
{ {
this.Handler = handler; this.Handler = handler;
this.LoaderAssemblyName = Assembly.GetCallingAssembly()?.GetName()?.Name;
} }
/// <summary> /// <summary>

View file

@ -1,4 +1,3 @@
using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
@ -9,22 +8,21 @@ using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
using Dalamud.IoC; using Dalamud.IoC;
using Dalamud.IoC.Internal; using Dalamud.IoC.Internal;
using Dalamud.Logging.Internal;
using Dalamud.Plugin.Internal.Types;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Serilog;
namespace Dalamud.Game.Command; namespace Dalamud.Game.Command;
/// <summary> /// <summary>
/// This class manages registered in-game slash commands. /// This class manages registered in-game slash commands.
/// </summary> /// </summary>
[PluginInterface]
[InterfaceVersion("1.0")] [InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService] [ServiceManager.BlockingEarlyLoadedService]
#pragma warning disable SA1015 internal sealed class CommandManager : IInternalDisposableService, ICommandManager
[ResolveVia<ICommandManager>]
#pragma warning restore SA1015
public sealed class CommandManager : IServiceType, IDisposable, ICommandManager
{ {
private static readonly ModuleLog Log = new("Command");
private readonly ConcurrentDictionary<string, CommandInfo> commandMap = new(); private readonly ConcurrentDictionary<string, CommandInfo> commandMap = new();
private readonly Regex commandRegexEn = new(@"^The command (?<command>.+) does not exist\.$", RegexOptions.Compiled); private readonly Regex commandRegexEn = new(@"^The command (?<command>.+) does not exist\.$", RegexOptions.Compiled);
private readonly Regex commandRegexJp = new(@"^そのコマンドはありません。: (?<command>.+)$", RegexOptions.Compiled); private readonly Regex commandRegexJp = new(@"^そのコマンドはありません。: (?<command>.+)$", RegexOptions.Compiled);
@ -37,15 +35,15 @@ public sealed class CommandManager : IServiceType, IDisposable, ICommandManager
private readonly ChatGui chatGui = Service<ChatGui>.Get(); private readonly ChatGui chatGui = Service<ChatGui>.Get();
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
private CommandManager(DalamudStartInfo startInfo) private CommandManager(Dalamud dalamud)
{ {
this.currentLangCommandRegex = startInfo.Language switch this.currentLangCommandRegex = (ClientLanguage)dalamud.StartInfo.Language switch
{ {
ClientLanguage.Japanese => this.commandRegexJp, ClientLanguage.Japanese => this.commandRegexJp,
ClientLanguage.English => this.commandRegexEn, ClientLanguage.English => this.commandRegexEn,
ClientLanguage.German => this.commandRegexDe, ClientLanguage.German => this.commandRegexDe,
ClientLanguage.French => this.commandRegexFr, ClientLanguage.French => this.commandRegexFr,
_ => this.currentLangCommandRegex, _ => this.commandRegexEn,
}; };
this.chatGui.CheckMessageHandled += this.OnCheckMessageHandled; this.chatGui.CheckMessageHandled += this.OnCheckMessageHandled;
@ -84,7 +82,7 @@ public sealed class CommandManager : IServiceType, IDisposable, ICommandManager
// => command: 0-12 (12 chars) // => command: 0-12 (12 chars)
// => argument: 13-17 (4 chars) // => argument: 13-17 (4 chars)
// => content.IndexOf(' ') == 12 // => content.IndexOf(' ') == 12
command = content.Substring(0, separatorPosition); command = content[..separatorPosition];
var argStart = separatorPosition + 1; var argStart = separatorPosition + 1;
argument = content[argStart..]; argument = content[argStart..];
@ -132,7 +130,7 @@ public sealed class CommandManager : IServiceType, IDisposable, ICommandManager
} }
/// <inheritdoc/> /// <inheritdoc/>
void IDisposable.Dispose() void IInternalDisposableService.DisposeService()
{ {
this.chatGui.CheckMessageHandled -= this.OnCheckMessageHandled; this.chatGui.CheckMessageHandled -= this.OnCheckMessageHandled;
} }
@ -162,3 +160,93 @@ public sealed class CommandManager : IServiceType, IDisposable, ICommandManager
} }
} }
} }
/// <summary>
/// Plugin-scoped version of a AddonLifecycle service.
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.ScopedService]
#pragma warning disable SA1015
[ResolveVia<ICommandManager>]
#pragma warning restore SA1015
internal class CommandManagerPluginScoped : IInternalDisposableService, ICommandManager
{
private static readonly ModuleLog Log = new("Command");
[ServiceManager.ServiceDependency]
private readonly CommandManager commandManagerService = Service<CommandManager>.Get();
private readonly List<string> pluginRegisteredCommands = new();
private readonly LocalPlugin pluginInfo;
/// <summary>
/// Initializes a new instance of the <see cref="CommandManagerPluginScoped"/> class.
/// </summary>
/// <param name="localPlugin">Info for the plugin that requests this service.</param>
public CommandManagerPluginScoped(LocalPlugin localPlugin)
{
this.pluginInfo = localPlugin;
}
/// <inheritdoc/>
public ReadOnlyDictionary<string, CommandInfo> Commands => this.commandManagerService.Commands;
/// <inheritdoc/>
void IInternalDisposableService.DisposeService()
{
foreach (var command in this.pluginRegisteredCommands)
{
this.commandManagerService.RemoveHandler(command);
}
this.pluginRegisteredCommands.Clear();
}
/// <inheritdoc/>
public bool ProcessCommand(string content)
=> this.commandManagerService.ProcessCommand(content);
/// <inheritdoc/>
public void DispatchCommand(string command, string argument, CommandInfo info)
=> this.commandManagerService.DispatchCommand(command, argument, info);
/// <inheritdoc/>
public bool AddHandler(string command, CommandInfo info)
{
if (!this.pluginRegisteredCommands.Contains(command))
{
info.LoaderAssemblyName = this.pluginInfo.InternalName;
if (this.commandManagerService.AddHandler(command, info))
{
this.pluginRegisteredCommands.Add(command);
return true;
}
}
else
{
Log.Error($"Command {command} is already registered.");
}
return false;
}
/// <inheritdoc/>
public bool RemoveHandler(string command)
{
if (this.pluginRegisteredCommands.Contains(command))
{
if (this.commandManagerService.RemoveHandler(command))
{
this.pluginRegisteredCommands.Remove(command);
return true;
}
}
else
{
Log.Error($"Command {command} not found.");
}
return false;
}
}

View file

@ -1,4 +1,5 @@
using System; using System.Threading.Tasks;
using Dalamud.Hooking; using Dalamud.Hooking;
using Dalamud.IoC; using Dalamud.IoC;
using Dalamud.IoC.Internal; using Dalamud.IoC.Internal;
@ -13,47 +14,86 @@ namespace Dalamud.Game.Config;
/// This class represents the game's configuration. /// This class represents the game's configuration.
/// </summary> /// </summary>
[InterfaceVersion("1.0")] [InterfaceVersion("1.0")]
[PluginInterface] [ServiceManager.BlockingEarlyLoadedService]
[ServiceManager.EarlyLoadedService] internal sealed class GameConfig : IInternalDisposableService, IGameConfig
#pragma warning disable SA1015
[ResolveVia<IGameConfig>]
#pragma warning restore SA1015
public sealed class GameConfig : IServiceType, IGameConfig, IDisposable
{ {
private readonly TaskCompletionSource tcsInitialization = new();
private readonly TaskCompletionSource<GameConfigSection> tcsSystem = new();
private readonly TaskCompletionSource<GameConfigSection> tcsUiConfig = new();
private readonly TaskCompletionSource<GameConfigSection> tcsUiControl = new();
private readonly GameConfigAddressResolver address = new(); private readonly GameConfigAddressResolver address = new();
private Hook<ConfigChangeDelegate>? configChangeHook; private Hook<ConfigChangeDelegate>? configChangeHook;
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
private unsafe GameConfig(Framework framework, SigScanner sigScanner) private unsafe GameConfig(Framework framework, TargetSigScanner sigScanner)
{ {
framework.RunOnTick(() => framework.RunOnTick(() =>
{ {
Log.Verbose("[GameConfig] Initializing"); try
var csFramework = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework.Instance(); {
var commonConfig = &csFramework->SystemConfig.CommonSystemConfig; Log.Verbose("[GameConfig] Initializing");
this.System = new GameConfigSection("System", framework, &commonConfig->ConfigBase); var csFramework = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework.Instance();
this.UiConfig = new GameConfigSection("UiConfig", framework, &commonConfig->UiConfig); var commonConfig = &csFramework->SystemConfig.CommonSystemConfig;
this.UiControl = new GameConfigSection("UiControl", framework, () => this.UiConfig.TryGetBool("PadMode", out var padMode) && padMode ? &commonConfig->UiControlGamepadConfig : &commonConfig->UiControlConfig); this.tcsSystem.SetResult(new("System", framework, &commonConfig->ConfigBase));
this.tcsUiConfig.SetResult(new("UiConfig", framework, &commonConfig->UiConfig));
this.address.Setup(sigScanner); this.tcsUiControl.SetResult(
this.configChangeHook = Hook<ConfigChangeDelegate>.FromAddress(this.address.ConfigChangeAddress, this.OnConfigChanged); new(
this.configChangeHook?.Enable(); "UiControl",
framework,
() => this.UiConfig.TryGetBool("PadMode", out var padMode) && padMode
? &commonConfig->UiControlGamepadConfig
: &commonConfig->UiControlConfig));
this.address.Setup(sigScanner);
this.configChangeHook = Hook<ConfigChangeDelegate>.FromAddress(
this.address.ConfigChangeAddress,
this.OnConfigChanged);
this.configChangeHook.Enable();
this.tcsInitialization.SetResult();
}
catch (Exception ex)
{
this.tcsInitialization.SetExceptionIfIncomplete(ex);
}
}); });
} }
private unsafe delegate nint ConfigChangeDelegate(ConfigBase* configBase, ConfigEntry* configEntry); private unsafe delegate nint ConfigChangeDelegate(ConfigBase* configBase, ConfigEntry* configEntry);
/// <inheritdoc/> /// <inheritdoc/>
public event EventHandler<ConfigChangeEvent> Changed; public event EventHandler<ConfigChangeEvent>? Changed;
#pragma warning disable 67
/// <summary>
/// Unused internally, used as a proxy for System.Changed via GameConfigPluginScoped
/// </summary>
public event EventHandler<ConfigChangeEvent>? SystemChanged;
/// <inheritdoc/> /// <summary>
public GameConfigSection System { get; private set; } /// Unused internally, used as a proxy for UiConfig.Changed via GameConfigPluginScoped
/// </summary>
public event EventHandler<ConfigChangeEvent>? UiConfigChanged;
/// <summary>
/// Unused internally, used as a proxy for UiControl.Changed via GameConfigPluginScoped
/// </summary>
public event EventHandler<ConfigChangeEvent>? UiControlChanged;
#pragma warning restore 67
/// <summary>
/// Gets a task representing the initialization state of this class.
/// </summary>
public Task InitializationTask => this.tcsInitialization.Task;
/// <inheritdoc/> /// <inheritdoc/>
public GameConfigSection UiConfig { get; private set; } public GameConfigSection System => this.tcsSystem.Task.Result;
/// <inheritdoc/> /// <inheritdoc/>
public GameConfigSection UiControl { get; private set; } public GameConfigSection UiConfig => this.tcsUiConfig.Task.Result;
/// <inheritdoc/>
public GameConfigSection UiControl => this.tcsUiControl.Task.Result;
/// <inheritdoc/> /// <inheritdoc/>
public bool TryGet(SystemConfigOption option, out bool value) => this.System.TryGet(option.GetName(), out value); public bool TryGet(SystemConfigOption option, out bool value) => this.System.TryGet(option.GetName(), out value);
@ -155,8 +195,13 @@ public sealed class GameConfig : IServiceType, IGameConfig, IDisposable
public void Set(UiControlOption option, string value) => this.UiControl.Set(option.GetName(), value); public void Set(UiControlOption option, string value) => this.UiControl.Set(option.GetName(), value);
/// <inheritdoc/> /// <inheritdoc/>
void IDisposable.Dispose() void IInternalDisposableService.DisposeService()
{ {
var ode = new ObjectDisposedException(nameof(GameConfig));
this.tcsInitialization.SetExceptionIfIncomplete(ode);
this.tcsSystem.SetExceptionIfIncomplete(ode);
this.tcsUiConfig.SetExceptionIfIncomplete(ode);
this.tcsUiControl.SetExceptionIfIncomplete(ode);
this.configChangeHook?.Disable(); this.configChangeHook?.Disable();
this.configChangeHook?.Dispose(); this.configChangeHook?.Dispose();
} }
@ -193,3 +238,219 @@ public sealed class GameConfig : IServiceType, IGameConfig, IDisposable
return returnValue; return returnValue;
} }
} }
/// <summary>
/// Plugin-scoped version of a GameConfig service.
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.ScopedService]
#pragma warning disable SA1015
[ResolveVia<IGameConfig>]
#pragma warning restore SA1015
internal class GameConfigPluginScoped : IInternalDisposableService, IGameConfig
{
[ServiceManager.ServiceDependency]
private readonly GameConfig gameConfigService = Service<GameConfig>.Get();
private readonly Task initializationTask;
/// <summary>
/// Initializes a new instance of the <see cref="GameConfigPluginScoped"/> class.
/// </summary>
internal GameConfigPluginScoped()
{
this.gameConfigService.Changed += this.ConfigChangedForward;
this.initializationTask = this.gameConfigService.InitializationTask.ContinueWith(
r =>
{
if (!r.IsCompletedSuccessfully)
return r;
this.gameConfigService.System.Changed += this.SystemConfigChangedForward;
this.gameConfigService.UiConfig.Changed += this.UiConfigConfigChangedForward;
this.gameConfigService.UiControl.Changed += this.UiControlConfigChangedForward;
return Task.CompletedTask;
}).Unwrap();
}
/// <inheritdoc/>
public event EventHandler<ConfigChangeEvent>? Changed;
/// <inheritdoc/>
public event EventHandler<ConfigChangeEvent>? SystemChanged;
/// <inheritdoc/>
public event EventHandler<ConfigChangeEvent>? UiConfigChanged;
/// <inheritdoc/>
public event EventHandler<ConfigChangeEvent>? UiControlChanged;
/// <inheritdoc/>
public GameConfigSection System => this.gameConfigService.System;
/// <inheritdoc/>
public GameConfigSection UiConfig => this.gameConfigService.UiConfig;
/// <inheritdoc/>
public GameConfigSection UiControl => this.gameConfigService.UiControl;
/// <inheritdoc/>
void IInternalDisposableService.DisposeService()
{
this.gameConfigService.Changed -= this.ConfigChangedForward;
this.initializationTask.ContinueWith(
r =>
{
if (!r.IsCompletedSuccessfully)
return;
this.gameConfigService.System.Changed -= this.SystemConfigChangedForward;
this.gameConfigService.UiConfig.Changed -= this.UiConfigConfigChangedForward;
this.gameConfigService.UiControl.Changed -= this.UiControlConfigChangedForward;
});
this.Changed = null;
this.SystemChanged = null;
this.UiConfigChanged = null;
this.UiControlChanged = null;
}
/// <inheritdoc/>
public bool TryGet(SystemConfigOption option, out bool value)
=> this.gameConfigService.TryGet(option, out value);
/// <inheritdoc/>
public bool TryGet(SystemConfigOption option, out uint value)
=> this.gameConfigService.TryGet(option, out value);
/// <inheritdoc/>
public bool TryGet(SystemConfigOption option, out float value)
=> this.gameConfigService.TryGet(option, out value);
/// <inheritdoc/>
public bool TryGet(SystemConfigOption option, out string value)
=> this.gameConfigService.TryGet(option, out value);
/// <inheritdoc/>
public bool TryGet(SystemConfigOption option, out UIntConfigProperties? properties)
=> this.gameConfigService.TryGet(option, out properties);
/// <inheritdoc/>
public bool TryGet(SystemConfigOption option, out FloatConfigProperties? properties)
=> this.gameConfigService.TryGet(option, out properties);
/// <inheritdoc/>
public bool TryGet(SystemConfigOption option, out StringConfigProperties? properties)
=> this.gameConfigService.TryGet(option, out properties);
/// <inheritdoc/>
public bool TryGet(UiConfigOption option, out bool value)
=> this.gameConfigService.TryGet(option, out value);
/// <inheritdoc/>
public bool TryGet(UiConfigOption option, out uint value)
=> this.gameConfigService.TryGet(option, out value);
/// <inheritdoc/>
public bool TryGet(UiConfigOption option, out float value)
=> this.gameConfigService.TryGet(option, out value);
/// <inheritdoc/>
public bool TryGet(UiConfigOption option, out string value)
=> this.gameConfigService.TryGet(option, out value);
/// <inheritdoc/>
public bool TryGet(UiConfigOption option, out UIntConfigProperties? properties)
=> this.gameConfigService.TryGet(option, out properties);
/// <inheritdoc/>
public bool TryGet(UiConfigOption option, out FloatConfigProperties? properties)
=> this.gameConfigService.TryGet(option, out properties);
/// <inheritdoc/>
public bool TryGet(UiConfigOption option, out StringConfigProperties? properties)
=> this.gameConfigService.TryGet(option, out properties);
/// <inheritdoc/>
public bool TryGet(UiControlOption option, out bool value)
=> this.gameConfigService.TryGet(option, out value);
/// <inheritdoc/>
public bool TryGet(UiControlOption option, out uint value)
=> this.gameConfigService.TryGet(option, out value);
/// <inheritdoc/>
public bool TryGet(UiControlOption option, out float value)
=> this.gameConfigService.TryGet(option, out value);
/// <inheritdoc/>
public bool TryGet(UiControlOption option, out string value)
=> this.gameConfigService.TryGet(option, out value);
/// <inheritdoc/>
public bool TryGet(UiControlOption option, out UIntConfigProperties? properties)
=> this.gameConfigService.TryGet(option, out properties);
/// <inheritdoc/>
public bool TryGet(UiControlOption option, out FloatConfigProperties? properties)
=> this.gameConfigService.TryGet(option, out properties);
/// <inheritdoc/>
public bool TryGet(UiControlOption option, out StringConfigProperties? properties)
=> this.gameConfigService.TryGet(option, out properties);
/// <inheritdoc/>
public void Set(SystemConfigOption option, bool value)
=> this.gameConfigService.Set(option, value);
/// <inheritdoc/>
public void Set(SystemConfigOption option, uint value)
=> this.gameConfigService.Set(option, value);
/// <inheritdoc/>
public void Set(SystemConfigOption option, float value)
=> this.gameConfigService.Set(option, value);
/// <inheritdoc/>
public void Set(SystemConfigOption option, string value)
=> this.gameConfigService.Set(option, value);
/// <inheritdoc/>
public void Set(UiConfigOption option, bool value)
=> this.gameConfigService.Set(option, value);
/// <inheritdoc/>
public void Set(UiConfigOption option, uint value)
=> this.gameConfigService.Set(option, value);
/// <inheritdoc/>
public void Set(UiConfigOption option, float value)
=> this.gameConfigService.Set(option, value);
/// <inheritdoc/>
public void Set(UiConfigOption option, string value)
=> this.gameConfigService.Set(option, value);
/// <inheritdoc/>
public void Set(UiControlOption option, bool value)
=> this.gameConfigService.Set(option, value);
/// <inheritdoc/>
public void Set(UiControlOption option, uint value)
=> this.gameConfigService.Set(option, value);
/// <inheritdoc/>
public void Set(UiControlOption option, float value)
=> this.gameConfigService.Set(option, value);
/// <inheritdoc/>
public void Set(UiControlOption option, string value)
=> this.gameConfigService.Set(option, value);
private void ConfigChangedForward(object sender, ConfigChangeEvent data) => this.Changed?.Invoke(sender, data);
private void SystemConfigChangedForward(object sender, ConfigChangeEvent data) => this.SystemChanged?.Invoke(sender, data);
private void UiConfigConfigChangedForward(object sender, ConfigChangeEvent data) => this.UiConfigChanged?.Invoke(sender, data);
private void UiControlConfigChangedForward(object sender, ConfigChangeEvent data) => this.UiControlChanged?.Invoke(sender, data);
}

View file

@ -3,7 +3,7 @@
/// <summary> /// <summary>
/// Game config system address resolver. /// Game config system address resolver.
/// </summary> /// </summary>
public sealed class GameConfigAddressResolver : BaseAddressResolver internal sealed class GameConfigAddressResolver : BaseAddressResolver
{ {
/// <summary> /// <summary>
/// Gets the address of the method called when any config option is changed. /// Gets the address of the method called when any config option is changed.
@ -11,7 +11,7 @@ public sealed class GameConfigAddressResolver : BaseAddressResolver
public nint ConfigChangeAddress { get; private set; } public nint ConfigChangeAddress { get; private set; }
/// <inheritdoc/> /// <inheritdoc/>
protected override void Setup64Bit(SigScanner scanner) protected override void Setup64Bit(ISigScanner scanner)
{ {
this.ConfigChangeAddress = scanner.ScanText("E8 ?? ?? ?? ?? 48 8B 3F 49 3B 3E"); this.ConfigChangeAddress = scanner.ScanText("E8 ?? ?? ?? ?? 48 8B 3F 49 3B 3E");
} }

View file

@ -1,5 +1,4 @@
using System; using System.Collections.Concurrent;
using System.Collections.Concurrent;
using System.Diagnostics; using System.Diagnostics;
using Dalamud.Memory; using Dalamud.Memory;
@ -18,11 +17,6 @@ public class GameConfigSection
private readonly ConcurrentDictionary<string, uint> indexMap = new(); private readonly ConcurrentDictionary<string, uint> indexMap = new();
private readonly ConcurrentDictionary<uint, object> enumMap = new(); private readonly ConcurrentDictionary<uint, object> enumMap = new();
/// <summary>
/// Event which is fired when a game config option is changed within the section.
/// </summary>
public event EventHandler<ConfigChangeEvent> Changed;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="GameConfigSection"/> class. /// Initializes a new instance of the <see cref="GameConfigSection"/> class.
/// </summary> /// </summary>
@ -54,6 +48,11 @@ public class GameConfigSection
/// <returns>Pointer to unmanaged ConfigBase.</returns> /// <returns>Pointer to unmanaged ConfigBase.</returns>
internal unsafe delegate ConfigBase* GetConfigBaseDelegate(); internal unsafe delegate ConfigBase* GetConfigBaseDelegate();
/// <summary>
/// Event which is fired when a game config option is changed within the section.
/// </summary>
internal event EventHandler<ConfigChangeEvent>? Changed;
/// <summary> /// <summary>
/// Gets the number of config entries contained within the section. /// Gets the number of config entries contained within the section.
/// Some entries may be empty with no data. /// Some entries may be empty with no data.

View file

@ -3473,4 +3473,67 @@ public enum UiConfigOption
/// </summary> /// </summary>
[GameConfigOption("ItemInventryStoreEnd", ConfigType.UInt)] [GameConfigOption("ItemInventryStoreEnd", ConfigType.UInt)]
ItemInventryStoreEnd, ItemInventryStoreEnd,
/// <summary>
/// System option with the internal name HotbarXHBEditEnable.
/// This option is a UInt.
/// </summary>
[GameConfigOption("HotbarXHBEditEnable", ConfigType.UInt)]
HotbarXHBEditEnable,
/// <summary>
/// System option with the internal name NamePlateDispJobIconInPublicParty.
/// This option is a UInt.
/// </summary>
[GameConfigOption("NamePlateDispJobIconInPublicParty", ConfigType.UInt)]
NamePlateDispJobIconInPublicParty,
/// <summary>
/// System option with the internal name NamePlateDispJobIconInPublicOther.
/// This option is a UInt.
/// </summary>
[GameConfigOption("NamePlateDispJobIconInPublicOther", ConfigType.UInt)]
NamePlateDispJobIconInPublicOther,
/// <summary>
/// System option with the internal name NamePlateDispJobIconInInstanceParty.
/// This option is a UInt.
/// </summary>
[GameConfigOption("NamePlateDispJobIconInInstanceParty", ConfigType.UInt)]
NamePlateDispJobIconInInstanceParty,
/// <summary>
/// System option with the internal name NamePlateDispJobIconInInstanceOther.
/// This option is a UInt.
/// </summary>
[GameConfigOption("NamePlateDispJobIconInInstanceOther", ConfigType.UInt)]
NamePlateDispJobIconInInstanceOther,
/// <summary>
/// System option with the internal name CCProgressAllyFixLeftSide.
/// This option is a UInt.
/// </summary>
[GameConfigOption("CCProgressAllyFixLeftSide", ConfigType.UInt)]
CCProgressAllyFixLeftSide,
/// <summary>
/// System option with the internal name CCMapAllyFixLeftSide.
/// This option is a UInt.
/// </summary>
[GameConfigOption("CCMapAllyFixLeftSide", ConfigType.UInt)]
CCMapAllyFixLeftSide,
/// <summary>
/// System option with the internal name DispCCCountDown.
/// This option is a UInt.
/// </summary>
[GameConfigOption("DispCCCountDown", ConfigType.UInt)]
DispCCCountDown,
/// <summary>
/// System option with the internal name TelepoCategoryType.
/// This option is a UInt.
/// </summary>
[GameConfigOption("TelepoCategoryType", ConfigType.UInt)]
TelepoCategoryType,
} }

View file

@ -1,25 +1,19 @@
using System; using System.Runtime.InteropServices;
using System.Runtime.InteropServices;
using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Conditions;
using Dalamud.Hooking; using Dalamud.Hooking;
using Dalamud.IoC; using Dalamud.IoC;
using Dalamud.IoC.Internal; using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Dalamud.Utility;
namespace Dalamud.Game.DutyState; namespace Dalamud.Game.DutyState;
/// <summary> /// <summary>
/// This class represents the state of the currently occupied duty. /// This class represents the state of the currently occupied duty.
/// </summary> /// </summary>
[PluginInterface]
[InterfaceVersion("1.0")] [InterfaceVersion("1.0")]
[ServiceManager.EarlyLoadedService] [ServiceManager.BlockingEarlyLoadedService]
#pragma warning disable SA1015 internal unsafe class DutyState : IInternalDisposableService, IDutyState
[ResolveVia<IDutyState>]
#pragma warning restore SA1015
public unsafe class DutyState : IDisposable, IServiceType, IDutyState
{ {
private readonly DutyStateAddressResolver address; private readonly DutyStateAddressResolver address;
private readonly Hook<SetupContentDirectNetworkMessageDelegate> contentDirectorNetworkMessageHook; private readonly Hook<SetupContentDirectNetworkMessageDelegate> contentDirectorNetworkMessageHook;
@ -34,7 +28,7 @@ public unsafe class DutyState : IDisposable, IServiceType, IDutyState
private readonly ClientState.ClientState clientState = Service<ClientState.ClientState>.Get(); private readonly ClientState.ClientState clientState = Service<ClientState.ClientState>.Get();
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
private DutyState(SigScanner sigScanner) private DutyState(TargetSigScanner sigScanner)
{ {
this.address = new DutyStateAddressResolver(); this.address = new DutyStateAddressResolver();
this.address.Setup(sigScanner); this.address.Setup(sigScanner);
@ -43,22 +37,24 @@ public unsafe class DutyState : IDisposable, IServiceType, IDutyState
this.framework.Update += this.FrameworkOnUpdateEvent; this.framework.Update += this.FrameworkOnUpdateEvent;
this.clientState.TerritoryChanged += this.TerritoryOnChangedEvent; this.clientState.TerritoryChanged += this.TerritoryOnChangedEvent;
this.contentDirectorNetworkMessageHook.Enable();
} }
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] [UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate byte SetupContentDirectNetworkMessageDelegate(IntPtr a1, IntPtr a2, ushort* a3); private delegate byte SetupContentDirectNetworkMessageDelegate(IntPtr a1, IntPtr a2, ushort* a3);
/// <inheritdoc/> /// <inheritdoc/>
public event EventHandler<ushort> DutyStarted; public event EventHandler<ushort>? DutyStarted;
/// <inheritdoc/> /// <inheritdoc/>
public event EventHandler<ushort> DutyWiped; public event EventHandler<ushort>? DutyWiped;
/// <inheritdoc/> /// <inheritdoc/>
public event EventHandler<ushort> DutyRecommenced; public event EventHandler<ushort>? DutyRecommenced;
/// <inheritdoc/> /// <inheritdoc/>
public event EventHandler<ushort> DutyCompleted; public event EventHandler<ushort>? DutyCompleted;
/// <inheritdoc/> /// <inheritdoc/>
public bool IsDutyStarted { get; private set; } public bool IsDutyStarted { get; private set; }
@ -66,19 +62,13 @@ public unsafe class DutyState : IDisposable, IServiceType, IDutyState
private bool CompletedThisTerritory { get; set; } private bool CompletedThisTerritory { get; set; }
/// <inheritdoc/> /// <inheritdoc/>
void IDisposable.Dispose() void IInternalDisposableService.DisposeService()
{ {
this.contentDirectorNetworkMessageHook.Dispose(); this.contentDirectorNetworkMessageHook.Dispose();
this.framework.Update -= this.FrameworkOnUpdateEvent; this.framework.Update -= this.FrameworkOnUpdateEvent;
this.clientState.TerritoryChanged -= this.TerritoryOnChangedEvent; this.clientState.TerritoryChanged -= this.TerritoryOnChangedEvent;
} }
[ServiceManager.CallWhenServicesReady]
private void ContinueConstruction()
{
this.contentDirectorNetworkMessageHook.Enable();
}
private byte ContentDirectorNetworkMessageDetour(IntPtr a1, IntPtr a2, ushort* a3) private byte ContentDirectorNetworkMessageDetour(IntPtr a1, IntPtr a2, ushort* a3)
{ {
var category = *a3; var category = *a3;
@ -92,33 +82,33 @@ public unsafe class DutyState : IDisposable, IServiceType, IDutyState
// Duty Commenced // Duty Commenced
case 0x4000_0001: case 0x4000_0001:
this.IsDutyStarted = true; this.IsDutyStarted = true;
this.DutyStarted.InvokeSafely(this, this.clientState.TerritoryType); this.DutyStarted?.Invoke(this, this.clientState.TerritoryType);
break; break;
// Party Wipe // Party Wipe
case 0x4000_0005: case 0x4000_0005:
this.IsDutyStarted = false; this.IsDutyStarted = false;
this.DutyWiped.InvokeSafely(this, this.clientState.TerritoryType); this.DutyWiped?.Invoke(this, this.clientState.TerritoryType);
break; break;
// Duty Recommence // Duty Recommence
case 0x4000_0006: case 0x4000_0006:
this.IsDutyStarted = true; this.IsDutyStarted = true;
this.DutyRecommenced.InvokeSafely(this, this.clientState.TerritoryType); this.DutyRecommenced?.Invoke(this, this.clientState.TerritoryType);
break; break;
// Duty Completed Flytext Shown // Duty Completed Flytext Shown
case 0x4000_0002 when !this.CompletedThisTerritory: case 0x4000_0002 when !this.CompletedThisTerritory:
this.IsDutyStarted = false; this.IsDutyStarted = false;
this.CompletedThisTerritory = true; this.CompletedThisTerritory = true;
this.DutyCompleted.InvokeSafely(this, this.clientState.TerritoryType); this.DutyCompleted?.Invoke(this, this.clientState.TerritoryType);
break; break;
// Duty Completed // Duty Completed
case 0x4000_0003 when !this.CompletedThisTerritory: case 0x4000_0003 when !this.CompletedThisTerritory:
this.IsDutyStarted = false; this.IsDutyStarted = false;
this.CompletedThisTerritory = true; this.CompletedThisTerritory = true;
this.DutyCompleted.InvokeSafely(this, this.clientState.TerritoryType); this.DutyCompleted?.Invoke(this, this.clientState.TerritoryType);
break; break;
} }
} }
@ -126,7 +116,7 @@ public unsafe class DutyState : IDisposable, IServiceType, IDutyState
return this.contentDirectorNetworkMessageHook.Original(a1, a2, a3); return this.contentDirectorNetworkMessageHook.Original(a1, a2, a3);
} }
private void TerritoryOnChangedEvent(object? sender, ushort e) private void TerritoryOnChangedEvent(ushort territoryId)
{ {
if (this.IsDutyStarted) if (this.IsDutyStarted)
{ {
@ -141,7 +131,7 @@ public unsafe class DutyState : IDisposable, IServiceType, IDutyState
/// Joining a duty in progress, or disconnecting and reconnecting will cause the player to miss the event. /// Joining a duty in progress, or disconnecting and reconnecting will cause the player to miss the event.
/// </summary> /// </summary>
/// <param name="framework1">Framework reference.</param> /// <param name="framework1">Framework reference.</param>
private void FrameworkOnUpdateEvent(Framework framework1) private void FrameworkOnUpdateEvent(IFramework framework1)
{ {
// If the duty hasn't been started, and has not been completed yet this territory // If the duty hasn't been started, and has not been completed yet this territory
if (!this.IsDutyStarted && !this.CompletedThisTerritory) if (!this.IsDutyStarted && !this.CompletedThisTerritory)
@ -161,11 +151,73 @@ public unsafe class DutyState : IDisposable, IServiceType, IDutyState
} }
private bool IsBoundByDuty() private bool IsBoundByDuty()
=> this.condition.Any(ConditionFlag.BoundByDuty,
ConditionFlag.BoundByDuty56,
ConditionFlag.BoundByDuty95);
private bool IsInCombat()
=> this.condition.Any(ConditionFlag.InCombat);
}
/// <summary>
/// Plugin scoped version of DutyState.
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.ScopedService]
#pragma warning disable SA1015
[ResolveVia<IDutyState>]
#pragma warning restore SA1015
internal class DutyStatePluginScoped : IInternalDisposableService, IDutyState
{
[ServiceManager.ServiceDependency]
private readonly DutyState dutyStateService = Service<DutyState>.Get();
/// <summary>
/// Initializes a new instance of the <see cref="DutyStatePluginScoped"/> class.
/// </summary>
internal DutyStatePluginScoped()
{ {
return this.condition[ConditionFlag.BoundByDuty] || this.dutyStateService.DutyStarted += this.DutyStartedForward;
this.condition[ConditionFlag.BoundByDuty56] || this.dutyStateService.DutyWiped += this.DutyWipedForward;
this.condition[ConditionFlag.BoundByDuty95]; this.dutyStateService.DutyRecommenced += this.DutyRecommencedForward;
this.dutyStateService.DutyCompleted += this.DutyCompletedForward;
} }
private bool IsInCombat() => this.condition[ConditionFlag.InCombat]; /// <inheritdoc/>
public event EventHandler<ushort>? DutyStarted;
/// <inheritdoc/>
public event EventHandler<ushort>? DutyWiped;
/// <inheritdoc/>
public event EventHandler<ushort>? DutyRecommenced;
/// <inheritdoc/>
public event EventHandler<ushort>? DutyCompleted;
/// <inheritdoc/>
public bool IsDutyStarted => this.dutyStateService.IsDutyStarted;
/// <inheritdoc/>
void IInternalDisposableService.DisposeService()
{
this.dutyStateService.DutyStarted -= this.DutyStartedForward;
this.dutyStateService.DutyWiped -= this.DutyWipedForward;
this.dutyStateService.DutyRecommenced -= this.DutyRecommencedForward;
this.dutyStateService.DutyCompleted -= this.DutyCompletedForward;
this.DutyStarted = null;
this.DutyWiped = null;
this.DutyRecommenced = null;
this.DutyCompleted = null;
}
private void DutyStartedForward(object sender, ushort territoryId) => this.DutyStarted?.Invoke(sender, territoryId);
private void DutyWipedForward(object sender, ushort territoryId) => this.DutyWiped?.Invoke(sender, territoryId);
private void DutyRecommencedForward(object sender, ushort territoryId) => this.DutyRecommenced?.Invoke(sender, territoryId);
private void DutyCompletedForward(object sender, ushort territoryId) => this.DutyCompleted?.Invoke(sender, territoryId);
} }

View file

@ -1,11 +1,9 @@
using System;
namespace Dalamud.Game.DutyState; namespace Dalamud.Game.DutyState;
/// <summary> /// <summary>
/// Duty state memory address resolver. /// Duty state memory address resolver.
/// </summary> /// </summary>
public class DutyStateAddressResolver : BaseAddressResolver internal class DutyStateAddressResolver : BaseAddressResolver
{ {
/// <summary> /// <summary>
/// Gets the address of the method which is called when the client receives a content director update. /// Gets the address of the method which is called when the client receives a content director update.
@ -16,7 +14,7 @@ public class DutyStateAddressResolver : BaseAddressResolver
/// Scan for and setup any configured address pointers. /// Scan for and setup any configured address pointers.
/// </summary> /// </summary>
/// <param name="sig">The signature scanner to facilitate setup.</param> /// <param name="sig">The signature scanner to facilitate setup.</param>
protected override void Setup64Bit(SigScanner sig) protected override void Setup64Bit(ISigScanner sig)
{ {
this.ContentDirectorNetworkMessage = sig.ScanText("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC ?? 48 8B D9 49 8B F8 41 0F B7 08"); this.ContentDirectorNetworkMessage = sig.ScanText("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC ?? 48 8B D9 49 8B F8 41 0F B7 08");
} }

Some files were not shown because too many files have changed in this diff Show more