mirror of
https://github.com/goatcorp/Dalamud.git
synced 2026-01-03 14:23:40 +01:00
Merge branch 'apiX' into feature/itextureprovider-updates
This commit is contained in:
commit
8c7771bf7d
2213 changed files with 10372 additions and 1088868 deletions
3
.github/workflows/main.yml
vendored
3
.github/workflows/main.yml
vendored
|
|
@ -16,6 +16,9 @@ jobs:
|
|||
fetch-depth: 0
|
||||
- name: Setup MSBuild
|
||||
uses: microsoft/setup-msbuild@v1.0.2
|
||||
- uses: actions/setup-dotnet@v3
|
||||
with:
|
||||
dotnet-version: '8.0.100'
|
||||
- name: Define VERSION
|
||||
run: |
|
||||
$env:COMMIT = $env:GITHUB_SHA.Substring(0, 7)
|
||||
|
|
|
|||
3
.github/workflows/rollup.yml
vendored
3
.github/workflows/rollup.yml
vendored
|
|
@ -11,8 +11,7 @@ jobs:
|
|||
strategy:
|
||||
matrix:
|
||||
branches:
|
||||
- net8
|
||||
#- new_im_hooks # Unmergeable
|
||||
- new_im_hooks
|
||||
|
||||
defaults:
|
||||
run:
|
||||
|
|
|
|||
2
.gitmodules
vendored
2
.gitmodules
vendored
|
|
@ -3,7 +3,7 @@
|
|||
url = https://github.com/goatcorp/ImGuiScene
|
||||
[submodule "lib/FFXIVClientStructs"]
|
||||
path = lib/FFXIVClientStructs
|
||||
url = https://github.com/aers/FFXIVClientStructs.git
|
||||
url = https://github.com/aers/FFXIVClientStructs
|
||||
[submodule "lib/Nomade040-nmd"]
|
||||
path = lib/Nomade040-nmd
|
||||
url = https://github.com/Nomade040/nmd.git
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@
|
|||
<SubSystem>Windows</SubSystem>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<EnableUAC>false</EnableUAC>
|
||||
<AdditionalDependencies>Version.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<AdditionalDependencies>Version.lib;Shlwapi.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<AdditionalLibraryDirectories>..\lib\CoreCLR;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
|
|
@ -137,6 +137,7 @@
|
|||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="ntdll.cpp" />
|
||||
<ClCompile Include="unicode.cpp">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
|
|
@ -176,6 +177,7 @@
|
|||
<ClInclude Include="DalamudStartInfo.h" />
|
||||
<ClInclude Include="hooks.h" />
|
||||
<ClInclude Include="logging.h" />
|
||||
<ClInclude Include="ntdll.h" />
|
||||
<ClInclude Include="resource.h" />
|
||||
<ClInclude Include="unicode.h" />
|
||||
<ClInclude Include="utils.h" />
|
||||
|
|
|
|||
|
|
@ -73,6 +73,9 @@
|
|||
<ClCompile Include="DalamudStartInfo.cpp">
|
||||
<Filter>Dalamud.Boot DLL</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="ntdll.cpp">
|
||||
<Filter>Dalamud.Boot DLL</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="..\lib\CoreCLR\CoreCLR.h">
|
||||
|
|
@ -140,6 +143,9 @@
|
|||
</ClInclude>
|
||||
<ClInclude Include="resource.h" />
|
||||
<ClInclude Include="crashhandler_shared.h" />
|
||||
<ClInclude Include="ntdll.h">
|
||||
<Filter>Dalamud.Boot DLL</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ResourceCompile Include="Dalamud.Boot.rc" />
|
||||
|
|
|
|||
|
|
@ -82,6 +82,21 @@ void from_json(const nlohmann::json& json, DalamudStartInfo::LoadMethod& value)
|
|||
}
|
||||
}
|
||||
|
||||
void from_json(const nlohmann::json& json, DalamudStartInfo::UnhandledExceptionHandlingMode& value) {
|
||||
if (json.is_number_integer()) {
|
||||
value = static_cast<DalamudStartInfo::UnhandledExceptionHandlingMode>(json.get<int>());
|
||||
|
||||
} else if (json.is_string()) {
|
||||
const auto langstr = unicode::convert<std::string>(json.get<std::string>(), &unicode::lower);
|
||||
if (langstr == "default")
|
||||
value = DalamudStartInfo::UnhandledExceptionHandlingMode::Default;
|
||||
else if (langstr == "stalldebug")
|
||||
value = DalamudStartInfo::UnhandledExceptionHandlingMode::StallDebug;
|
||||
else if (langstr == "none")
|
||||
value = DalamudStartInfo::UnhandledExceptionHandlingMode::None;
|
||||
}
|
||||
}
|
||||
|
||||
void from_json(const nlohmann::json& json, DalamudStartInfo& config) {
|
||||
if (!json.is_object())
|
||||
return;
|
||||
|
|
@ -121,7 +136,7 @@ void from_json(const nlohmann::json& json, DalamudStartInfo& config) {
|
|||
}
|
||||
|
||||
config.CrashHandlerShow = json.value("CrashHandlerShow", config.CrashHandlerShow);
|
||||
config.NoExceptionHandlers = json.value("NoExceptionHandlers", config.NoExceptionHandlers);
|
||||
config.UnhandledException = json.value("UnhandledException", config.UnhandledException);
|
||||
}
|
||||
|
||||
void DalamudStartInfo::from_envvars() {
|
||||
|
|
|
|||
|
|
@ -32,6 +32,13 @@ struct DalamudStartInfo {
|
|||
};
|
||||
friend void from_json(const nlohmann::json&, LoadMethod&);
|
||||
|
||||
enum class UnhandledExceptionHandlingMode : int {
|
||||
Default,
|
||||
StallDebug,
|
||||
None,
|
||||
};
|
||||
friend void from_json(const nlohmann::json&, UnhandledExceptionHandlingMode&);
|
||||
|
||||
LoadMethod DalamudLoadMethod = LoadMethod::Entrypoint;
|
||||
std::string WorkingDirectory;
|
||||
std::string ConfigurationPath;
|
||||
|
|
@ -59,7 +66,7 @@ struct DalamudStartInfo {
|
|||
std::set<std::string> BootUnhookDlls{};
|
||||
|
||||
bool CrashHandlerShow = false;
|
||||
bool NoExceptionHandlers = false;
|
||||
UnhandledExceptionHandlingMode UnhandledException = UnhandledExceptionHandlingMode::Default;
|
||||
|
||||
friend void from_json(const nlohmann::json&, DalamudStartInfo&);
|
||||
void from_envvars();
|
||||
|
|
|
|||
|
|
@ -133,7 +133,7 @@ HRESULT WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) {
|
|||
// ============================== VEH ======================================== //
|
||||
|
||||
logging::I("Initializing VEH...");
|
||||
if (g_startInfo.NoExceptionHandlers) {
|
||||
if (g_startInfo.UnhandledException == DalamudStartInfo::UnhandledExceptionHandlingMode::None) {
|
||||
logging::W("=> Exception handlers are disabled from DalamudStartInfo.");
|
||||
} else if (g_startInfo.BootVehEnabled) {
|
||||
if (veh::add_handler(g_startInfo.BootVehFull, g_startInfo.WorkingDirectory))
|
||||
|
|
|
|||
|
|
@ -2,39 +2,9 @@
|
|||
|
||||
#include "hooks.h"
|
||||
|
||||
#include "ntdll.h"
|
||||
#include "logging.h"
|
||||
|
||||
enum {
|
||||
LDR_DLL_NOTIFICATION_REASON_LOADED = 1,
|
||||
LDR_DLL_NOTIFICATION_REASON_UNLOADED = 2,
|
||||
};
|
||||
|
||||
struct LDR_DLL_UNLOADED_NOTIFICATION_DATA {
|
||||
ULONG Flags; //Reserved.
|
||||
const UNICODE_STRING* FullDllName; //The full path name of the DLL module.
|
||||
const UNICODE_STRING* BaseDllName; //The base file name of the DLL module.
|
||||
PVOID DllBase; //A pointer to the base address for the DLL in memory.
|
||||
ULONG SizeOfImage; //The size of the DLL image, in bytes.
|
||||
};
|
||||
|
||||
struct LDR_DLL_LOADED_NOTIFICATION_DATA {
|
||||
ULONG Flags; //Reserved.
|
||||
const UNICODE_STRING* FullDllName; //The full path name of the DLL module.
|
||||
const UNICODE_STRING* BaseDllName; //The base file name of the DLL module.
|
||||
PVOID DllBase; //A pointer to the base address for the DLL in memory.
|
||||
ULONG SizeOfImage; //The size of the DLL image, in bytes.
|
||||
};
|
||||
|
||||
union LDR_DLL_NOTIFICATION_DATA {
|
||||
LDR_DLL_LOADED_NOTIFICATION_DATA Loaded;
|
||||
LDR_DLL_UNLOADED_NOTIFICATION_DATA Unloaded;
|
||||
};
|
||||
|
||||
using PLDR_DLL_NOTIFICATION_FUNCTION = VOID CALLBACK(_In_ ULONG NotificationReason, _In_ const LDR_DLL_NOTIFICATION_DATA* NotificationData, _In_opt_ PVOID Context);
|
||||
|
||||
static const auto LdrRegisterDllNotification = utils::loaded_module(GetModuleHandleW(L"ntdll.dll")).get_exported_function<NTSTATUS(NTAPI)(ULONG Flags, PLDR_DLL_NOTIFICATION_FUNCTION NotificationFunction, PVOID Context, PVOID* Cookie)>("LdrRegisterDllNotification");
|
||||
static const auto LdrUnregisterDllNotification = utils::loaded_module(GetModuleHandleW(L"ntdll.dll")).get_exported_function<NTSTATUS(NTAPI)(PVOID Cookie)>("LdrUnregisterDllNotification");
|
||||
|
||||
hooks::getprocaddress_singleton_import_hook::getprocaddress_singleton_import_hook()
|
||||
: m_pfnGetProcAddress(GetProcAddress)
|
||||
, m_thunk("kernel32!GetProcAddress(Singleton Import Hook)",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
#pragma once
|
||||
|
||||
#include <limits>
|
||||
#include <map>
|
||||
|
||||
#include "utils.h"
|
||||
|
|
|
|||
15
Dalamud.Boot/ntdll.cpp
Normal file
15
Dalamud.Boot/ntdll.cpp
Normal 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
33
Dalamud.Boot/ntdll.h
Normal 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);
|
||||
|
|
@ -15,14 +15,20 @@
|
|||
#include <Windows.h>
|
||||
|
||||
// Windows Header Files (2)
|
||||
#include <DbgHelp.h>
|
||||
#include <Dbt.h>
|
||||
#include <dwmapi.h>
|
||||
#include <iphlpapi.h>
|
||||
#include <PathCch.h>
|
||||
#include <Psapi.h>
|
||||
#include <ShlObj.h>
|
||||
#include <Shlwapi.h>
|
||||
#include <SubAuth.h>
|
||||
#include <TlHelp32.h>
|
||||
|
||||
// Windows Header Files (3)
|
||||
#include <icmpapi.h> // Must be loaded after iphlpapi.h
|
||||
|
||||
// MSVC Compiler Intrinsic
|
||||
#include <intrin.h>
|
||||
|
||||
|
|
@ -30,6 +36,7 @@
|
|||
#include <comdef.h>
|
||||
|
||||
// C++ Standard Libraries
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
|
|
|
|||
|
|
@ -136,6 +136,17 @@ static void append_injector_launch_args(std::vector<std::wstring>& args)
|
|||
args.emplace_back(L"--msgbox2");
|
||||
if ((g_startInfo.BootWaitMessageBox & DalamudStartInfo::WaitMessageboxFlags::BeforeDalamudConstruct) != DalamudStartInfo::WaitMessageboxFlags::None)
|
||||
args.emplace_back(L"--msgbox3");
|
||||
switch (g_startInfo.UnhandledException) {
|
||||
case DalamudStartInfo::UnhandledExceptionHandlingMode::Default:
|
||||
args.emplace_back(L"--unhandled-exception=default");
|
||||
break;
|
||||
case DalamudStartInfo::UnhandledExceptionHandlingMode::StallDebug:
|
||||
args.emplace_back(L"--unhandled-exception=stalldebug");
|
||||
break;
|
||||
case DalamudStartInfo::UnhandledExceptionHandlingMode::None:
|
||||
args.emplace_back(L"--unhandled-exception=none");
|
||||
break;
|
||||
}
|
||||
|
||||
args.emplace_back(L"--");
|
||||
|
||||
|
|
@ -148,6 +159,13 @@ static void append_injector_launch_args(std::vector<std::wstring>& args)
|
|||
|
||||
LONG exception_handler(EXCEPTION_POINTERS* ex)
|
||||
{
|
||||
if (g_startInfo.UnhandledException == DalamudStartInfo::UnhandledExceptionHandlingMode::StallDebug) {
|
||||
while (!IsDebuggerPresent())
|
||||
Sleep(100);
|
||||
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
|
||||
// block any other exceptions hitting the handler while the messagebox is open
|
||||
const auto lock = std::lock_guard(g_exception_handler_mutex);
|
||||
|
||||
|
|
|
|||
|
|
@ -5,9 +5,8 @@
|
|||
#include "DalamudStartInfo.h"
|
||||
#include "hooks.h"
|
||||
#include "logging.h"
|
||||
#include "ntdll.h"
|
||||
#include "utils.h"
|
||||
#include <iphlpapi.h>
|
||||
#include <icmpapi.h>
|
||||
|
||||
template<typename T>
|
||||
static std::span<T> assume_nonempty_span(std::span<T> t, const char* descr) {
|
||||
|
|
@ -546,6 +545,109 @@ void xivfixes::prevent_icmphandle_crashes(bool bApply) {
|
|||
}
|
||||
}
|
||||
|
||||
void xivfixes::symbol_load_patches(bool bApply) {
|
||||
static const char* LogTag = "[xivfixes:symbol_load_patches]";
|
||||
|
||||
static std::optional<hooks::import_hook<decltype(SymInitialize)>> s_hookSymInitialize;
|
||||
static PVOID s_dllNotificationCookie = nullptr;
|
||||
|
||||
static const auto RemoveFullPathPdbInfo = [](const utils::loaded_module& mod) {
|
||||
const auto ddva = mod.data_directory(IMAGE_DIRECTORY_ENTRY_DEBUG).VirtualAddress;
|
||||
if (!ddva)
|
||||
return;
|
||||
|
||||
const auto& ddir = mod.ref_as<IMAGE_DEBUG_DIRECTORY>(ddva);
|
||||
if (ddir.Type == IMAGE_DEBUG_TYPE_CODEVIEW) {
|
||||
// The Visual C++ debug information.
|
||||
// Ghidra calls it "DotNetPdbInfo".
|
||||
static constexpr DWORD DotNetPdbInfoSignatureValue = 0x53445352;
|
||||
struct DotNetPdbInfo {
|
||||
DWORD Signature; // RSDS
|
||||
GUID Guid;
|
||||
DWORD Age;
|
||||
char PdbPath[1];
|
||||
};
|
||||
|
||||
const auto& pdbref = mod.ref_as<DotNetPdbInfo>(ddir.AddressOfRawData);
|
||||
if (pdbref.Signature == DotNetPdbInfoSignatureValue) {
|
||||
const auto pathSpan = std::string_view(pdbref.PdbPath, strlen(pdbref.PdbPath));
|
||||
const auto pathWide = unicode::convert<std::wstring>(pathSpan);
|
||||
std::wstring windowsDirectory(GetWindowsDirectoryW(nullptr, 0) + 1, L'\0');
|
||||
windowsDirectory.resize(
|
||||
GetWindowsDirectoryW(windowsDirectory.data(), static_cast<UINT>(windowsDirectory.size())));
|
||||
if (!PathIsRelativeW(pathWide.c_str()) && !PathIsSameRootW(windowsDirectory.c_str(), pathWide.c_str())) {
|
||||
utils::memory_tenderizer pathOverwrite(&pdbref.PdbPath, pathSpan.size(), PAGE_READWRITE);
|
||||
auto sep = std::find(pathSpan.rbegin(), pathSpan.rend(), '/');
|
||||
if (sep == pathSpan.rend())
|
||||
sep = std::find(pathSpan.rbegin(), pathSpan.rend(), '\\');
|
||||
if (sep != pathSpan.rend()) {
|
||||
logging::I(
|
||||
"{} Stripping pdb path folder: {} to {}",
|
||||
LogTag,
|
||||
pathSpan,
|
||||
&*sep + 1);
|
||||
memmove(const_cast<char*>(pathSpan.data()), &*sep + 1, sep - pathSpan.rbegin() + 1);
|
||||
} else {
|
||||
logging::I("{} Leaving pdb path unchanged: {}", LogTag, pathSpan);
|
||||
}
|
||||
} else {
|
||||
logging::I("{} Leaving pdb path unchanged: {}", LogTag, pathSpan);
|
||||
}
|
||||
} else {
|
||||
logging::I("{} CODEVIEW struct signature mismatch: got {:08X} instead.", LogTag, pdbref.Signature);
|
||||
}
|
||||
} else {
|
||||
logging::I("{} Debug directory: type {} is unsupported.", LogTag, ddir.Type);
|
||||
}
|
||||
};
|
||||
|
||||
if (bApply) {
|
||||
if (!g_startInfo.BootEnabledGameFixes.contains("symbol_load_patches")) {
|
||||
logging::I("{} Turned off via environment variable.", LogTag);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const auto& mod : utils::loaded_module::all_modules())
|
||||
RemoveFullPathPdbInfo(mod);
|
||||
|
||||
if (!s_dllNotificationCookie) {
|
||||
const auto res = LdrRegisterDllNotification(
|
||||
0,
|
||||
[](ULONG notiReason, const LDR_DLL_NOTIFICATION_DATA* pData, void* /* context */) {
|
||||
if (notiReason == LDR_DLL_NOTIFICATION_REASON_LOADED)
|
||||
RemoveFullPathPdbInfo(pData->Loaded.DllBase);
|
||||
},
|
||||
nullptr,
|
||||
&s_dllNotificationCookie);
|
||||
|
||||
if (res != STATUS_SUCCESS) {
|
||||
logging::E("{} LdrRegisterDllNotification failure: 0x{:08X}", LogTag, res);
|
||||
s_dllNotificationCookie = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
s_hookSymInitialize.emplace("dbghelp.dll!SymInitialize (import, symbol_load_patches)", "dbghelp.dll", "SymInitialize", 0);
|
||||
s_hookSymInitialize->set_detour([](HANDLE hProcess, PCSTR UserSearchPath, BOOL fInvadeProcess) noexcept {
|
||||
logging::I("{} Suppressed SymInitialize.", LogTag);
|
||||
SetLastError(ERROR_NOT_SUPPORTED);
|
||||
return FALSE;
|
||||
});
|
||||
|
||||
logging::I("{} Enable", LogTag);
|
||||
}
|
||||
else {
|
||||
if (s_hookSymInitialize) {
|
||||
logging::I("{} Disable", LogTag);
|
||||
s_hookSymInitialize.reset();
|
||||
}
|
||||
|
||||
if (s_dllNotificationCookie) {
|
||||
(void)LdrUnregisterDllNotification(s_dllNotificationCookie);
|
||||
s_dllNotificationCookie = nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void xivfixes::apply_all(bool bApply) {
|
||||
for (const auto& [taskName, taskFunction] : std::initializer_list<std::pair<const char*, void(*)(bool)>>
|
||||
{
|
||||
|
|
@ -554,7 +656,8 @@ void xivfixes::apply_all(bool bApply) {
|
|||
{ "disable_game_openprocess_access_check", &disable_game_openprocess_access_check },
|
||||
{ "redirect_openprocess", &redirect_openprocess },
|
||||
{ "backup_userdata_save", &backup_userdata_save },
|
||||
{ "prevent_icmphandle_crashes", &prevent_icmphandle_crashes }
|
||||
{ "prevent_icmphandle_crashes", &prevent_icmphandle_crashes },
|
||||
{ "symbol_load_patches", &symbol_load_patches },
|
||||
}
|
||||
) {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ namespace xivfixes {
|
|||
void redirect_openprocess(bool bApply);
|
||||
void backup_userdata_save(bool bApply);
|
||||
void prevent_icmphandle_crashes(bool bApply);
|
||||
void symbol_load_patches(bool bApply);
|
||||
|
||||
void apply_all(bool bApply);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
|
|
|||
|
|
@ -145,7 +145,7 @@ public record DalamudStartInfo
|
|||
public bool CrashHandlerShow { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to disable all kinds of global exception handlers.
|
||||
/// Gets or sets a value indicating how to deal with unhandled exceptions.
|
||||
/// </summary>
|
||||
public bool NoExceptionHandlers { get; set; }
|
||||
public UnhandledExceptionHandlingMode UnhandledException { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ public sealed class GameVersion : ICloneable, IComparable, IComparable<GameVersi
|
|||
/// Initializes a new instance of the <see cref="GameVersion"/> class.
|
||||
/// </summary>
|
||||
/// <param name="version">Version string to parse.</param>
|
||||
[JsonConstructor]
|
||||
public GameVersion(string version)
|
||||
{
|
||||
var ver = Parse(version);
|
||||
|
|
@ -42,20 +41,9 @@ public sealed class GameVersion : ICloneable, IComparable, IComparable<GameVersi
|
|||
/// <param name="day">The day.</param>
|
||||
/// <param name="major">The major version.</param>
|
||||
/// <param name="minor">The minor version.</param>
|
||||
public GameVersion(int year, int month, int day, int major, int minor)
|
||||
[JsonConstructor]
|
||||
public GameVersion(int year, int month, int day, int major, int minor) : this(year, month, day, major)
|
||||
{
|
||||
if ((this.Year = year) < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(year));
|
||||
|
||||
if ((this.Month = month) < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(month));
|
||||
|
||||
if ((this.Day = day) < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(day));
|
||||
|
||||
if ((this.Major = major) < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(major));
|
||||
|
||||
if ((this.Minor = minor) < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(minor));
|
||||
}
|
||||
|
|
@ -67,17 +55,8 @@ public sealed class GameVersion : ICloneable, IComparable, IComparable<GameVersi
|
|||
/// <param name="month">The month.</param>
|
||||
/// <param name="day">The day.</param>
|
||||
/// <param name="major">The major version.</param>
|
||||
public GameVersion(int year, int month, int day, int major)
|
||||
public GameVersion(int year, int month, int day, int major) : this(year, month, day)
|
||||
{
|
||||
if ((this.Year = year) < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(year));
|
||||
|
||||
if ((this.Month = month) < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(month));
|
||||
|
||||
if ((this.Day = day) < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(day));
|
||||
|
||||
if ((this.Major = major) < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(major));
|
||||
}
|
||||
|
|
@ -88,14 +67,8 @@ public sealed class GameVersion : ICloneable, IComparable, IComparable<GameVersi
|
|||
/// <param name="year">The year.</param>
|
||||
/// <param name="month">The month.</param>
|
||||
/// <param name="day">The day.</param>
|
||||
public GameVersion(int year, int month, int day)
|
||||
public GameVersion(int year, int month, int day) : this(year, month)
|
||||
{
|
||||
if ((this.Year = year) < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(year));
|
||||
|
||||
if ((this.Month = month) < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(month));
|
||||
|
||||
if ((this.Day = day) < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(day));
|
||||
}
|
||||
|
|
@ -105,11 +78,8 @@ public sealed class GameVersion : ICloneable, IComparable, IComparable<GameVersi
|
|||
/// </summary>
|
||||
/// <param name="year">The year.</param>
|
||||
/// <param name="month">The month.</param>
|
||||
public GameVersion(int year, int month)
|
||||
public GameVersion(int year, int month) : this(year)
|
||||
{
|
||||
if ((this.Year = year) < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(year));
|
||||
|
||||
if ((this.Month = month) < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(month));
|
||||
}
|
||||
|
|
@ -139,26 +109,31 @@ public sealed class GameVersion : ICloneable, IComparable, IComparable<GameVersi
|
|||
/// <summary>
|
||||
/// Gets the year component.
|
||||
/// </summary>
|
||||
[JsonRequired]
|
||||
public int Year { get; } = -1;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the month component.
|
||||
/// </summary>
|
||||
[JsonRequired]
|
||||
public int Month { get; } = -1;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the day component.
|
||||
/// </summary>
|
||||
[JsonRequired]
|
||||
public int Day { get; } = -1;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the major version component.
|
||||
/// </summary>
|
||||
[JsonRequired]
|
||||
public int Major { get; } = -1;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the minor version component.
|
||||
/// </summary>
|
||||
[JsonRequired]
|
||||
public int Minor { get; } = -1;
|
||||
|
||||
public static implicit operator GameVersion(string ver)
|
||||
|
|
@ -183,17 +158,13 @@ public sealed class GameVersion : ICloneable, IComparable, IComparable<GameVersi
|
|||
|
||||
public static bool operator <(GameVersion v1, GameVersion v2)
|
||||
{
|
||||
if (v1 is null)
|
||||
throw new ArgumentNullException(nameof(v1));
|
||||
|
||||
ArgumentNullException.ThrowIfNull(v1);
|
||||
return v1.CompareTo(v2) < 0;
|
||||
}
|
||||
|
||||
public static bool operator <=(GameVersion v1, GameVersion v2)
|
||||
{
|
||||
if (v1 is null)
|
||||
throw new ArgumentNullException(nameof(v1));
|
||||
|
||||
ArgumentNullException.ThrowIfNull(v1);
|
||||
return v1.CompareTo(v2) <= 0;
|
||||
}
|
||||
|
||||
|
|
@ -209,8 +180,7 @@ public sealed class GameVersion : ICloneable, IComparable, IComparable<GameVersi
|
|||
|
||||
public static GameVersion operator +(GameVersion v1, TimeSpan v2)
|
||||
{
|
||||
if (v1 == null)
|
||||
throw new ArgumentNullException(nameof(v1));
|
||||
ArgumentNullException.ThrowIfNull(v1);
|
||||
|
||||
if (v1.Year == -1 || v1.Month == -1 || v1.Day == -1)
|
||||
return v1;
|
||||
|
|
@ -222,8 +192,7 @@ public sealed class GameVersion : ICloneable, IComparable, IComparable<GameVersi
|
|||
|
||||
public static GameVersion operator -(GameVersion v1, TimeSpan v2)
|
||||
{
|
||||
if (v1 == null)
|
||||
throw new ArgumentNullException(nameof(v1));
|
||||
ArgumentNullException.ThrowIfNull(v1);
|
||||
|
||||
if (v1.Year == -1 || v1.Month == -1 || v1.Day == -1)
|
||||
return v1;
|
||||
|
|
@ -240,18 +209,18 @@ public sealed class GameVersion : ICloneable, IComparable, IComparable<GameVersi
|
|||
/// <returns>GameVersion object.</returns>
|
||||
public static GameVersion Parse(string input)
|
||||
{
|
||||
if (input == null)
|
||||
throw new ArgumentNullException(nameof(input));
|
||||
ArgumentNullException.ThrowIfNull(input);
|
||||
|
||||
if (input.ToLower(CultureInfo.InvariantCulture) == "any")
|
||||
return new GameVersion();
|
||||
return Any;
|
||||
|
||||
var parts = input.Split('.');
|
||||
var tplParts = parts.Select(p =>
|
||||
{
|
||||
var result = int.TryParse(p, out var value);
|
||||
return (result, value);
|
||||
}).ToArray();
|
||||
var tplParts = parts.Select(
|
||||
p =>
|
||||
{
|
||||
var result = int.TryParse(p, out var value);
|
||||
return (result, value);
|
||||
}).ToArray();
|
||||
|
||||
if (tplParts.Any(t => !t.result))
|
||||
throw new FormatException("Bad formatting");
|
||||
|
|
@ -259,18 +228,15 @@ public sealed class GameVersion : ICloneable, IComparable, IComparable<GameVersi
|
|||
var intParts = tplParts.Select(t => t.value).ToArray();
|
||||
var len = intParts.Length;
|
||||
|
||||
if (len == 1)
|
||||
return new GameVersion(intParts[0]);
|
||||
else if (len == 2)
|
||||
return new GameVersion(intParts[0], intParts[1]);
|
||||
else if (len == 3)
|
||||
return new GameVersion(intParts[0], intParts[1], intParts[2]);
|
||||
else if (len == 4)
|
||||
return new GameVersion(intParts[0], intParts[1], intParts[2], intParts[3]);
|
||||
else if (len == 5)
|
||||
return new GameVersion(intParts[0], intParts[1], intParts[2], intParts[3], intParts[4]);
|
||||
else
|
||||
throw new ArgumentException("Too many parts");
|
||||
return len switch
|
||||
{
|
||||
1 => new GameVersion(intParts[0]),
|
||||
2 => new GameVersion(intParts[0], intParts[1]),
|
||||
3 => new GameVersion(intParts[0], intParts[1], intParts[2]),
|
||||
4 => new GameVersion(intParts[0], intParts[1], intParts[2], intParts[3]),
|
||||
5 => new GameVersion(intParts[0], intParts[1], intParts[2], intParts[3], intParts[4]),
|
||||
_ => throw new ArgumentException("Too many parts"),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -299,17 +265,12 @@ public sealed class GameVersion : ICloneable, IComparable, IComparable<GameVersi
|
|||
/// <inheritdoc/>
|
||||
public int CompareTo(object? obj)
|
||||
{
|
||||
if (obj == null)
|
||||
return 1;
|
||||
|
||||
if (obj is GameVersion value)
|
||||
return obj switch
|
||||
{
|
||||
return this.CompareTo(value);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentException("Argument must be a GameVersion");
|
||||
}
|
||||
null => 1,
|
||||
GameVersion value => this.CompareTo(value),
|
||||
_ => throw new ArgumentException("Argument must be a GameVersion", nameof(obj)),
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
|
@ -342,16 +303,14 @@ public sealed class GameVersion : ICloneable, IComparable, IComparable<GameVersi
|
|||
if (this.Minor != value.Minor)
|
||||
return this.Minor > value.Minor ? 1 : -1;
|
||||
|
||||
// This should never happen
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
if (obj is not GameVersion value)
|
||||
return false;
|
||||
|
||||
return this.Equals(value);
|
||||
return obj is GameVersion value && this.Equals(value);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
|
@ -373,16 +332,8 @@ public sealed class GameVersion : ICloneable, IComparable, IComparable<GameVersi
|
|||
/// <inheritdoc/>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
var accumulator = 0;
|
||||
|
||||
// This might be horribly wrong, but it isn't used heavily.
|
||||
accumulator |= this.Year.GetHashCode();
|
||||
accumulator |= this.Month.GetHashCode();
|
||||
accumulator |= this.Day.GetHashCode();
|
||||
accumulator |= this.Major.GetHashCode();
|
||||
accumulator |= this.Minor.GetHashCode();
|
||||
|
||||
return accumulator;
|
||||
// https://learn.microsoft.com/en-us/dotnet/api/system.object.gethashcode?view=net-8.0#notes-to-inheritors
|
||||
return HashCode.Combine(this.Year, this.Month, this.Day, this.Major, this.Minor);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
|
@ -396,11 +347,11 @@ public sealed class GameVersion : ICloneable, IComparable, IComparable<GameVersi
|
|||
return "any";
|
||||
|
||||
return new StringBuilder()
|
||||
.Append(string.Format("{0:D4}.", this.Year == -1 ? 0 : this.Year))
|
||||
.Append(string.Format("{0:D2}.", this.Month == -1 ? 0 : this.Month))
|
||||
.Append(string.Format("{0:D2}.", this.Day == -1 ? 0 : this.Day))
|
||||
.Append(string.Format("{0:D4}.", this.Major == -1 ? 0 : this.Major))
|
||||
.Append(string.Format("{0:D4}", this.Minor == -1 ? 0 : this.Minor))
|
||||
.Append($"{(this.Year == -1 ? 0 : this.Year):D4}.")
|
||||
.Append($"{(this.Month == -1 ? 0 : this.Month):D2}.")
|
||||
.Append($"{(this.Day == -1 ? 0 : this.Day):D2}.")
|
||||
.Append($"{(this.Major == -1 ? 0 : this.Major):D4}.")
|
||||
.Append($"{(this.Minor == -1 ? 0 : this.Minor):D4}")
|
||||
.ToString();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,17 +15,16 @@ public sealed class GameVersionConverter : JsonConverter
|
|||
/// <param name="serializer">The calling serializer.</param>
|
||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||
{
|
||||
if (value == null)
|
||||
switch (value)
|
||||
{
|
||||
writer.WriteNull();
|
||||
}
|
||||
else if (value is GameVersion)
|
||||
{
|
||||
writer.WriteValue(value.ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new JsonSerializationException("Expected GameVersion object value");
|
||||
case null:
|
||||
writer.WriteNull();
|
||||
break;
|
||||
case GameVersion:
|
||||
writer.WriteValue(value.ToString());
|
||||
break;
|
||||
default:
|
||||
throw new JsonSerializationException("Expected GameVersion object value");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -43,24 +42,20 @@ public sealed class GameVersionConverter : JsonConverter
|
|||
{
|
||||
return null;
|
||||
}
|
||||
else
|
||||
|
||||
if (reader.TokenType == JsonToken.String)
|
||||
{
|
||||
if (reader.TokenType == JsonToken.String)
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
return new GameVersion((string)reader.Value!);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new JsonSerializationException($"Error parsing GameVersion string: {reader.Value}", ex);
|
||||
}
|
||||
return new GameVersion((string)reader.Value!);
|
||||
}
|
||||
else
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new JsonSerializationException($"Unexpected token or value when parsing GameVersion. Token: {reader.TokenType}, Value: {reader.Value}");
|
||||
throw new JsonSerializationException($"Error parsing GameVersion string: {reader.Value}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
throw new JsonSerializationException($"Unexpected token or value when parsing GameVersion. Token: {reader.TokenType}, Value: {reader.Value}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
16
Dalamud.Common/UnhandledExceptionHandlingMode.cs
Normal file
16
Dalamud.Common/UnhandledExceptionHandlingMode.cs
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
namespace Dalamud.Common;
|
||||
|
||||
/// <summary>Enum describing what to do on unhandled exceptions.</summary>
|
||||
public enum UnhandledExceptionHandlingMode
|
||||
{
|
||||
/// <summary>Always show Dalamud Crash Handler on crash, except for some exceptions.</summary>
|
||||
/// <remarks>See `vectored_exception_handler` in `veh.cpp`.</remarks>
|
||||
Default,
|
||||
|
||||
/// <summary>Waits for debugger if none is attached, and pass the exception to the next handler.</summary>
|
||||
/// <remarks>See `exception_handler` in `veh.cpp`.</remarks>
|
||||
StallDebug,
|
||||
|
||||
/// <summary>Do not register an exception handler.</summary>
|
||||
None,
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<AssemblyName>Dalamud.CorePlugin</AssemblyName>
|
||||
<TargetFramework>net7.0-windows</TargetFramework>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<Platforms>x64</Platforms>
|
||||
<LangVersion>10.0</LangVersion>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
|
|
|
|||
|
|
@ -97,8 +97,6 @@ namespace Dalamud.CorePlugin
|
|||
this.Interface.UiBuilder.Draw -= this.OnDraw;
|
||||
|
||||
this.windowSystem.RemoveAllWindows();
|
||||
|
||||
this.Interface.ExplicitDispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup Label="Target">
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
<Platforms>x64;AnyCPU</Platforms>
|
||||
|
|
|
|||
|
|
@ -99,7 +99,6 @@ namespace Dalamud.Injector
|
|||
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)
|
||||
|
|
@ -277,6 +276,7 @@ namespace Dalamud.Injector
|
|||
var logName = startInfo.LogName;
|
||||
var logPath = startInfo.LogPath;
|
||||
var languageStr = startInfo.Language.ToString().ToLowerInvariant();
|
||||
var unhandledExceptionStr = startInfo.UnhandledException.ToString().ToLowerInvariant();
|
||||
var troubleshootingData = "{\"empty\": true, \"description\": \"No troubleshooting data supplied.\"}";
|
||||
|
||||
for (var i = 2; i < args.Count; i++)
|
||||
|
|
@ -317,6 +317,10 @@ namespace Dalamud.Injector
|
|||
{
|
||||
logPath = args[i][key.Length..];
|
||||
}
|
||||
else if (args[i].StartsWith(key = "--unhandled-exception="))
|
||||
{
|
||||
unhandledExceptionStr = args[i][key.Length..];
|
||||
}
|
||||
else
|
||||
{
|
||||
continue;
|
||||
|
|
@ -395,9 +399,15 @@ namespace Dalamud.Injector
|
|||
startInfo.BootShowConsole = args.Contains("--console");
|
||||
startInfo.BootEnableEtw = args.Contains("--etw");
|
||||
startInfo.BootLogPath = GetLogPath(startInfo.LogPath, "dalamud.boot", startInfo.LogName);
|
||||
startInfo.BootEnabledGameFixes = new List<string> {
|
||||
"prevent_devicechange_crashes", "disable_game_openprocess_access_check",
|
||||
"redirect_openprocess", "backup_userdata_save", "prevent_icmphandle_crashes",
|
||||
startInfo.BootEnabledGameFixes = new()
|
||||
{
|
||||
// See: xivfixes.h, xivfixes.cpp
|
||||
"prevent_devicechange_crashes",
|
||||
"disable_game_openprocess_access_check",
|
||||
"redirect_openprocess",
|
||||
"backup_userdata_save",
|
||||
"prevent_icmphandle_crashes",
|
||||
"symbol_load_patches",
|
||||
};
|
||||
startInfo.BootDotnetOpenProcessHookMode = 0;
|
||||
startInfo.BootWaitMessageBox |= args.Contains("--msgbox1") ? 1 : 0;
|
||||
|
|
@ -410,7 +420,14 @@ namespace Dalamud.Injector
|
|||
startInfo.NoLoadThirdPartyPlugins = args.Contains("--no-3rd-plugin");
|
||||
// startInfo.BootUnhookDlls = new List<string>() { "kernel32.dll", "ntdll.dll", "user32.dll" };
|
||||
startInfo.CrashHandlerShow = args.Contains("--crash-handler-console");
|
||||
startInfo.NoExceptionHandlers = args.Contains("--no-exception-handlers");
|
||||
startInfo.UnhandledException =
|
||||
Enum.TryParse<UnhandledExceptionHandlingMode>(
|
||||
unhandledExceptionStr,
|
||||
true,
|
||||
out var parsedUnhandledException)
|
||||
? parsedUnhandledException
|
||||
: throw new CommandLineException(
|
||||
$"\"{unhandledExceptionStr}\" is not a valid unhandled exception handling mode.");
|
||||
|
||||
return startInfo;
|
||||
}
|
||||
|
|
@ -452,7 +469,7 @@ namespace Dalamud.Injector
|
|||
Console.WriteLine("Verbose logging:\t[-v]");
|
||||
Console.WriteLine("Show Console:\t[--console] [--crash-handler-console]");
|
||||
Console.WriteLine("Enable ETW:\t[--etw]");
|
||||
Console.WriteLine("Enable VEH:\t[--veh], [--veh-full], [--no-exception-handlers]");
|
||||
Console.WriteLine("Enable VEH:\t[--veh], [--veh-full], [--unhandled-exception=default|stalldebug|none]");
|
||||
Console.WriteLine("Show messagebox:\t[--msgbox1], [--msgbox2], [--msgbox3]");
|
||||
Console.WriteLine("No plugins:\t[--no-plugin] [--no-3rd-plugin]");
|
||||
Console.WriteLine("Logging:\t[--logname=<logfile suffix>] [--logpath=<log base directory>]");
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup Label="Target">
|
||||
<TargetFramework>net7.0-windows</TargetFramework>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
<Platforms>x64;AnyCPU</Platforms>
|
||||
<LangVersion>9.0</LangVersion>
|
||||
<LangVersion>11.0</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Label="Feature">
|
||||
|
|
|
|||
138
Dalamud.Test/Game/GameVersionConverterTests.cs
Normal file
138
Dalamud.Test/Game/GameVersionConverterTests.cs
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
using Dalamud.Common.Game;
|
||||
|
||||
using JetBrains.Annotations;
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
using Xunit;
|
||||
|
||||
namespace Dalamud.Test.Game;
|
||||
|
||||
public class GameVersionConverterTests
|
||||
{
|
||||
[Fact]
|
||||
public void ReadJson_ConvertsFromString()
|
||||
{
|
||||
var serialized = """
|
||||
{
|
||||
"Version": "2020.06.15.0000.0000"
|
||||
}
|
||||
""";
|
||||
var deserialized = JsonConvert.DeserializeObject<TestSerializationClass>(serialized);
|
||||
|
||||
Assert.NotNull(deserialized);
|
||||
Assert.Equal(GameVersion.Parse("2020.06.15.0000.0000"), deserialized.Version);
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public void ReadJson_ConvertsFromNull()
|
||||
{
|
||||
var serialized = """
|
||||
{
|
||||
"Version": null
|
||||
}
|
||||
""";
|
||||
var deserialized = JsonConvert.DeserializeObject<TestSerializationClass>(serialized);
|
||||
|
||||
Assert.NotNull(deserialized);
|
||||
Assert.Null(deserialized.Version);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadJson_WhenInvalidType_Throws()
|
||||
{
|
||||
var serialized = """
|
||||
{
|
||||
"Version": 2
|
||||
}
|
||||
""";
|
||||
Assert.Throws<JsonSerializationException>(
|
||||
() => JsonConvert.DeserializeObject<TestSerializationClass>(serialized));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadJson_WhenInvalidVersion_Throws()
|
||||
{
|
||||
var serialized = """
|
||||
{
|
||||
"Version": "junk"
|
||||
}
|
||||
""";
|
||||
Assert.Throws<JsonSerializationException>(
|
||||
() => JsonConvert.DeserializeObject<TestSerializationClass>(serialized));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteJson_ConvertsToString()
|
||||
{
|
||||
var deserialized = new TestSerializationClass
|
||||
{
|
||||
Version = GameVersion.Parse("2020.06.15.0000.0000"),
|
||||
};
|
||||
var serialized = JsonConvert.SerializeObject(deserialized);
|
||||
|
||||
Assert.Equal("""{"Version":"2020.06.15.0000.0000"}""", RemoveWhitespace(serialized));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteJson_ConvertsToNull()
|
||||
{
|
||||
var deserialized = new TestSerializationClass
|
||||
{
|
||||
Version = null,
|
||||
};
|
||||
var serialized = JsonConvert.SerializeObject(deserialized);
|
||||
|
||||
Assert.Equal("""{"Version":null}""", RemoveWhitespace(serialized));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteJson_WhenInvalidVersion_Throws()
|
||||
{
|
||||
var deserialized = new TestWrongTypeSerializationClass
|
||||
{
|
||||
Version = 42,
|
||||
};
|
||||
Assert.Throws<JsonSerializationException>(() => JsonConvert.SerializeObject(deserialized));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanConvert_WhenGameVersion_ReturnsTrue()
|
||||
{
|
||||
var converter = new GameVersionConverter();
|
||||
Assert.True(converter.CanConvert(typeof(GameVersion)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanConvert_WhenNotGameVersion_ReturnsFalse()
|
||||
{
|
||||
var converter = new GameVersionConverter();
|
||||
Assert.False(converter.CanConvert(typeof(int)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanConvert_WhenNull_ReturnsFalse()
|
||||
{
|
||||
var converter = new GameVersionConverter();
|
||||
Assert.False(converter.CanConvert(null!));
|
||||
}
|
||||
|
||||
private static string RemoveWhitespace(string input)
|
||||
{
|
||||
return input.Replace(" ", "").Replace("\r", "").Replace("\n", "");
|
||||
}
|
||||
|
||||
private class TestSerializationClass
|
||||
{
|
||||
[JsonConverter(typeof(GameVersionConverter))]
|
||||
[CanBeNull]
|
||||
public GameVersion Version { get; init; }
|
||||
}
|
||||
|
||||
private class TestWrongTypeSerializationClass
|
||||
{
|
||||
[JsonConverter(typeof(GameVersionConverter))]
|
||||
public int Version { get; init; }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,71 @@
|
|||
using System;
|
||||
|
||||
using Dalamud.Common.Game;
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
using Xunit;
|
||||
|
||||
namespace Dalamud.Test.Game
|
||||
{
|
||||
public class GameVersionTests
|
||||
{
|
||||
[Fact]
|
||||
public void VersionComparisons()
|
||||
{
|
||||
var v1 = GameVersion.Parse("2021.01.01.0000.0000");
|
||||
var v2 = GameVersion.Parse("2021.01.01.0000.0000");
|
||||
Assert.True(v1 == v2);
|
||||
Assert.False(v1 != v2);
|
||||
Assert.False(v1 < v2);
|
||||
Assert.True(v1 <= v2);
|
||||
Assert.False(v1 > v2);
|
||||
Assert.True(v1 >= v2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VersionAddition()
|
||||
{
|
||||
var v1 = GameVersion.Parse("2021.01.01.0000.0000");
|
||||
var v2 = GameVersion.Parse("2021.01.05.0000.0000");
|
||||
Assert.Equal(v2, v1 + TimeSpan.FromDays(4));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VersionAdditionAny()
|
||||
{
|
||||
Assert.Equal(GameVersion.Any, GameVersion.Any + TimeSpan.FromDays(4));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VersionSubtraction()
|
||||
{
|
||||
var v1 = GameVersion.Parse("2021.01.05.0000.0000");
|
||||
var v2 = GameVersion.Parse("2021.01.01.0000.0000");
|
||||
Assert.Equal(v2, v1 - TimeSpan.FromDays(4));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VersionSubtractionAny()
|
||||
{
|
||||
Assert.Equal(GameVersion.Any, GameVersion.Any - TimeSpan.FromDays(4));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VersionClone()
|
||||
{
|
||||
var v1 = GameVersion.Parse("2021.01.01.0000.0000");
|
||||
var v2 = v1.Clone();
|
||||
Assert.NotSame(v1, v2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VersionCast()
|
||||
{
|
||||
var v = GameVersion.Parse("2021.01.01.0000.0000");
|
||||
Assert.Equal("2021.01.01.0000.0000", v);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("any", "any")]
|
||||
[InlineData("2021.01.01.0000.0000", "2021.01.01.0000.0000")]
|
||||
|
|
@ -14,6 +75,18 @@ namespace Dalamud.Test.Game
|
|||
var v2 = GameVersion.Parse(ver2);
|
||||
|
||||
Assert.Equal(v1, v2);
|
||||
Assert.Equal(0, v1.CompareTo(v2));
|
||||
Assert.Equal(v1.GetHashCode(), v2.GetHashCode());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VersionNullEquality()
|
||||
{
|
||||
// Tests `Equals(GameVersion? value)`
|
||||
Assert.False(GameVersion.Parse("2021.01.01.0000.0000").Equals(null));
|
||||
|
||||
// Tests `Equals(object? value)`
|
||||
Assert.False(GameVersion.Parse("2021.01.01.0000.0000").Equals((object)null));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
|
@ -31,6 +104,67 @@ namespace Dalamud.Test.Game
|
|||
Assert.True(v1.CompareTo(v2) < 0);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("any", "2020.06.15.0000.0000")]
|
||||
public void VersionComparisonInverse(string ver1, string ver2)
|
||||
{
|
||||
var v1 = GameVersion.Parse(ver1);
|
||||
var v2 = GameVersion.Parse(ver2);
|
||||
|
||||
Assert.True(v1.CompareTo(v2) > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VersionComparisonNull()
|
||||
{
|
||||
var v = GameVersion.Parse("2020.06.15.0000.0000");
|
||||
|
||||
// Tests `CompareTo(GameVersion? value)`
|
||||
Assert.True(v.CompareTo(null) > 0);
|
||||
|
||||
// Tests `CompareTo(object? value)`
|
||||
Assert.True(v.CompareTo((object)null) > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VersionComparisonBoxed()
|
||||
{
|
||||
var v1 = GameVersion.Parse("2020.06.15.0000.0000");
|
||||
var v2 = GameVersion.Parse("2020.06.15.0000.0000");
|
||||
Assert.Equal(0, v1.CompareTo((object)v2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VersionComparisonBoxedInvalid()
|
||||
{
|
||||
var v = GameVersion.Parse("2020.06.15.0000.0000");
|
||||
Assert.Throws<ArgumentException>(() => v.CompareTo(42));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("2020.06.15.0000.0000")]
|
||||
[InlineData("2021.01.01.0000")]
|
||||
[InlineData("2021.01.01")]
|
||||
[InlineData("2021.01")]
|
||||
[InlineData("2021")]
|
||||
public void VersionParse(string ver)
|
||||
{
|
||||
var v = GameVersion.Parse(ver);
|
||||
Assert.NotNull(v);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("2020.06.15.0000.0000")]
|
||||
[InlineData("2021.01.01.0000")]
|
||||
[InlineData("2021.01.01")]
|
||||
[InlineData("2021.01")]
|
||||
[InlineData("2021")]
|
||||
public void VersionTryParse(string ver)
|
||||
{
|
||||
Assert.True(GameVersion.TryParse(ver, out var v));
|
||||
Assert.NotNull(v);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("2020.06.15.0000.0000")]
|
||||
[InlineData("2021.01.01.0000")]
|
||||
|
|
@ -39,9 +173,8 @@ namespace Dalamud.Test.Game
|
|||
[InlineData("2021")]
|
||||
public void VersionConstructor(string ver)
|
||||
{
|
||||
var v = GameVersion.Parse(ver);
|
||||
|
||||
Assert.True(v != null);
|
||||
var v = new GameVersion(ver);
|
||||
Assert.NotNull(v);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
|
@ -54,5 +187,89 @@ namespace Dalamud.Test.Game
|
|||
Assert.False(result);
|
||||
Assert.Null(v);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("any", "any")]
|
||||
[InlineData("2020.06.15.0000.0000", "2020.06.15.0000.0000")]
|
||||
[InlineData("2021.01.01.0000", "2021.01.01.0000.0000")]
|
||||
[InlineData("2021.01.01", "2021.01.01.0000.0000")]
|
||||
[InlineData("2021.01", "2021.01.00.0000.0000")]
|
||||
[InlineData("2021", "2021.00.00.0000.0000")]
|
||||
public void VersionToString(string ver1, string ver2)
|
||||
{
|
||||
var v1 = GameVersion.Parse(ver1);
|
||||
Assert.Equal(ver2, v1.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VersionIsSerializationSafe()
|
||||
{
|
||||
var v = GameVersion.Parse("2020.06.15.0000.0000");
|
||||
var serialized = JsonConvert.SerializeObject(v);
|
||||
var deserialized = JsonConvert.DeserializeObject<GameVersion>(serialized);
|
||||
Assert.Equal(v, deserialized);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VersionInvalidDeserialization()
|
||||
{
|
||||
var serialized = """
|
||||
{
|
||||
"Year": -1,
|
||||
"Month": -1,
|
||||
"Day": -1,
|
||||
"Major": -1,
|
||||
"Minor": -1,
|
||||
}
|
||||
""";
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => JsonConvert.DeserializeObject<GameVersion>(serialized));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VersionInvalidTypeDeserialization()
|
||||
{
|
||||
var serialized = """
|
||||
{
|
||||
"Value": "Hello"
|
||||
}
|
||||
""";
|
||||
Assert.Throws<JsonSerializationException>(() => JsonConvert.DeserializeObject<GameVersion>(serialized));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VersionConstructorNegativeYear()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new GameVersion(-2024));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VersionConstructorNegativeMonth()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new GameVersion(2024, -3));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VersionConstructorNegativeDay()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new GameVersion(2024, 3, -13));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VersionConstructorNegativeMajor()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new GameVersion(2024, 3, 13, -1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VersionConstructorNegativeMinor()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new GameVersion(2024, 3, 13, 0, -1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VersionParseNull()
|
||||
{
|
||||
Assert.Throws<ArgumentNullException>(() => GameVersion.Parse(null!));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
395
Dalamud.Test/Storage/ReliableFileStorageTests.cs
Normal file
395
Dalamud.Test/Storage/ReliableFileStorageTests.cs
Normal file
|
|
@ -0,0 +1,395 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reactive.Disposables;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Dalamud.Storage;
|
||||
|
||||
using Xunit;
|
||||
|
||||
namespace Dalamud.Test.Storage;
|
||||
|
||||
public class ReliableFileStorageTests
|
||||
{
|
||||
private const string DbFileName = "dalamudVfs.db";
|
||||
private const string TestFileName = "file.txt";
|
||||
private const string TestFileContent1 = "hello from señor dalamundo";
|
||||
private const string TestFileContent2 = "rewritten";
|
||||
|
||||
[Fact]
|
||||
public async Task IsConcurrencySafe()
|
||||
{
|
||||
var dbDir = CreateTempDir();
|
||||
var rfs = new DisposableReliableFileStorage(dbDir);
|
||||
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
|
||||
|
||||
// Do reads/writes/deletes on the same file on many threads at once and
|
||||
// see if anything throws
|
||||
await Task.WhenAll(
|
||||
Enumerable.Range(1, 6)
|
||||
.Select(
|
||||
i => Parallel.ForEachAsync(
|
||||
Enumerable.Range(1, 100),
|
||||
(j, _) =>
|
||||
{
|
||||
if (i % 2 == 0)
|
||||
{
|
||||
// ReSharper disable once AccessToDisposedClosure
|
||||
rfs.Instance.WriteAllText(tempFile, j.ToString());
|
||||
}
|
||||
else if (i % 3 == 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
// ReSharper disable once AccessToDisposedClosure
|
||||
rfs.Instance.ReadAllText(tempFile);
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
// this is fine
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
File.Delete(tempFile);
|
||||
}
|
||||
|
||||
return ValueTask.CompletedTask;
|
||||
})));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_Dispose_Works()
|
||||
{
|
||||
var dbDir = CreateTempDir();
|
||||
var dbPath = Path.Combine(dbDir, DbFileName);
|
||||
using var rfs = new DisposableReliableFileStorage(dbDir);
|
||||
|
||||
Assert.True(File.Exists(dbPath));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Exists_ThrowsIfPathIsEmpty()
|
||||
{
|
||||
using var rfs = CreateRfs();
|
||||
Assert.Throws<ArgumentException>(() => rfs.Instance.Exists(""));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Exists_ThrowsIfPathIsNull()
|
||||
{
|
||||
using var rfs = CreateRfs();
|
||||
Assert.Throws<ArgumentNullException>(() => rfs.Instance.Exists(null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Exists_WhenFileMissing_ReturnsFalse()
|
||||
{
|
||||
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
|
||||
using var rfs = CreateRfs();
|
||||
|
||||
Assert.False(rfs.Instance.Exists(tempFile));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Exists_WhenFileMissing_WhenDbFailed_ReturnsFalse()
|
||||
{
|
||||
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
|
||||
using var rfs = CreateFailedRfs();
|
||||
|
||||
Assert.False(rfs.Instance.Exists(tempFile));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Exists_WhenFileOnDisk_ReturnsTrue()
|
||||
{
|
||||
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
|
||||
await File.WriteAllTextAsync(tempFile, TestFileContent1);
|
||||
using var rfs = CreateRfs();
|
||||
|
||||
Assert.True(rfs.Instance.Exists(tempFile));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Exists_WhenFileInBackup_ReturnsTrue()
|
||||
{
|
||||
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
|
||||
using var rfs = CreateRfs();
|
||||
|
||||
rfs.Instance.WriteAllText(tempFile, TestFileContent1);
|
||||
|
||||
File.Delete(tempFile);
|
||||
Assert.True(rfs.Instance.Exists(tempFile));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Exists_WhenFileInBackup_WithDifferentContainerId_ReturnsFalse()
|
||||
{
|
||||
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
|
||||
using var rfs = CreateRfs();
|
||||
|
||||
rfs.Instance.WriteAllText(tempFile, TestFileContent1);
|
||||
|
||||
File.Delete(tempFile);
|
||||
Assert.False(rfs.Instance.Exists(tempFile, Guid.NewGuid()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteAllText_ThrowsIfPathIsEmpty()
|
||||
{
|
||||
using var rfs = CreateRfs();
|
||||
Assert.Throws<ArgumentException>(() => rfs.Instance.WriteAllText("", TestFileContent1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteAllText_ThrowsIfPathIsNull()
|
||||
{
|
||||
using var rfs = CreateRfs();
|
||||
Assert.Throws<ArgumentNullException>(() => rfs.Instance.WriteAllText(null!, TestFileContent1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteAllText_WritesToDbAndDisk()
|
||||
{
|
||||
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
|
||||
using var rfs = CreateRfs();
|
||||
|
||||
rfs.Instance.WriteAllText(tempFile, TestFileContent1);
|
||||
|
||||
Assert.True(File.Exists(tempFile));
|
||||
Assert.Equal(TestFileContent1, rfs.Instance.ReadAllText(tempFile, forceBackup: true));
|
||||
Assert.Equal(TestFileContent1, await File.ReadAllTextAsync(tempFile));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteAllText_SeparatesContainers()
|
||||
{
|
||||
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
|
||||
var containerId = Guid.NewGuid();
|
||||
|
||||
using var rfs = CreateRfs();
|
||||
rfs.Instance.WriteAllText(tempFile, TestFileContent1);
|
||||
rfs.Instance.WriteAllText(tempFile, TestFileContent2, containerId);
|
||||
File.Delete(tempFile);
|
||||
|
||||
Assert.Equal(TestFileContent1, rfs.Instance.ReadAllText(tempFile, forceBackup: true));
|
||||
Assert.Equal(TestFileContent2, rfs.Instance.ReadAllText(tempFile, forceBackup: true, containerId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteAllText_WhenDbFailed_WritesToDisk()
|
||||
{
|
||||
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
|
||||
using var rfs = CreateFailedRfs();
|
||||
|
||||
rfs.Instance.WriteAllText(tempFile, TestFileContent1);
|
||||
|
||||
Assert.True(File.Exists(tempFile));
|
||||
Assert.Equal(TestFileContent1, await File.ReadAllTextAsync(tempFile));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteAllText_CanUpdateExistingFile()
|
||||
{
|
||||
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
|
||||
using var rfs = CreateRfs();
|
||||
|
||||
rfs.Instance.WriteAllText(tempFile, TestFileContent1);
|
||||
rfs.Instance.WriteAllText(tempFile, TestFileContent2);
|
||||
|
||||
Assert.True(File.Exists(tempFile));
|
||||
Assert.Equal(TestFileContent2, rfs.Instance.ReadAllText(tempFile, forceBackup: true));
|
||||
Assert.Equal(TestFileContent2, await File.ReadAllTextAsync(tempFile));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteAllText_SupportsNullContent()
|
||||
{
|
||||
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
|
||||
using var rfs = CreateRfs();
|
||||
|
||||
rfs.Instance.WriteAllText(tempFile, null);
|
||||
|
||||
Assert.True(File.Exists(tempFile));
|
||||
Assert.Equal("", rfs.Instance.ReadAllText(tempFile));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadAllText_ThrowsIfPathIsEmpty()
|
||||
{
|
||||
using var rfs = CreateRfs();
|
||||
Assert.Throws<ArgumentException>(() => rfs.Instance.ReadAllText(""));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadAllText_ThrowsIfPathIsNull()
|
||||
{
|
||||
using var rfs = CreateRfs();
|
||||
Assert.Throws<ArgumentNullException>(() => rfs.Instance.ReadAllText(null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadAllText_WhenFileOnDisk_ReturnsContent()
|
||||
{
|
||||
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
|
||||
await File.WriteAllTextAsync(tempFile, TestFileContent1);
|
||||
using var rfs = CreateRfs();
|
||||
|
||||
Assert.Equal(TestFileContent1, rfs.Instance.ReadAllText(tempFile));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadAllText_WhenFileMissingWithBackup_ReturnsContent()
|
||||
{
|
||||
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
|
||||
using var rfs = CreateRfs();
|
||||
|
||||
rfs.Instance.WriteAllText(tempFile, TestFileContent1);
|
||||
File.Delete(tempFile);
|
||||
|
||||
Assert.Equal(TestFileContent1, rfs.Instance.ReadAllText(tempFile));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadAllText_WhenFileMissingWithBackup_ThrowsWithDifferentContainerId()
|
||||
{
|
||||
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
|
||||
var containerId = Guid.NewGuid();
|
||||
using var rfs = CreateRfs();
|
||||
|
||||
rfs.Instance.WriteAllText(tempFile, TestFileContent1);
|
||||
File.Delete(tempFile);
|
||||
|
||||
Assert.Throws<FileNotFoundException>(() => rfs.Instance.ReadAllText(tempFile, containerId: containerId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadAllText_WhenFileMissing_ThrowsIfDbFailed()
|
||||
{
|
||||
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
|
||||
using var rfs = CreateFailedRfs();
|
||||
Assert.Throws<FileNotFoundException>(() => rfs.Instance.ReadAllText(tempFile));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadAllText_WithReader_WhenFileOnDisk_ReadsContent()
|
||||
{
|
||||
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
|
||||
await File.WriteAllTextAsync(tempFile, TestFileContent1);
|
||||
using var rfs = CreateRfs();
|
||||
rfs.Instance.ReadAllText(tempFile, text => Assert.Equal(TestFileContent1, text));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadAllText_WithReader_WhenReaderThrows_ThrowsIfBackupMissing()
|
||||
{
|
||||
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
|
||||
await File.WriteAllTextAsync(tempFile, TestFileContent1);
|
||||
|
||||
var readerCalledOnce = false;
|
||||
|
||||
using var rfs = CreateRfs();
|
||||
Assert.Throws<FileReadException>(() => rfs.Instance.ReadAllText(tempFile, Reader));
|
||||
|
||||
return;
|
||||
|
||||
void Reader(string text)
|
||||
{
|
||||
var wasReaderCalledOnce = readerCalledOnce;
|
||||
readerCalledOnce = true;
|
||||
if (!wasReaderCalledOnce) throw new Exception();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadAllText_WithReader_WhenReaderThrows_ReadsContentFromBackup()
|
||||
{
|
||||
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
|
||||
|
||||
var readerCalledOnce = false;
|
||||
var assertionCalled = false;
|
||||
|
||||
using var rfs = CreateRfs();
|
||||
rfs.Instance.WriteAllText(tempFile, TestFileContent1);
|
||||
File.Delete(tempFile);
|
||||
|
||||
rfs.Instance.ReadAllText(tempFile, Reader);
|
||||
Assert.True(assertionCalled);
|
||||
|
||||
return;
|
||||
|
||||
void Reader(string text)
|
||||
{
|
||||
var wasReaderCalledOnce = readerCalledOnce;
|
||||
readerCalledOnce = true;
|
||||
if (!wasReaderCalledOnce) throw new Exception();
|
||||
Assert.Equal(TestFileContent1, text);
|
||||
assertionCalled = true;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadAllText_WithReader_RethrowsFileNotFoundException()
|
||||
{
|
||||
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
|
||||
await File.WriteAllTextAsync(tempFile, TestFileContent1);
|
||||
using var rfs = CreateRfs();
|
||||
Assert.Throws<FileNotFoundException>(() => rfs.Instance.ReadAllText(tempFile, _ => throw new FileNotFoundException()));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public void ReadAllText_WhenFileDoesNotExist_Throws(bool forceBackup)
|
||||
{
|
||||
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
|
||||
using var rfs = CreateRfs();
|
||||
Assert.Throws<FileNotFoundException>(() => rfs.Instance.ReadAllText(tempFile, forceBackup));
|
||||
}
|
||||
|
||||
private static DisposableReliableFileStorage CreateRfs()
|
||||
{
|
||||
var dbDir = CreateTempDir();
|
||||
return new(dbDir);
|
||||
}
|
||||
|
||||
private static DisposableReliableFileStorage CreateFailedRfs()
|
||||
{
|
||||
var dbDir = CreateTempDir();
|
||||
var dbPath = Path.Combine(dbDir, DbFileName);
|
||||
|
||||
// Create a corrupt DB deliberately, and hold its handle until
|
||||
// the end of the scope
|
||||
using var f = File.Open(dbPath, FileMode.CreateNew);
|
||||
f.Write("broken"u8);
|
||||
|
||||
// Throws an SQLiteException initially, and then throws an
|
||||
// IOException when attempting to delete the file because
|
||||
// there's already an active handle associated with it
|
||||
return new(dbDir);
|
||||
}
|
||||
|
||||
private static string CreateTempDir()
|
||||
{
|
||||
string tempDir;
|
||||
do
|
||||
{
|
||||
// Generate temp directories until we get a new one (usually happens on the first try)
|
||||
tempDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
|
||||
}
|
||||
while (File.Exists(tempDir));
|
||||
|
||||
Directory.CreateDirectory(tempDir);
|
||||
return tempDir;
|
||||
}
|
||||
|
||||
private sealed class DisposableReliableFileStorage : IDisposable
|
||||
{
|
||||
public DisposableReliableFileStorage(string rfsDbPath) => this.Instance = new(rfsDbPath);
|
||||
|
||||
public ReliableFileStorage Instance { get; }
|
||||
|
||||
public void Dispose() => ((IInternalDisposableService)this.Instance).DisposeService();
|
||||
}
|
||||
}
|
||||
|
|
@ -40,6 +40,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "DalamudCrashHandler", "Dala
|
|||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dalamud.Common", "Dalamud.Common\Dalamud.Common.csproj", "{F21B13D2-D7D0-4456-B70F-3F8D695064E2}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dalamud.LocExporter", "tools\Dalamud.LocExporter\Dalamud.LocExporter.csproj", "{A568929D-6FF6-4DFA-9D14-5D7DC08FA5E0}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
|
@ -102,6 +104,10 @@ Global
|
|||
{F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A568929D-6FF6-4DFA-9D14-5D7DC08FA5E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A568929D-6FF6-4DFA-9D14-5D7DC08FA5E0}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A568929D-6FF6-4DFA-9D14-5D7DC08FA5E0}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A568929D-6FF6-4DFA-9D14-5D7DC08FA5E0}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
using Dalamud.Game.Text;
|
||||
using Dalamud.Interface.FontIdentifier;
|
||||
|
|
@ -26,7 +28,7 @@ namespace Dalamud.Configuration.Internal;
|
|||
#pragma warning disable SA1015
|
||||
[InherentDependency<ReliableFileStorage>] // We must still have this when unloading
|
||||
#pragma warning restore SA1015
|
||||
internal sealed class DalamudConfiguration : IServiceType, IDisposable
|
||||
internal sealed class DalamudConfiguration : IInternalDisposableService
|
||||
{
|
||||
private static readonly JsonSerializerSettings SerializerSettings = new()
|
||||
{
|
||||
|
|
@ -367,6 +369,11 @@ internal sealed class DalamudConfiguration : IServiceType, IDisposable
|
|||
/// </summary>
|
||||
public bool ShowTsm { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to reduce motions (animations).
|
||||
/// </summary>
|
||||
public bool? ReduceMotions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether or not market board data should be uploaded.
|
||||
/// </summary>
|
||||
|
|
@ -484,6 +491,15 @@ internal sealed class DalamudConfiguration : IServiceType, IDisposable
|
|||
|
||||
deserialized ??= new DalamudConfiguration();
|
||||
deserialized.configPath = path;
|
||||
|
||||
try
|
||||
{
|
||||
deserialized.SetDefaults();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Failed to set defaults for DalamudConfiguration");
|
||||
}
|
||||
|
||||
return deserialized;
|
||||
}
|
||||
|
|
@ -505,7 +521,7 @@ internal sealed class DalamudConfiguration : IServiceType, IDisposable
|
|||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
void IInternalDisposableService.DisposeService()
|
||||
{
|
||||
// Make sure that we save, if a save is queued while we are shutting down
|
||||
this.Update();
|
||||
|
|
@ -525,6 +541,31 @@ internal sealed class DalamudConfiguration : IServiceType, IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
private void SetDefaults()
|
||||
{
|
||||
// "Reduced motion"
|
||||
if (!this.ReduceMotions.HasValue)
|
||||
{
|
||||
// https://source.chromium.org/chromium/chromium/src/+/main:ui/gfx/animation/animation_win.cc;l=29?q=ReducedMotion&ss=chromium
|
||||
var winAnimEnabled = 0;
|
||||
var success = NativeFunctions.SystemParametersInfo(
|
||||
(uint)NativeFunctions.AccessibilityParameter.SPI_GETCLIENTAREAANIMATION,
|
||||
0,
|
||||
ref winAnimEnabled,
|
||||
0);
|
||||
|
||||
if (!success)
|
||||
{
|
||||
Log.Warning("Failed to get Windows animation setting, assuming reduced motion is off (GetLastError: {GetLastError:X})", Marshal.GetLastPInvokeError());
|
||||
this.ReduceMotions = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.ReduceMotions = winAnimEnabled == 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Save()
|
||||
{
|
||||
ThreadSafety.AssertMainThread();
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Dalamud.Configuration.Internal;
|
||||
|
||||
|
|
@ -21,4 +22,9 @@ internal sealed class DevPluginSettings
|
|||
/// Gets or sets an ID uniquely identifying this specific instance of a devPlugin.
|
||||
/// </summary>
|
||||
public Guid WorkingPluginId { get; set; } = Guid.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a list of validation problems that have been dismissed by the user.
|
||||
/// </summary>
|
||||
public List<string> DismissedValidationProblems { get; set; } = new();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ using System.Threading.Tasks;
|
|||
using Dalamud.Common;
|
||||
using Dalamud.Configuration.Internal;
|
||||
using Dalamud.Game;
|
||||
using Dalamud.Interface.Internal;
|
||||
using Dalamud.Plugin.Internal;
|
||||
using Dalamud.Storage;
|
||||
using Dalamud.Utility;
|
||||
|
|
@ -187,27 +186,6 @@ internal sealed class Dalamud : IServiceType
|
|||
this.unloadSignal.WaitOne();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose subsystems related to plugin handling.
|
||||
/// </summary>
|
||||
public void DisposePlugins()
|
||||
{
|
||||
// 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>
|
||||
/// Replace the current exception handler with the default one.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup Label="Target">
|
||||
<TargetFramework>net7.0-windows</TargetFramework>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
<Platforms>x64;AnyCPU</Platforms>
|
||||
<LangVersion>11.0</LangVersion>
|
||||
<LangVersion>12.0</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Label="Feature">
|
||||
<DalamudVersion>9.0.0.21</DalamudVersion>
|
||||
<DalamudVersion>9.1.0.5</DalamudVersion>
|
||||
<Description>XIV Launcher addon framework</Description>
|
||||
<AssemblyVersion>$(DalamudVersion)</AssemblyVersion>
|
||||
<Version>$(DalamudVersion)</Version>
|
||||
|
|
@ -76,7 +76,6 @@
|
|||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="MinSharp" Version="1.0.4" />
|
||||
<PackageReference Include="MonoModReorg.RuntimeDetour" Version="23.1.2-prerelease.1" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
|
||||
<PackageReference Include="Serilog" Version="2.11.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Async" Version="1.5.0" />
|
||||
|
|
@ -113,10 +112,6 @@
|
|||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Game\Addon\" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="AddRuntimeDependenciesToContent" BeforeTargets="GetCopyToOutputDirectoryItems" DependsOnTargets="GenerateBuildDependencyFile;GenerateBuildRuntimeConfigurationFiles">
|
||||
<ItemGroup>
|
||||
<ContentWithTargetPath Include="$(ProjectDepsFilePath)" CopyToOutputDirectory="PreserveNewest" TargetPath="$(ProjectDepsFileName)" />
|
||||
|
|
@ -124,14 +119,6 @@
|
|||
</ItemGroup>
|
||||
</Target>
|
||||
|
||||
<Target Name="ChangeAliasesOfNugetRefs" BeforeTargets="FindReferenceAssembliesForReferences;ResolveReferences">
|
||||
<ItemGroup>
|
||||
<ReferencePath Condition="'%(FileName)' == 'MonoMod.Iced'">
|
||||
<Aliases>monomod</Aliases>
|
||||
</ReferencePath>
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- Needed temporarily for CI -->
|
||||
<TempVerFile>$(OutputPath)TEMP_gitver.txt</TempVerFile>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
|
|
@ -9,7 +7,6 @@ using Dalamud.IoC.Internal;
|
|||
using Dalamud.Plugin.Services;
|
||||
using Dalamud.Utility;
|
||||
using Dalamud.Utility.Timing;
|
||||
using JetBrains.Annotations;
|
||||
using Lumina;
|
||||
using Lumina.Data;
|
||||
using Lumina.Excel;
|
||||
|
|
@ -23,11 +20,11 @@ namespace Dalamud.Data;
|
|||
/// </summary>
|
||||
[PluginInterface]
|
||||
[InterfaceVersion("1.0")]
|
||||
[ServiceManager.BlockingEarlyLoadedService]
|
||||
[ServiceManager.EarlyLoadedService]
|
||||
#pragma warning disable SA1015
|
||||
[ResolveVia<IDataManager>]
|
||||
#pragma warning restore SA1015
|
||||
internal sealed class DataManager : IDisposable, IServiceType, IDataManager
|
||||
internal sealed class DataManager : IInternalDisposableService, IDataManager
|
||||
{
|
||||
private readonly Thread luminaResourceThread;
|
||||
private readonly CancellationTokenSource luminaCancellationTokenSource;
|
||||
|
|
@ -76,6 +73,9 @@ internal sealed class DataManager : IDisposable, IServiceType, IDataManager
|
|||
dalamud.StartInfo.TroubleshootingPackData);
|
||||
this.HasModifiedGameDataFiles =
|
||||
tsInfo?.IndexIntegrity is LauncherTroubleshootingInfo.IndexIntegrityResult.Failed or LauncherTroubleshootingInfo.IndexIntegrityResult.Exception;
|
||||
|
||||
if (this.HasModifiedGameDataFiles)
|
||||
Log.Verbose("Game data integrity check failed!\n{TsData}", dalamud.StartInfo.TroubleshootingPackData);
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
|
@ -158,7 +158,7 @@ internal sealed class DataManager : IDisposable, IServiceType, IDataManager
|
|||
#endregion
|
||||
|
||||
/// <inheritdoc/>
|
||||
void IDisposable.Dispose()
|
||||
void IInternalDisposableService.DisposeService()
|
||||
{
|
||||
this.luminaCancellationTokenSource.Cancel();
|
||||
}
|
||||
|
|
@ -175,6 +175,6 @@ internal sealed class DataManager : IDisposable, IServiceType, IDataManager
|
|||
Success,
|
||||
}
|
||||
|
||||
public IndexIntegrityResult IndexIntegrity { get; set; }
|
||||
public IndexIntegrityResult? IndexIntegrity { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -147,8 +147,16 @@ public sealed class EntryPoint
|
|||
LogLevelSwitch.MinimumLevel = configuration.LogLevel;
|
||||
|
||||
// Log any unhandled exception.
|
||||
if (!info.NoExceptionHandlers)
|
||||
AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
|
||||
switch (info.UnhandledException)
|
||||
{
|
||||
case UnhandledExceptionHandlingMode.Default:
|
||||
AppDomain.CurrentDomain.UnhandledException += OnUnhandledExceptionDefault;
|
||||
break;
|
||||
case UnhandledExceptionHandlingMode.StallDebug:
|
||||
AppDomain.CurrentDomain.UnhandledException += OnUnhandledExceptionStallDebug;
|
||||
break;
|
||||
}
|
||||
|
||||
TaskScheduler.UnobservedTaskException += OnUnobservedTaskException;
|
||||
|
||||
var unloadFailed = false;
|
||||
|
|
@ -197,8 +205,15 @@ public sealed class EntryPoint
|
|||
finally
|
||||
{
|
||||
TaskScheduler.UnobservedTaskException -= OnUnobservedTaskException;
|
||||
if (!info.NoExceptionHandlers)
|
||||
AppDomain.CurrentDomain.UnhandledException -= OnUnhandledException;
|
||||
switch (info.UnhandledException)
|
||||
{
|
||||
case UnhandledExceptionHandlingMode.Default:
|
||||
AppDomain.CurrentDomain.UnhandledException -= OnUnhandledExceptionDefault;
|
||||
break;
|
||||
case UnhandledExceptionHandlingMode.StallDebug:
|
||||
AppDomain.CurrentDomain.UnhandledException -= OnUnhandledExceptionStallDebug;
|
||||
break;
|
||||
}
|
||||
|
||||
Log.Information("Session has ended.");
|
||||
Log.CloseAndFlush();
|
||||
|
|
@ -246,7 +261,7 @@ public sealed class EntryPoint
|
|||
}
|
||||
}
|
||||
|
||||
private static void OnUnhandledException(object sender, UnhandledExceptionEventArgs args)
|
||||
private static void OnUnhandledExceptionDefault(object sender, UnhandledExceptionEventArgs args)
|
||||
{
|
||||
switch (args.ExceptionObject)
|
||||
{
|
||||
|
|
@ -306,6 +321,12 @@ public sealed class EntryPoint
|
|||
}
|
||||
}
|
||||
|
||||
private static void OnUnhandledExceptionStallDebug(object sender, UnhandledExceptionEventArgs args)
|
||||
{
|
||||
while (!Debugger.IsAttached)
|
||||
Thread.Sleep(100);
|
||||
}
|
||||
|
||||
private static void OnUnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs args)
|
||||
{
|
||||
if (!args.Observed)
|
||||
|
|
|
|||
107
Dalamud/Game/Addon/AddonLifecyclePooledArgs.cs
Normal file
107
Dalamud/Game/Addon/AddonLifecyclePooledArgs.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -18,8 +18,8 @@ namespace Dalamud.Game.Addon.Events;
|
|||
/// Service provider for addon event management.
|
||||
/// </summary>
|
||||
[InterfaceVersion("1.0")]
|
||||
[ServiceManager.BlockingEarlyLoadedService]
|
||||
internal unsafe class AddonEventManager : IDisposable, IServiceType
|
||||
[ServiceManager.EarlyLoadedService]
|
||||
internal unsafe class AddonEventManager : IInternalDisposableService
|
||||
{
|
||||
/// <summary>
|
||||
/// PluginName for Dalamud Internal use.
|
||||
|
|
@ -62,7 +62,7 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType
|
|||
private delegate nint UpdateCursorDelegate(RaptureAtkModule* module);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
void IInternalDisposableService.DisposeService()
|
||||
{
|
||||
this.onUpdateCursor.Dispose();
|
||||
|
||||
|
|
@ -204,7 +204,7 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType
|
|||
#pragma warning disable SA1015
|
||||
[ResolveVia<IAddonEventManager>]
|
||||
#pragma warning restore SA1015
|
||||
internal class AddonEventManagerPluginScoped : IDisposable, IServiceType, IAddonEventManager
|
||||
internal class AddonEventManagerPluginScoped : IInternalDisposableService, IAddonEventManager
|
||||
{
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly AddonEventManager eventManagerService = Service<AddonEventManager>.Get();
|
||||
|
|
@ -225,7 +225,7 @@ internal class AddonEventManagerPluginScoped : IDisposable, IServiceType, IAddon
|
|||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
void IInternalDisposableService.DisposeService()
|
||||
{
|
||||
// if multiple plugins force cursors and dispose without un-forcing them then all forces will be cleared.
|
||||
if (this.isForcingCursor)
|
||||
|
|
|
|||
|
|
@ -44,10 +44,10 @@ public abstract unsafe class AddonArgs
|
|||
get => this.addon;
|
||||
set
|
||||
{
|
||||
if (this.addon == value)
|
||||
return;
|
||||
|
||||
this.addon = value;
|
||||
|
||||
// Note: always clear addonName on updating the addon being pointed.
|
||||
// Same address may point to a different addon.
|
||||
this.addonName = null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
|
@ -19,14 +18,17 @@ namespace Dalamud.Game.Addon.Lifecycle;
|
|||
/// This class provides events for in-game addon lifecycles.
|
||||
/// </summary>
|
||||
[InterfaceVersion("1.0")]
|
||||
[ServiceManager.BlockingEarlyLoadedService]
|
||||
internal unsafe class AddonLifecycle : IDisposable, IServiceType
|
||||
[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;
|
||||
|
|
@ -38,18 +40,6 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
|
|||
private readonly Hook<AddonOnRefreshDelegate> onAddonRefreshHook;
|
||||
private readonly CallHook<AddonOnRequestedUpdateDelegate> onAddonRequestedUpdateHook;
|
||||
|
||||
// Note: these can be sourced from ObjectPool of appropriate types instead, but since we don't import that NuGet
|
||||
// package, and these events are always called from the main thread, this is fine.
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
// TODO: turn constructors of these internal
|
||||
private readonly AddonSetupArgs recyclingSetupArgs = new();
|
||||
private readonly AddonFinalizeArgs recyclingFinalizeArgs = new();
|
||||
private readonly AddonDrawArgs recyclingDrawArgs = new();
|
||||
private readonly AddonUpdateArgs recyclingUpdateArgs = new();
|
||||
private readonly AddonRefreshArgs recyclingRefreshArgs = new();
|
||||
private readonly AddonRequestedUpdateArgs recyclingRequestedUpdateArgs = new();
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
|
||||
[ServiceManager.ServiceConstructor]
|
||||
private AddonLifecycle(TargetSigScanner sigScanner)
|
||||
{
|
||||
|
|
@ -99,7 +89,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
|
|||
internal List<AddonLifecycleEventListener> EventListeners { get; } = new();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
void IInternalDisposableService.DisposeService()
|
||||
{
|
||||
this.onAddonSetupHook.Dispose();
|
||||
this.onAddonSetup2Hook.Dispose();
|
||||
|
|
@ -253,12 +243,13 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
|
|||
Log.Error(e, "Exception in OnAddonSetup ReceiveEvent Registration.");
|
||||
}
|
||||
|
||||
this.recyclingSetupArgs.AddonInternal = (nint)addon;
|
||||
this.recyclingSetupArgs.AtkValueCount = valueCount;
|
||||
this.recyclingSetupArgs.AtkValues = (nint)values;
|
||||
this.InvokeListenersSafely(AddonEvent.PreSetup, this.recyclingSetupArgs);
|
||||
valueCount = this.recyclingSetupArgs.AtkValueCount;
|
||||
values = (AtkValue*)this.recyclingSetupArgs.AtkValues;
|
||||
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
|
||||
{
|
||||
|
|
@ -269,7 +260,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
|
|||
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, this.recyclingSetupArgs);
|
||||
this.InvokeListenersSafely(AddonEvent.PostSetup, arg);
|
||||
}
|
||||
|
||||
private void OnAddonFinalize(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase)
|
||||
|
|
@ -284,8 +275,9 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
|
|||
Log.Error(e, "Exception in OnAddonFinalize ReceiveEvent Removal.");
|
||||
}
|
||||
|
||||
this.recyclingFinalizeArgs.AddonInternal = (nint)atkUnitBase[0];
|
||||
this.InvokeListenersSafely(AddonEvent.PreFinalize, this.recyclingFinalizeArgs);
|
||||
using var returner = this.argsPool.Rent(out AddonFinalizeArgs arg);
|
||||
arg.AddonInternal = (nint)atkUnitBase[0];
|
||||
this.InvokeListenersSafely(AddonEvent.PreFinalize, arg);
|
||||
|
||||
try
|
||||
{
|
||||
|
|
@ -299,8 +291,9 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
|
|||
|
||||
private void OnAddonDraw(AtkUnitBase* addon)
|
||||
{
|
||||
this.recyclingDrawArgs.AddonInternal = (nint)addon;
|
||||
this.InvokeListenersSafely(AddonEvent.PreDraw, this.recyclingDrawArgs);
|
||||
using var returner = this.argsPool.Rent(out AddonDrawArgs arg);
|
||||
arg.AddonInternal = (nint)addon;
|
||||
this.InvokeListenersSafely(AddonEvent.PreDraw, arg);
|
||||
|
||||
try
|
||||
{
|
||||
|
|
@ -311,14 +304,15 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
|
|||
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, this.recyclingDrawArgs);
|
||||
this.InvokeListenersSafely(AddonEvent.PostDraw, arg);
|
||||
}
|
||||
|
||||
private void OnAddonUpdate(AtkUnitBase* addon, float delta)
|
||||
{
|
||||
this.recyclingUpdateArgs.AddonInternal = (nint)addon;
|
||||
this.recyclingUpdateArgs.TimeDeltaInternal = delta;
|
||||
this.InvokeListenersSafely(AddonEvent.PreUpdate, this.recyclingUpdateArgs);
|
||||
using var returner = this.argsPool.Rent(out AddonUpdateArgs arg);
|
||||
arg.AddonInternal = (nint)addon;
|
||||
arg.TimeDeltaInternal = delta;
|
||||
this.InvokeListenersSafely(AddonEvent.PreUpdate, arg);
|
||||
|
||||
try
|
||||
{
|
||||
|
|
@ -329,19 +323,20 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
|
|||
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, this.recyclingUpdateArgs);
|
||||
this.InvokeListenersSafely(AddonEvent.PostUpdate, arg);
|
||||
}
|
||||
|
||||
private byte OnAddonRefresh(AtkUnitManager* atkUnitManager, AtkUnitBase* addon, uint valueCount, AtkValue* values)
|
||||
{
|
||||
byte result = 0;
|
||||
|
||||
this.recyclingRefreshArgs.AddonInternal = (nint)addon;
|
||||
this.recyclingRefreshArgs.AtkValueCount = valueCount;
|
||||
this.recyclingRefreshArgs.AtkValues = (nint)values;
|
||||
this.InvokeListenersSafely(AddonEvent.PreRefresh, this.recyclingRefreshArgs);
|
||||
valueCount = this.recyclingRefreshArgs.AtkValueCount;
|
||||
values = (AtkValue*)this.recyclingRefreshArgs.AtkValues;
|
||||
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
|
||||
{
|
||||
|
|
@ -352,18 +347,19 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
|
|||
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, this.recyclingRefreshArgs);
|
||||
this.InvokeListenersSafely(AddonEvent.PostRefresh, arg);
|
||||
return result;
|
||||
}
|
||||
|
||||
private void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData)
|
||||
{
|
||||
this.recyclingRequestedUpdateArgs.AddonInternal = (nint)addon;
|
||||
this.recyclingRequestedUpdateArgs.NumberArrayData = (nint)numberArrayData;
|
||||
this.recyclingRequestedUpdateArgs.StringArrayData = (nint)stringArrayData;
|
||||
this.InvokeListenersSafely(AddonEvent.PreRequestedUpdate, this.recyclingRequestedUpdateArgs);
|
||||
numberArrayData = (NumberArrayData**)this.recyclingRequestedUpdateArgs.NumberArrayData;
|
||||
stringArrayData = (StringArrayData**)this.recyclingRequestedUpdateArgs.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
|
||||
{
|
||||
|
|
@ -374,7 +370,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
|
|||
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, this.recyclingRequestedUpdateArgs);
|
||||
this.InvokeListenersSafely(AddonEvent.PostRequestedUpdate, arg);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -387,7 +383,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
|
|||
#pragma warning disable SA1015
|
||||
[ResolveVia<IAddonLifecycle>]
|
||||
#pragma warning restore SA1015
|
||||
internal class AddonLifecyclePluginScoped : IDisposable, IServiceType, IAddonLifecycle
|
||||
internal class AddonLifecyclePluginScoped : IInternalDisposableService, IAddonLifecycle
|
||||
{
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly AddonLifecycle addonLifecycleService = Service<AddonLifecycle>.Get();
|
||||
|
|
@ -395,7 +391,7 @@ internal class AddonLifecyclePluginScoped : IDisposable, IServiceType, IAddonLif
|
|||
private readonly List<AddonLifecycleEventListener> eventListeners = new();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
void IInternalDisposableService.DisposeService()
|
||||
{
|
||||
foreach (var listener in this.eventListeners)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -16,12 +16,8 @@ internal unsafe class AddonLifecycleReceiveEventListener : IDisposable
|
|||
{
|
||||
private static readonly ModuleLog Log = new("AddonLifecycle");
|
||||
|
||||
// Note: these can be sourced from ObjectPool of appropriate types instead, but since we don't import that NuGet
|
||||
// package, and these events are always called from the main thread, this is fine.
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
// TODO: turn constructors of these internal
|
||||
private readonly AddonReceiveEventArgs recyclingReceiveEventArgs = new();
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly AddonLifecyclePooledArgs argsPool = Service<AddonLifecyclePooledArgs>.Get();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AddonLifecycleReceiveEventListener"/> class.
|
||||
|
|
@ -82,16 +78,17 @@ internal unsafe class AddonLifecycleReceiveEventListener : IDisposable
|
|||
return;
|
||||
}
|
||||
|
||||
this.recyclingReceiveEventArgs.AddonInternal = (nint)addon;
|
||||
this.recyclingReceiveEventArgs.AtkEventType = (byte)eventType;
|
||||
this.recyclingReceiveEventArgs.EventParam = eventParam;
|
||||
this.recyclingReceiveEventArgs.AtkEvent = (IntPtr)atkEvent;
|
||||
this.recyclingReceiveEventArgs.Data = data;
|
||||
this.AddonLifecycle.InvokeListenersSafely(AddonEvent.PreReceiveEvent, this.recyclingReceiveEventArgs);
|
||||
eventType = (AtkEventType)this.recyclingReceiveEventArgs.AtkEventType;
|
||||
eventParam = this.recyclingReceiveEventArgs.EventParam;
|
||||
atkEvent = (AtkEvent*)this.recyclingReceiveEventArgs.AtkEvent;
|
||||
data = this.recyclingReceiveEventArgs.Data;
|
||||
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
|
||||
{
|
||||
|
|
@ -102,6 +99,6 @@ internal unsafe class AddonLifecycleReceiveEventListener : IDisposable
|
|||
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, this.recyclingReceiveEventArgs);
|
||||
this.AddonLifecycle.InvokeListenersSafely(AddonEvent.PostReceiveEvent, arg);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,12 +7,14 @@ using System.Threading.Tasks;
|
|||
|
||||
using CheapLoc;
|
||||
using Dalamud.Configuration.Internal;
|
||||
using Dalamud.Game.ClientState.Conditions;
|
||||
using Dalamud.Game.Gui;
|
||||
using Dalamud.Game.Text;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
||||
using Dalamud.Interface.ImGuiNotification;
|
||||
using Dalamud.Interface.ImGuiNotification.Internal;
|
||||
using Dalamud.Interface.Internal;
|
||||
using Dalamud.Interface.Internal.Notifications;
|
||||
using Dalamud.Interface.Internal.Windows;
|
||||
using Dalamud.Interface.Internal.Windows.PluginInstaller;
|
||||
using Dalamud.Logging.Internal;
|
||||
|
|
@ -24,7 +26,7 @@ namespace Dalamud.Game;
|
|||
/// <summary>
|
||||
/// Chat events and public helper functions.
|
||||
/// </summary>
|
||||
[ServiceManager.BlockingEarlyLoadedService]
|
||||
[ServiceManager.EarlyLoadedService]
|
||||
internal class ChatHandlers : IServiceType
|
||||
{
|
||||
// private static readonly Dictionary<string, string> UnicodeToDiscordEmojiDict = new()
|
||||
|
|
@ -289,13 +291,20 @@ internal class ChatHandlers : IServiceType
|
|||
var chatGui = Service<ChatGui>.GetNullable();
|
||||
var pluginManager = Service<PluginManager>.GetNullable();
|
||||
var notifications = Service<NotificationManager>.GetNullable();
|
||||
var condition = Service<Condition>.GetNullable();
|
||||
|
||||
if (chatGui == null || pluginManager == null || notifications == null)
|
||||
if (chatGui == null || pluginManager == null || notifications == null || condition == null)
|
||||
{
|
||||
Log.Warning("Aborting auto-update because a required service was not loaded.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (condition.Any(ConditionFlag.BoundByDuty, ConditionFlag.BoundByDuty56, ConditionFlag.BoundByDuty95))
|
||||
{
|
||||
Log.Warning("Aborting auto-update because the player is in a duty.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!pluginManager.ReposReady || !pluginManager.InstalledPlugins.Any() || !pluginManager.AvailablePlugins.Any())
|
||||
{
|
||||
// Plugins aren't ready yet.
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ namespace Dalamud.Game.ClientState.Aetherytes;
|
|||
/// </summary>
|
||||
[PluginInterface]
|
||||
[InterfaceVersion("1.0")]
|
||||
[ServiceManager.BlockingEarlyLoadedService]
|
||||
[ServiceManager.EarlyLoadedService]
|
||||
#pragma warning disable SA1015
|
||||
[ResolveVia<IAetheryteList>]
|
||||
#pragma warning restore SA1015
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ namespace Dalamud.Game.ClientState.Buddy;
|
|||
/// </summary>
|
||||
[PluginInterface]
|
||||
[InterfaceVersion("1.0")]
|
||||
[ServiceManager.BlockingEarlyLoadedService]
|
||||
[ServiceManager.EarlyLoadedService]
|
||||
#pragma warning disable SA1015
|
||||
[ResolveVia<IBuddyList>]
|
||||
#pragma warning restore SA1015
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ using Dalamud.Logging.Internal;
|
|||
using Dalamud.Plugin.Services;
|
||||
using Dalamud.Utility;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
|
||||
using Lumina.Excel.GeneratedSheets;
|
||||
|
||||
using Action = System.Action;
|
||||
|
|
@ -22,8 +24,8 @@ namespace Dalamud.Game.ClientState;
|
|||
/// This class represents the state of the game client at the time of access.
|
||||
/// </summary>
|
||||
[InterfaceVersion("1.0")]
|
||||
[ServiceManager.BlockingEarlyLoadedService]
|
||||
internal sealed class ClientState : IDisposable, IServiceType, IClientState
|
||||
[ServiceManager.EarlyLoadedService]
|
||||
internal sealed class ClientState : IInternalDisposableService, IClientState
|
||||
{
|
||||
private static readonly ModuleLog Log = new("ClientState");
|
||||
|
||||
|
|
@ -89,6 +91,16 @@ internal sealed class ClientState : IDisposable, IServiceType, IClientState
|
|||
/// <inheritdoc/>
|
||||
public ushort TerritoryType { get; private set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public unsafe uint MapId
|
||||
{
|
||||
get
|
||||
{
|
||||
var agentMap = AgentMap.Instance();
|
||||
return agentMap != null ? AgentMap.Instance()->CurrentMapId : 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public PlayerCharacter? LocalPlayer => Service<ObjectTable>.GetNullable()?[0] as PlayerCharacter;
|
||||
|
||||
|
|
@ -115,7 +127,7 @@ internal sealed class ClientState : IDisposable, IServiceType, IClientState
|
|||
/// <summary>
|
||||
/// Dispose of managed and unmanaged resources.
|
||||
/// </summary>
|
||||
void IDisposable.Dispose()
|
||||
void IInternalDisposableService.DisposeService()
|
||||
{
|
||||
this.setupTerritoryTypeHook.Dispose();
|
||||
this.framework.Update -= this.FrameworkOnOnUpdateEvent;
|
||||
|
|
@ -196,7 +208,7 @@ internal sealed class ClientState : IDisposable, IServiceType, IClientState
|
|||
#pragma warning disable SA1015
|
||||
[ResolveVia<IClientState>]
|
||||
#pragma warning restore SA1015
|
||||
internal class ClientStatePluginScoped : IDisposable, IServiceType, IClientState
|
||||
internal class ClientStatePluginScoped : IInternalDisposableService, IClientState
|
||||
{
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly ClientState clientStateService = Service<ClientState>.Get();
|
||||
|
|
@ -237,6 +249,9 @@ internal class ClientStatePluginScoped : IDisposable, IServiceType, IClientState
|
|||
|
||||
/// <inheritdoc/>
|
||||
public ushort TerritoryType => this.clientStateService.TerritoryType;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public uint MapId => this.clientStateService.MapId;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public PlayerCharacter? LocalPlayer => this.clientStateService.LocalPlayer;
|
||||
|
|
@ -257,7 +272,7 @@ internal class ClientStatePluginScoped : IDisposable, IServiceType, IClientState
|
|||
public bool IsGPosing => this.clientStateService.IsGPosing;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
void IInternalDisposableService.DisposeService()
|
||||
{
|
||||
this.clientStateService.TerritoryChanged -= this.TerritoryChangedForward;
|
||||
this.clientStateService.Login -= this.LoginForward;
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ namespace Dalamud.Game.ClientState.Conditions;
|
|||
/// Provides access to conditions (generally player state). You can check whether a player is in combat, mounted, etc.
|
||||
/// </summary>
|
||||
[InterfaceVersion("1.0")]
|
||||
[ServiceManager.BlockingEarlyLoadedService]
|
||||
internal sealed partial class Condition : IServiceType, ICondition
|
||||
[ServiceManager.EarlyLoadedService]
|
||||
internal sealed class Condition : IInternalDisposableService, ICondition
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the current max number of conditions. You can get this just by looking at the condition sheet and how many rows it has.
|
||||
|
|
@ -22,6 +22,8 @@ internal sealed partial class Condition : IServiceType, ICondition
|
|||
|
||||
private readonly bool[] cache = new bool[MaxConditionEntries];
|
||||
|
||||
private bool isDisposed;
|
||||
|
||||
[ServiceManager.ServiceConstructor]
|
||||
private Condition(ClientState clientState)
|
||||
{
|
||||
|
|
@ -35,6 +37,9 @@ internal sealed partial class Condition : IServiceType, ICondition
|
|||
this.framework.Update += this.FrameworkUpdate;
|
||||
}
|
||||
|
||||
/// <summary>Finalizes an instance of the <see cref="Condition" /> class.</summary>
|
||||
~Condition() => this.Dispose(false);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event ICondition.ConditionChangeDelegate? ConditionChange;
|
||||
|
||||
|
|
@ -60,6 +65,9 @@ internal sealed partial class Condition : IServiceType, ICondition
|
|||
public bool this[ConditionFlag flag]
|
||||
=> this[(int)flag];
|
||||
|
||||
/// <inheritdoc/>
|
||||
void IInternalDisposableService.DisposeService() => this.Dispose(true);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Any()
|
||||
{
|
||||
|
|
@ -89,6 +97,19 @@ internal sealed partial class Condition : IServiceType, ICondition
|
|||
return false;
|
||||
}
|
||||
|
||||
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++)
|
||||
|
|
@ -112,44 +133,6 @@ internal sealed partial class Condition : IServiceType, ICondition
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides access to conditions (generally player state). You can check whether a player is in combat, mounted, etc.
|
||||
/// </summary>
|
||||
internal sealed partial class Condition : IDisposable
|
||||
{
|
||||
private bool isDisposed;
|
||||
|
||||
/// <summary>
|
||||
/// Finalizes an instance of the <see cref="Condition" /> class.
|
||||
/// </summary>
|
||||
~Condition()
|
||||
{
|
||||
this.Dispose(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes this instance, alongside its hooks.
|
||||
/// </summary>
|
||||
void IDisposable.Dispose()
|
||||
{
|
||||
GC.SuppressFinalize(this);
|
||||
this.Dispose(true);
|
||||
}
|
||||
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (this.isDisposed)
|
||||
return;
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
this.framework.Update -= this.FrameworkUpdate;
|
||||
}
|
||||
|
||||
this.isDisposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plugin-scoped version of a Condition service.
|
||||
/// </summary>
|
||||
|
|
@ -159,7 +142,7 @@ internal sealed partial class Condition : IDisposable
|
|||
#pragma warning disable SA1015
|
||||
[ResolveVia<ICondition>]
|
||||
#pragma warning restore SA1015
|
||||
internal class ConditionPluginScoped : IDisposable, IServiceType, ICondition
|
||||
internal class ConditionPluginScoped : IInternalDisposableService, ICondition
|
||||
{
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly Condition conditionService = Service<Condition>.Get();
|
||||
|
|
@ -185,7 +168,7 @@ internal class ConditionPluginScoped : IDisposable, IServiceType, ICondition
|
|||
public bool this[int flag] => this.conditionService[flag];
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
void IInternalDisposableService.DisposeService()
|
||||
{
|
||||
this.conditionService.ConditionChange -= this.ConditionChangedForward;
|
||||
|
||||
|
|
|
|||
|
|
@ -428,7 +428,14 @@ public enum ConditionFlag
|
|||
/// <summary>
|
||||
/// Unable to execute command while bound by duty.
|
||||
/// </summary>
|
||||
[Obsolete("Use InDutyQueue")]
|
||||
BoundToDuty97 = 91,
|
||||
|
||||
/// <summary>
|
||||
/// Unable to execute command while bound by duty.
|
||||
/// Specifically triggered when you are in a queue for a duty but not inside a duty.
|
||||
/// </summary>
|
||||
InDutyQueue = 91,
|
||||
|
||||
/// <summary>
|
||||
/// Unable to execute command while readying to visit another World.
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ namespace Dalamud.Game.ClientState.Fates;
|
|||
/// </summary>
|
||||
[PluginInterface]
|
||||
[InterfaceVersion("1.0")]
|
||||
[ServiceManager.BlockingEarlyLoadedService]
|
||||
[ServiceManager.EarlyLoadedService]
|
||||
#pragma warning disable SA1015
|
||||
[ResolveVia<IFateTable>]
|
||||
#pragma warning restore SA1015
|
||||
|
|
|
|||
|
|
@ -17,11 +17,11 @@ namespace Dalamud.Game.ClientState.GamePad;
|
|||
/// </summary>
|
||||
[PluginInterface]
|
||||
[InterfaceVersion("1.0")]
|
||||
[ServiceManager.BlockingEarlyLoadedService]
|
||||
[ServiceManager.EarlyLoadedService]
|
||||
#pragma warning disable SA1015
|
||||
[ResolveVia<IGamepadState>]
|
||||
#pragma warning restore SA1015
|
||||
internal unsafe class GamepadState : IDisposable, IServiceType, IGamepadState
|
||||
internal unsafe class GamepadState : IInternalDisposableService, IGamepadState
|
||||
{
|
||||
private readonly Hook<ControllerPoll>? gamepadPoll;
|
||||
|
||||
|
|
@ -109,7 +109,7 @@ internal unsafe class GamepadState : IDisposable, IServiceType, IGamepadState
|
|||
/// <summary>
|
||||
/// Disposes this instance, alongside its hooks.
|
||||
/// </summary>
|
||||
void IDisposable.Dispose()
|
||||
void IInternalDisposableService.DisposeService()
|
||||
{
|
||||
this.Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ namespace Dalamud.Game.ClientState.JobGauge;
|
|||
/// </summary>
|
||||
[PluginInterface]
|
||||
[InterfaceVersion("1.0")]
|
||||
[ServiceManager.BlockingEarlyLoadedService]
|
||||
[ServiceManager.EarlyLoadedService]
|
||||
#pragma warning disable SA1015
|
||||
[ResolveVia<IJobGauges>]
|
||||
#pragma warning restore SA1015
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ namespace Dalamud.Game.ClientState.Keys;
|
|||
/// </remarks>
|
||||
[PluginInterface]
|
||||
[InterfaceVersion("1.0")]
|
||||
[ServiceManager.BlockingEarlyLoadedService]
|
||||
[ServiceManager.EarlyLoadedService]
|
||||
#pragma warning disable SA1015
|
||||
[ResolveVia<IKeyState>]
|
||||
#pragma warning restore SA1015
|
||||
|
|
|
|||
|
|
@ -1,15 +1,22 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
using Dalamud.Game.ClientState.Objects.Enums;
|
||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Dalamud.IoC;
|
||||
using Dalamud.IoC.Internal;
|
||||
using Dalamud.Plugin.Internal;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Dalamud.Utility;
|
||||
|
||||
using Microsoft.Extensions.ObjectPool;
|
||||
|
||||
using Serilog;
|
||||
|
||||
using CSGameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject;
|
||||
|
||||
namespace Dalamud.Game.ClientState.Objects;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -17,7 +24,7 @@ namespace Dalamud.Game.ClientState.Objects;
|
|||
/// </summary>
|
||||
[PluginInterface]
|
||||
[InterfaceVersion("1.0")]
|
||||
[ServiceManager.BlockingEarlyLoadedService]
|
||||
[ServiceManager.EarlyLoadedService]
|
||||
#pragma warning disable SA1015
|
||||
[ResolveVia<IObjectTable>]
|
||||
#pragma warning restore SA1015
|
||||
|
|
@ -25,18 +32,41 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable
|
|||
{
|
||||
private const int ObjectTableLength = 599;
|
||||
|
||||
private readonly ClientStateAddressResolver address;
|
||||
private readonly ClientState clientState;
|
||||
private readonly CachedEntry[] cachedObjectTable = new CachedEntry[ObjectTableLength];
|
||||
|
||||
private readonly ObjectPool<Enumerator> multiThreadedEnumerators =
|
||||
new DefaultObjectPoolProvider().Create<Enumerator>();
|
||||
|
||||
private readonly Enumerator?[] frameworkThreadEnumerators = new Enumerator?[4];
|
||||
|
||||
private long nextMultithreadedUsageWarnTime;
|
||||
|
||||
[ServiceManager.ServiceConstructor]
|
||||
private ObjectTable(ClientState clientState)
|
||||
private unsafe ObjectTable(ClientState clientState)
|
||||
{
|
||||
this.address = clientState.AddressResolver;
|
||||
this.clientState = clientState;
|
||||
|
||||
Log.Verbose($"Object table address 0x{this.address.ObjectTable.ToInt64():X}");
|
||||
var nativeObjectTableAddress = (CSGameObject**)this.clientState.AddressResolver.ObjectTable;
|
||||
for (var i = 0; i < this.cachedObjectTable.Length; i++)
|
||||
this.cachedObjectTable[i] = new(nativeObjectTableAddress, i);
|
||||
|
||||
for (var i = 0; i < this.frameworkThreadEnumerators.Length; i++)
|
||||
this.frameworkThreadEnumerators[i] = new(this, i);
|
||||
|
||||
Log.Verbose($"Object table address 0x{this.clientState.AddressResolver.ObjectTable.ToInt64():X}");
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IntPtr Address => this.address.ObjectTable;
|
||||
public nint Address
|
||||
{
|
||||
get
|
||||
{
|
||||
_ = this.WarnMultithreadedUsage();
|
||||
|
||||
return this.clientState.AddressResolver.ObjectTable;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int Length => ObjectTableLength;
|
||||
|
|
@ -46,50 +76,49 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable
|
|||
{
|
||||
get
|
||||
{
|
||||
var address = this.GetObjectAddress(index);
|
||||
return this.CreateObjectReference(address);
|
||||
_ = this.WarnMultithreadedUsage();
|
||||
|
||||
return index is >= ObjectTableLength or < 0 ? null : this.cachedObjectTable[index].Update();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public GameObject? SearchById(ulong objectId)
|
||||
{
|
||||
_ = this.WarnMultithreadedUsage();
|
||||
|
||||
if (objectId is GameObject.InvalidGameObjectId or 0)
|
||||
return null;
|
||||
|
||||
foreach (var obj in this)
|
||||
foreach (var e in this.cachedObjectTable)
|
||||
{
|
||||
if (obj == null)
|
||||
continue;
|
||||
|
||||
if (obj.ObjectId == objectId)
|
||||
return obj;
|
||||
if (e.Update() is { } o && o.ObjectId == objectId)
|
||||
return o;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public unsafe IntPtr GetObjectAddress(int index)
|
||||
public unsafe nint GetObjectAddress(int index)
|
||||
{
|
||||
if (index < 0 || index >= ObjectTableLength)
|
||||
return IntPtr.Zero;
|
||||
_ = this.WarnMultithreadedUsage();
|
||||
|
||||
return *(IntPtr*)(this.address.ObjectTable + (8 * index));
|
||||
return index is < 0 or >= ObjectTableLength ? nint.Zero : (nint)this.cachedObjectTable[index].Address;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public unsafe GameObject? CreateObjectReference(IntPtr address)
|
||||
public unsafe GameObject? CreateObjectReference(nint address)
|
||||
{
|
||||
var clientState = Service<ClientState>.GetNullable();
|
||||
_ = this.WarnMultithreadedUsage();
|
||||
|
||||
if (clientState == null || clientState.LocalContentId == 0)
|
||||
if (this.clientState.LocalContentId == 0)
|
||||
return null;
|
||||
|
||||
if (address == IntPtr.Zero)
|
||||
if (address == nint.Zero)
|
||||
return null;
|
||||
|
||||
var obj = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)address;
|
||||
var obj = (CSGameObject*)address;
|
||||
var objKind = (ObjectKind)obj->ObjectKind;
|
||||
return objKind switch
|
||||
{
|
||||
|
|
@ -104,6 +133,82 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable
|
|||
_ => new GameObject(address),
|
||||
};
|
||||
}
|
||||
|
||||
[Api10ToDo("Use ThreadSafety.AssertMainThread() instead of this.")]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private bool WarnMultithreadedUsage()
|
||||
{
|
||||
if (ThreadSafety.IsMainThread)
|
||||
return false;
|
||||
|
||||
var n = Environment.TickCount64;
|
||||
if (this.nextMultithreadedUsageWarnTime < n)
|
||||
{
|
||||
this.nextMultithreadedUsageWarnTime = n + 30000;
|
||||
|
||||
Log.Warning(
|
||||
"{plugin} is accessing {objectTable} outside the main thread. This is deprecated.",
|
||||
Service<PluginManager>.Get().FindCallingPlugin()?.Name ?? "<unknown plugin>",
|
||||
nameof(ObjectTable));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Stores an object table entry, with preallocated concrete types.</summary>
|
||||
internal readonly unsafe struct CachedEntry
|
||||
{
|
||||
private readonly CSGameObject** gameObjectPtrPtr;
|
||||
private readonly PlayerCharacter playerCharacter;
|
||||
private readonly BattleNpc battleNpc;
|
||||
private readonly Npc npc;
|
||||
private readonly EventObj eventObj;
|
||||
private readonly GameObject gameObject;
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="CachedEntry"/> struct.</summary>
|
||||
/// <param name="ownerTable">The object table that this entry should be pointing to.</param>
|
||||
/// <param name="slot">The slot index inside the table.</param>
|
||||
public CachedEntry(CSGameObject** ownerTable, int slot)
|
||||
{
|
||||
this.gameObjectPtrPtr = ownerTable + slot;
|
||||
this.playerCharacter = new(nint.Zero);
|
||||
this.battleNpc = new(nint.Zero);
|
||||
this.npc = new(nint.Zero);
|
||||
this.eventObj = new(nint.Zero);
|
||||
this.gameObject = new(nint.Zero);
|
||||
}
|
||||
|
||||
/// <summary>Gets the address of the underlying native object. May be null.</summary>
|
||||
public CSGameObject* Address
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => *this.gameObjectPtrPtr;
|
||||
}
|
||||
|
||||
/// <summary>Updates and gets the wrapped game object pointed by this struct.</summary>
|
||||
/// <returns>The pointed object, or <c>null</c> if no object exists at that slot.</returns>
|
||||
public GameObject? Update()
|
||||
{
|
||||
var address = this.Address;
|
||||
if (address is null)
|
||||
return null;
|
||||
|
||||
var activeObject = (ObjectKind)address->ObjectKind switch
|
||||
{
|
||||
ObjectKind.Player => this.playerCharacter,
|
||||
ObjectKind.BattleNpc => this.battleNpc,
|
||||
ObjectKind.EventNpc => this.npc,
|
||||
ObjectKind.Retainer => this.npc,
|
||||
ObjectKind.EventObj => this.eventObj,
|
||||
ObjectKind.Companion => this.npc,
|
||||
ObjectKind.MountType => this.npc,
|
||||
ObjectKind.Ornament => this.npc,
|
||||
_ => this.gameObject,
|
||||
};
|
||||
activeObject.Address = (nint)address;
|
||||
return activeObject;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -111,23 +216,93 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable
|
|||
/// </summary>
|
||||
internal sealed partial class ObjectTable
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
int IReadOnlyCollection<GameObject>.Count => this.Length;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerator<GameObject> GetEnumerator()
|
||||
{
|
||||
for (var i = 0; i < ObjectTableLength; i++)
|
||||
// If something's trying to enumerate outside the framework thread, we use the ObjectPool.
|
||||
if (this.WarnMultithreadedUsage())
|
||||
{
|
||||
var obj = this[i];
|
||||
|
||||
if (obj == null)
|
||||
continue;
|
||||
|
||||
yield return obj;
|
||||
// let's not
|
||||
var e = this.multiThreadedEnumerators.Get();
|
||||
e.InitializeForPooledObjects(this);
|
||||
return e;
|
||||
}
|
||||
|
||||
// If we're on the framework thread, see if there's an already allocated enumerator available for use.
|
||||
foreach (ref var x in this.frameworkThreadEnumerators.AsSpan())
|
||||
{
|
||||
if (x is not null)
|
||||
{
|
||||
var t = x;
|
||||
x = null;
|
||||
t.Reset();
|
||||
return t;
|
||||
}
|
||||
}
|
||||
|
||||
// No reusable enumerator is available; allocate a new temporary one.
|
||||
return new Enumerator(this, -1);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
|
||||
|
||||
private sealed class Enumerator : IEnumerator<GameObject>, IResettable
|
||||
{
|
||||
private readonly int slotId;
|
||||
private ObjectTable? owner;
|
||||
|
||||
private int index = -1;
|
||||
|
||||
public Enumerator() => this.slotId = -1;
|
||||
|
||||
public Enumerator(ObjectTable owner, int slotId)
|
||||
{
|
||||
this.owner = owner;
|
||||
this.slotId = slotId;
|
||||
}
|
||||
|
||||
public GameObject Current { get; private set; } = null!;
|
||||
|
||||
object IEnumerator.Current => this.Current;
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
if (this.index == ObjectTableLength)
|
||||
return false;
|
||||
|
||||
var cache = this.owner!.cachedObjectTable.AsSpan();
|
||||
for (this.index++; this.index < ObjectTableLength; this.index++)
|
||||
{
|
||||
if (cache[this.index].Update() is { } ao)
|
||||
{
|
||||
this.Current = ao;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void InitializeForPooledObjects(ObjectTable ot) => this.owner = ot;
|
||||
|
||||
public void Reset() => this.index = -1;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (this.owner is not { } o)
|
||||
return;
|
||||
|
||||
if (this.slotId == -1)
|
||||
o.multiThreadedEnumerators.Return(this);
|
||||
else
|
||||
o.frameworkThreadEnumerators[this.slotId] = this;
|
||||
}
|
||||
|
||||
public bool TryReset()
|
||||
{
|
||||
this.Reset();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,5 +31,5 @@ public unsafe class PlayerCharacter : BattleChara
|
|||
/// <summary>
|
||||
/// Gets the target actor ID of the PlayerCharacter.
|
||||
/// </summary>
|
||||
public override ulong TargetObjectId => this.Struct->Character.LookTargetId;
|
||||
public override ulong TargetObjectId => this.Struct->Character.Gaze.Controller.GazesSpan[0].TargetInfo.TargetId;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ namespace Dalamud.Game.ClientState.Objects;
|
|||
/// </summary>
|
||||
[PluginInterface]
|
||||
[InterfaceVersion("1.0")]
|
||||
[ServiceManager.BlockingEarlyLoadedService]
|
||||
[ServiceManager.EarlyLoadedService]
|
||||
#pragma warning disable SA1015
|
||||
[ResolveVia<ITargetManager>]
|
||||
#pragma warning restore SA1015
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using System;
|
||||
|
||||
using Dalamud.Game.ClientState.Statuses;
|
||||
using Dalamud.Utility;
|
||||
|
||||
namespace Dalamud.Game.ClientState.Objects.Types;
|
||||
|
||||
|
|
@ -57,8 +58,22 @@ public unsafe class BattleChara : Character
|
|||
/// <summary>
|
||||
/// Gets the total casting time of the spell being cast by the chara.
|
||||
/// </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;
|
||||
|
||||
/// <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>
|
||||
/// Gets the underlying structure.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ public unsafe partial class GameObject : IEquatable<GameObject>
|
|||
/// <summary>
|
||||
/// Gets the address of the game object in memory.
|
||||
/// </summary>
|
||||
public IntPtr Address { get; }
|
||||
public IntPtr Address { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Dalamud instance.
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ namespace Dalamud.Game.ClientState.Party;
|
|||
/// </summary>
|
||||
[PluginInterface]
|
||||
[InterfaceVersion("1.0")]
|
||||
[ServiceManager.BlockingEarlyLoadedService]
|
||||
[ServiceManager.EarlyLoadedService]
|
||||
#pragma warning disable SA1015
|
||||
[ResolveVia<IPartyList>]
|
||||
#pragma warning restore SA1015
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@ namespace Dalamud.Game.Command;
|
|||
/// This class manages registered in-game slash commands.
|
||||
/// </summary>
|
||||
[InterfaceVersion("1.0")]
|
||||
[ServiceManager.BlockingEarlyLoadedService]
|
||||
internal sealed class CommandManager : IServiceType, IDisposable, ICommandManager
|
||||
[ServiceManager.EarlyLoadedService]
|
||||
internal sealed class CommandManager : IInternalDisposableService, ICommandManager
|
||||
{
|
||||
private static readonly ModuleLog Log = new("Command");
|
||||
|
||||
|
|
@ -130,7 +130,7 @@ internal sealed class CommandManager : IServiceType, IDisposable, ICommandManage
|
|||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
void IDisposable.Dispose()
|
||||
void IInternalDisposableService.DisposeService()
|
||||
{
|
||||
this.chatGui.CheckMessageHandled -= this.OnCheckMessageHandled;
|
||||
}
|
||||
|
|
@ -170,7 +170,7 @@ internal sealed class CommandManager : IServiceType, IDisposable, ICommandManage
|
|||
#pragma warning disable SA1015
|
||||
[ResolveVia<ICommandManager>]
|
||||
#pragma warning restore SA1015
|
||||
internal class CommandManagerPluginScoped : IDisposable, IServiceType, ICommandManager
|
||||
internal class CommandManagerPluginScoped : IInternalDisposableService, ICommandManager
|
||||
{
|
||||
private static readonly ModuleLog Log = new("Command");
|
||||
|
||||
|
|
@ -193,7 +193,7 @@ internal class CommandManagerPluginScoped : IDisposable, IServiceType, ICommandM
|
|||
public ReadOnlyDictionary<string, CommandInfo> Commands => this.commandManagerService.Commands;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
void IInternalDisposableService.DisposeService()
|
||||
{
|
||||
foreach (var command in this.pluginRegisteredCommands)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@ namespace Dalamud.Game.Config;
|
|||
/// This class represents the game's configuration.
|
||||
/// </summary>
|
||||
[InterfaceVersion("1.0")]
|
||||
[ServiceManager.BlockingEarlyLoadedService]
|
||||
internal sealed class GameConfig : IServiceType, IGameConfig, IDisposable
|
||||
[ServiceManager.EarlyLoadedService]
|
||||
internal sealed class GameConfig : IInternalDisposableService, IGameConfig
|
||||
{
|
||||
private readonly TaskCompletionSource tcsInitialization = new();
|
||||
private readonly TaskCompletionSource<GameConfigSection> tcsSystem = new();
|
||||
|
|
@ -195,7 +195,7 @@ internal sealed class GameConfig : IServiceType, IGameConfig, IDisposable
|
|||
public void Set(UiControlOption option, string value) => this.UiControl.Set(option.GetName(), value);
|
||||
|
||||
/// <inheritdoc/>
|
||||
void IDisposable.Dispose()
|
||||
void IInternalDisposableService.DisposeService()
|
||||
{
|
||||
var ode = new ObjectDisposedException(nameof(GameConfig));
|
||||
this.tcsInitialization.SetExceptionIfIncomplete(ode);
|
||||
|
|
@ -248,7 +248,7 @@ internal sealed class GameConfig : IServiceType, IGameConfig, IDisposable
|
|||
#pragma warning disable SA1015
|
||||
[ResolveVia<IGameConfig>]
|
||||
#pragma warning restore SA1015
|
||||
internal class GameConfigPluginScoped : IDisposable, IServiceType, IGameConfig
|
||||
internal class GameConfigPluginScoped : IInternalDisposableService, IGameConfig
|
||||
{
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly GameConfig gameConfigService = Service<GameConfig>.Get();
|
||||
|
|
@ -295,7 +295,7 @@ internal class GameConfigPluginScoped : IDisposable, IServiceType, IGameConfig
|
|||
public GameConfigSection UiControl => this.gameConfigService.UiControl;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
void IInternalDisposableService.DisposeService()
|
||||
{
|
||||
this.gameConfigService.Changed -= this.ConfigChangedForward;
|
||||
this.initializationTask.ContinueWith(
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ namespace Dalamud.Game.DutyState;
|
|||
/// This class represents the state of the currently occupied duty.
|
||||
/// </summary>
|
||||
[InterfaceVersion("1.0")]
|
||||
[ServiceManager.BlockingEarlyLoadedService]
|
||||
internal unsafe class DutyState : IDisposable, IServiceType, IDutyState
|
||||
[ServiceManager.EarlyLoadedService]
|
||||
internal unsafe class DutyState : IInternalDisposableService, IDutyState
|
||||
{
|
||||
private readonly DutyStateAddressResolver address;
|
||||
private readonly Hook<SetupContentDirectNetworkMessageDelegate> contentDirectorNetworkMessageHook;
|
||||
|
|
@ -62,7 +62,7 @@ internal unsafe class DutyState : IDisposable, IServiceType, IDutyState
|
|||
private bool CompletedThisTerritory { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
void IInternalDisposableService.DisposeService()
|
||||
{
|
||||
this.contentDirectorNetworkMessageHook.Dispose();
|
||||
this.framework.Update -= this.FrameworkOnUpdateEvent;
|
||||
|
|
@ -168,7 +168,7 @@ internal unsafe class DutyState : IDisposable, IServiceType, IDutyState
|
|||
#pragma warning disable SA1015
|
||||
[ResolveVia<IDutyState>]
|
||||
#pragma warning restore SA1015
|
||||
internal class DutyStatePluginScoped : IDisposable, IServiceType, IDutyState
|
||||
internal class DutyStatePluginScoped : IInternalDisposableService, IDutyState
|
||||
{
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly DutyState dutyStateService = Service<DutyState>.Get();
|
||||
|
|
@ -200,7 +200,7 @@ internal class DutyStatePluginScoped : IDisposable, IServiceType, IDutyState
|
|||
public bool IsDutyStarted => this.dutyStateService.IsDutyStarted;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
void IInternalDisposableService.DisposeService()
|
||||
{
|
||||
this.dutyStateService.DutyStarted -= this.DutyStartedForward;
|
||||
this.dutyStateService.DutyWiped -= this.DutyWipedForward;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
|
|
@ -21,14 +22,12 @@ namespace Dalamud.Game;
|
|||
/// This class represents the Framework of the native game client and grants access to various subsystems.
|
||||
/// </summary>
|
||||
[InterfaceVersion("1.0")]
|
||||
[ServiceManager.BlockingEarlyLoadedService]
|
||||
internal sealed class Framework : IDisposable, IServiceType, IFramework
|
||||
[ServiceManager.EarlyLoadedService]
|
||||
internal sealed class Framework : IInternalDisposableService, IFramework
|
||||
{
|
||||
private static readonly ModuleLog Log = new("Framework");
|
||||
|
||||
|
||||
private static readonly Stopwatch StatsStopwatch = new();
|
||||
|
||||
private readonly GameLifecycle lifecycle;
|
||||
|
||||
private readonly Stopwatch updateStopwatch = new();
|
||||
private readonly HitchDetector hitchDetector;
|
||||
|
|
@ -37,25 +36,37 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework
|
|||
private readonly Hook<OnRealDestroyDelegate> destroyHook;
|
||||
|
||||
private readonly FrameworkAddressResolver addressResolver;
|
||||
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly GameLifecycle lifecycle = Service<GameLifecycle>.Get();
|
||||
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.Get();
|
||||
|
||||
private readonly object runOnNextTickTaskListSync = new();
|
||||
private List<RunOnNextTickTaskBase> runOnNextTickTaskList = new();
|
||||
private List<RunOnNextTickTaskBase> runOnNextTickTaskList2 = new();
|
||||
private readonly CancellationTokenSource frameworkDestroy;
|
||||
private readonly ThreadBoundTaskScheduler frameworkThreadTaskScheduler;
|
||||
|
||||
private Thread? frameworkUpdateThread;
|
||||
private readonly ConcurrentDictionary<TaskCompletionSource, (ulong Expire, CancellationToken CancellationToken)>
|
||||
tickDelayedTaskCompletionSources = new();
|
||||
|
||||
private ulong tickCounter;
|
||||
|
||||
[ServiceManager.ServiceConstructor]
|
||||
private Framework(TargetSigScanner sigScanner, GameLifecycle lifecycle)
|
||||
private Framework(TargetSigScanner sigScanner)
|
||||
{
|
||||
this.lifecycle = lifecycle;
|
||||
this.hitchDetector = new HitchDetector("FrameworkUpdate", this.configuration.FrameworkUpdateHitch);
|
||||
|
||||
this.addressResolver = new FrameworkAddressResolver();
|
||||
this.addressResolver.Setup(sigScanner);
|
||||
|
||||
this.frameworkDestroy = new();
|
||||
this.frameworkThreadTaskScheduler = new();
|
||||
this.FrameworkThreadTaskFactory = new(
|
||||
this.frameworkDestroy.Token,
|
||||
TaskCreationOptions.None,
|
||||
TaskContinuationOptions.None,
|
||||
this.frameworkThreadTaskScheduler);
|
||||
|
||||
this.updateHook = Hook<OnUpdateDetour>.FromAddress(this.addressResolver.TickAddress, this.HandleFrameworkUpdate);
|
||||
this.destroyHook = Hook<OnRealDestroyDelegate>.FromAddress(this.addressResolver.DestroyAddress, this.HandleFrameworkDestroy);
|
||||
|
||||
|
|
@ -76,6 +87,11 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework
|
|||
/// <inheritdoc/>
|
||||
public event IFramework.OnUpdateDelegate? Update;
|
||||
|
||||
/// <summary>
|
||||
/// Executes during FrameworkUpdate before all <see cref="Update"/> delegates.
|
||||
/// </summary>
|
||||
internal event IFramework.OnUpdateDelegate? BeforeUpdate;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the collection of stats is enabled.
|
||||
/// </summary>
|
||||
|
|
@ -96,10 +112,10 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework
|
|||
public TimeSpan UpdateDelta { get; private set; } = TimeSpan.Zero;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsInFrameworkUpdateThread => Thread.CurrentThread == this.frameworkUpdateThread;
|
||||
public bool IsInFrameworkUpdateThread => this.frameworkThreadTaskScheduler.IsOnBoundThread;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsFrameworkUnloading { get; internal set; }
|
||||
public bool IsFrameworkUnloading => this.frameworkDestroy.IsCancellationRequested;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of update sub-delegates that didn't get updated this frame.
|
||||
|
|
@ -111,6 +127,56 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework
|
|||
/// </summary>
|
||||
internal bool DispatchUpdateEvents { get; set; } = true;
|
||||
|
||||
private TaskFactory FrameworkThreadTaskFactory { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public TaskFactory GetTaskFactory() => this.FrameworkThreadTaskFactory;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task DelayTicks(long numTicks, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (this.frameworkDestroy.IsCancellationRequested)
|
||||
return Task.FromCanceled(this.frameworkDestroy.Token);
|
||||
if (numTicks <= 0)
|
||||
return Task.CompletedTask;
|
||||
|
||||
var tcs = new TaskCompletionSource();
|
||||
this.tickDelayedTaskCompletionSources[tcs] = (this.tickCounter + (ulong)numTicks, cancellationToken);
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task Run(Action action, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (cancellationToken == default)
|
||||
cancellationToken = this.FrameworkThreadTaskFactory.CancellationToken;
|
||||
return this.FrameworkThreadTaskFactory.StartNew(action, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<T> Run<T>(Func<T> action, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (cancellationToken == default)
|
||||
cancellationToken = this.FrameworkThreadTaskFactory.CancellationToken;
|
||||
return this.FrameworkThreadTaskFactory.StartNew(action, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task Run(Func<Task> action, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (cancellationToken == default)
|
||||
cancellationToken = this.FrameworkThreadTaskFactory.CancellationToken;
|
||||
return this.FrameworkThreadTaskFactory.StartNew(action, cancellationToken).Unwrap();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<T> Run<T>(Func<Task<T>> action, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (cancellationToken == default)
|
||||
cancellationToken = this.FrameworkThreadTaskFactory.CancellationToken;
|
||||
return this.FrameworkThreadTaskFactory.StartNew(action, cancellationToken).Unwrap();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<T> RunOnFrameworkThread<T>(Func<T> func) =>
|
||||
this.IsInFrameworkUpdateThread || this.IsFrameworkUnloading ? Task.FromResult(func()) : this.RunOnTick(func);
|
||||
|
|
@ -157,20 +223,18 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework
|
|||
return Task.FromCanceled<T>(cts.Token);
|
||||
}
|
||||
|
||||
var tcs = new TaskCompletionSource<T>();
|
||||
lock (this.runOnNextTickTaskListSync)
|
||||
{
|
||||
this.runOnNextTickTaskList.Add(new RunOnNextTickTaskFunc<T>()
|
||||
if (cancellationToken == default)
|
||||
cancellationToken = this.FrameworkThreadTaskFactory.CancellationToken;
|
||||
return this.FrameworkThreadTaskFactory.ContinueWhenAll(
|
||||
new[]
|
||||
{
|
||||
RemainingTicks = delayTicks,
|
||||
RunAfterTickCount = Environment.TickCount64 + (long)Math.Ceiling(delay.TotalMilliseconds),
|
||||
CancellationToken = cancellationToken,
|
||||
TaskCompletionSource = tcs,
|
||||
Func = func,
|
||||
});
|
||||
}
|
||||
|
||||
return tcs.Task;
|
||||
Task.Delay(delay, cancellationToken),
|
||||
this.DelayTicks(delayTicks, cancellationToken),
|
||||
},
|
||||
_ => func(),
|
||||
cancellationToken,
|
||||
TaskContinuationOptions.HideScheduler,
|
||||
this.frameworkThreadTaskScheduler);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
|
@ -186,20 +250,18 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework
|
|||
return Task.FromCanceled(cts.Token);
|
||||
}
|
||||
|
||||
var tcs = new TaskCompletionSource();
|
||||
lock (this.runOnNextTickTaskListSync)
|
||||
{
|
||||
this.runOnNextTickTaskList.Add(new RunOnNextTickTaskAction()
|
||||
if (cancellationToken == default)
|
||||
cancellationToken = this.FrameworkThreadTaskFactory.CancellationToken;
|
||||
return this.FrameworkThreadTaskFactory.ContinueWhenAll(
|
||||
new[]
|
||||
{
|
||||
RemainingTicks = delayTicks,
|
||||
RunAfterTickCount = Environment.TickCount64 + (long)Math.Ceiling(delay.TotalMilliseconds),
|
||||
CancellationToken = cancellationToken,
|
||||
TaskCompletionSource = tcs,
|
||||
Action = action,
|
||||
});
|
||||
}
|
||||
|
||||
return tcs.Task;
|
||||
Task.Delay(delay, cancellationToken),
|
||||
this.DelayTicks(delayTicks, cancellationToken),
|
||||
},
|
||||
_ => action(),
|
||||
cancellationToken,
|
||||
TaskContinuationOptions.HideScheduler,
|
||||
this.frameworkThreadTaskScheduler);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
|
@ -215,20 +277,18 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework
|
|||
return Task.FromCanceled<T>(cts.Token);
|
||||
}
|
||||
|
||||
var tcs = new TaskCompletionSource<Task<T>>();
|
||||
lock (this.runOnNextTickTaskListSync)
|
||||
{
|
||||
this.runOnNextTickTaskList.Add(new RunOnNextTickTaskFunc<Task<T>>()
|
||||
if (cancellationToken == default)
|
||||
cancellationToken = this.FrameworkThreadTaskFactory.CancellationToken;
|
||||
return this.FrameworkThreadTaskFactory.ContinueWhenAll(
|
||||
new[]
|
||||
{
|
||||
RemainingTicks = delayTicks,
|
||||
RunAfterTickCount = Environment.TickCount64 + (long)Math.Ceiling(delay.TotalMilliseconds),
|
||||
CancellationToken = cancellationToken,
|
||||
TaskCompletionSource = tcs,
|
||||
Func = func,
|
||||
});
|
||||
}
|
||||
|
||||
return tcs.Task.ContinueWith(x => x.Result, cancellationToken).Unwrap();
|
||||
Task.Delay(delay, cancellationToken),
|
||||
this.DelayTicks(delayTicks, cancellationToken),
|
||||
},
|
||||
_ => func(),
|
||||
cancellationToken,
|
||||
TaskContinuationOptions.HideScheduler,
|
||||
this.frameworkThreadTaskScheduler).Unwrap();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
|
@ -244,26 +304,24 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework
|
|||
return Task.FromCanceled(cts.Token);
|
||||
}
|
||||
|
||||
var tcs = new TaskCompletionSource<Task>();
|
||||
lock (this.runOnNextTickTaskListSync)
|
||||
{
|
||||
this.runOnNextTickTaskList.Add(new RunOnNextTickTaskFunc<Task>()
|
||||
if (cancellationToken == default)
|
||||
cancellationToken = this.FrameworkThreadTaskFactory.CancellationToken;
|
||||
return this.FrameworkThreadTaskFactory.ContinueWhenAll(
|
||||
new[]
|
||||
{
|
||||
RemainingTicks = delayTicks,
|
||||
RunAfterTickCount = Environment.TickCount64 + (long)Math.Ceiling(delay.TotalMilliseconds),
|
||||
CancellationToken = cancellationToken,
|
||||
TaskCompletionSource = tcs,
|
||||
Func = func,
|
||||
});
|
||||
}
|
||||
|
||||
return tcs.Task.ContinueWith(x => x.Result, cancellationToken).Unwrap();
|
||||
Task.Delay(delay, cancellationToken),
|
||||
this.DelayTicks(delayTicks, cancellationToken),
|
||||
},
|
||||
_ => func(),
|
||||
cancellationToken,
|
||||
TaskContinuationOptions.HideScheduler,
|
||||
this.frameworkThreadTaskScheduler).Unwrap();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose of managed and unmanaged resources.
|
||||
/// </summary>
|
||||
void IDisposable.Dispose()
|
||||
void IInternalDisposableService.DisposeService()
|
||||
{
|
||||
this.RunOnFrameworkThread(() =>
|
||||
{
|
||||
|
|
@ -280,7 +338,7 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework
|
|||
this.updateStopwatch.Reset();
|
||||
StatsStopwatch.Reset();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Adds a update time to the stats history.
|
||||
/// </summary>
|
||||
|
|
@ -307,7 +365,7 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework
|
|||
internal void ProfileAndInvoke(IFramework.OnUpdateDelegate? eventDelegate, IFramework frameworkInstance)
|
||||
{
|
||||
if (eventDelegate is null) return;
|
||||
|
||||
|
||||
var invokeList = eventDelegate.GetInvocationList();
|
||||
|
||||
// Individually invoke OnUpdate handlers and time them.
|
||||
|
|
@ -333,26 +391,14 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework
|
|||
}
|
||||
}
|
||||
|
||||
private void RunPendingTickTasks()
|
||||
{
|
||||
if (this.runOnNextTickTaskList.Count == 0 && this.runOnNextTickTaskList2.Count == 0)
|
||||
return;
|
||||
|
||||
for (var i = 0; i < 2; i++)
|
||||
{
|
||||
lock (this.runOnNextTickTaskListSync)
|
||||
(this.runOnNextTickTaskList, this.runOnNextTickTaskList2) = (this.runOnNextTickTaskList2, this.runOnNextTickTaskList);
|
||||
|
||||
this.runOnNextTickTaskList2.RemoveAll(x => x.Run());
|
||||
}
|
||||
}
|
||||
|
||||
private bool HandleFrameworkUpdate(IntPtr framework)
|
||||
{
|
||||
this.frameworkUpdateThread ??= Thread.CurrentThread;
|
||||
this.frameworkThreadTaskScheduler.BoundThread ??= Thread.CurrentThread;
|
||||
|
||||
ThreadSafety.MarkMainThread();
|
||||
|
||||
this.BeforeUpdate?.InvokeSafely(this);
|
||||
|
||||
this.hitchDetector.Start();
|
||||
|
||||
try
|
||||
|
|
@ -381,18 +427,30 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework
|
|||
|
||||
this.LastUpdate = DateTime.Now;
|
||||
this.LastUpdateUTC = DateTime.UtcNow;
|
||||
this.tickCounter++;
|
||||
foreach (var (k, (expiry, ct)) in this.tickDelayedTaskCompletionSources)
|
||||
{
|
||||
if (ct.IsCancellationRequested)
|
||||
k.SetCanceled(ct);
|
||||
else if (expiry <= this.tickCounter)
|
||||
k.SetResult();
|
||||
else
|
||||
continue;
|
||||
|
||||
this.tickDelayedTaskCompletionSources.Remove(k, out _);
|
||||
}
|
||||
|
||||
if (StatsEnabled)
|
||||
{
|
||||
StatsStopwatch.Restart();
|
||||
this.RunPendingTickTasks();
|
||||
this.frameworkThreadTaskScheduler.Run();
|
||||
StatsStopwatch.Stop();
|
||||
|
||||
AddToStats(nameof(this.RunPendingTickTasks), StatsStopwatch.Elapsed.TotalMilliseconds);
|
||||
AddToStats(nameof(this.frameworkThreadTaskScheduler), StatsStopwatch.Elapsed.TotalMilliseconds);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.RunPendingTickTasks();
|
||||
this.frameworkThreadTaskScheduler.Run();
|
||||
}
|
||||
|
||||
if (StatsEnabled && this.Update != null)
|
||||
|
|
@ -404,7 +462,7 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework
|
|||
// Cleanup handlers that are no longer being called
|
||||
foreach (var key in this.NonUpdatedSubDelegates)
|
||||
{
|
||||
if (key == nameof(this.RunPendingTickTasks))
|
||||
if (key == nameof(this.FrameworkThreadTaskFactory))
|
||||
continue;
|
||||
|
||||
if (StatsHistory[key].Count > 0)
|
||||
|
|
@ -425,14 +483,17 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework
|
|||
|
||||
this.hitchDetector.Stop();
|
||||
|
||||
original:
|
||||
original:
|
||||
return this.updateHook.OriginalDisposeSafe(framework);
|
||||
}
|
||||
|
||||
private bool HandleFrameworkDestroy(IntPtr framework)
|
||||
{
|
||||
this.IsFrameworkUnloading = true;
|
||||
this.frameworkDestroy.Cancel();
|
||||
this.DispatchUpdateEvents = false;
|
||||
foreach (var k in this.tickDelayedTaskCompletionSources.Keys)
|
||||
k.SetCanceled(this.frameworkDestroy.Token);
|
||||
this.tickDelayedTaskCompletionSources.Clear();
|
||||
|
||||
// All the same, for now...
|
||||
this.lifecycle.SetShuttingDown();
|
||||
|
|
@ -440,95 +501,12 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework
|
|||
|
||||
Log.Information("Framework::Destroy!");
|
||||
Service<Dalamud>.Get().Unload();
|
||||
this.RunPendingTickTasks();
|
||||
this.frameworkThreadTaskScheduler.Run();
|
||||
ServiceManager.WaitForServiceUnload();
|
||||
Log.Information("Framework::Destroy OK!");
|
||||
|
||||
return this.destroyHook.OriginalDisposeSafe(framework);
|
||||
}
|
||||
|
||||
private abstract class RunOnNextTickTaskBase
|
||||
{
|
||||
internal int RemainingTicks { get; set; }
|
||||
|
||||
internal long RunAfterTickCount { get; init; }
|
||||
|
||||
internal CancellationToken CancellationToken { get; init; }
|
||||
|
||||
internal bool Run()
|
||||
{
|
||||
if (this.CancellationToken.IsCancellationRequested)
|
||||
{
|
||||
this.CancelImpl();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.RemainingTicks > 0)
|
||||
this.RemainingTicks -= 1;
|
||||
if (this.RemainingTicks > 0)
|
||||
return false;
|
||||
|
||||
if (this.RunAfterTickCount > Environment.TickCount64)
|
||||
return false;
|
||||
|
||||
this.RunImpl();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected abstract void RunImpl();
|
||||
|
||||
protected abstract void CancelImpl();
|
||||
}
|
||||
|
||||
private class RunOnNextTickTaskFunc<T> : RunOnNextTickTaskBase
|
||||
{
|
||||
internal TaskCompletionSource<T> TaskCompletionSource { get; init; }
|
||||
|
||||
internal Func<T> Func { get; init; }
|
||||
|
||||
protected override void RunImpl()
|
||||
{
|
||||
try
|
||||
{
|
||||
this.TaskCompletionSource.SetResult(this.Func());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.TaskCompletionSource.SetException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void CancelImpl()
|
||||
{
|
||||
this.TaskCompletionSource.SetCanceled();
|
||||
}
|
||||
}
|
||||
|
||||
private class RunOnNextTickTaskAction : RunOnNextTickTaskBase
|
||||
{
|
||||
internal TaskCompletionSource TaskCompletionSource { get; init; }
|
||||
|
||||
internal Action Action { get; init; }
|
||||
|
||||
protected override void RunImpl()
|
||||
{
|
||||
try
|
||||
{
|
||||
this.Action();
|
||||
this.TaskCompletionSource.SetResult();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.TaskCompletionSource.SetException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void CancelImpl()
|
||||
{
|
||||
this.TaskCompletionSource.SetCanceled();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -540,7 +518,7 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework
|
|||
#pragma warning disable SA1015
|
||||
[ResolveVia<IFramework>]
|
||||
#pragma warning restore SA1015
|
||||
internal class FrameworkPluginScoped : IDisposable, IServiceType, IFramework
|
||||
internal class FrameworkPluginScoped : IInternalDisposableService, IFramework
|
||||
{
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly Framework frameworkService = Service<Framework>.Get();
|
||||
|
|
@ -558,51 +536,74 @@ internal class FrameworkPluginScoped : IDisposable, IServiceType, IFramework
|
|||
|
||||
/// <inheritdoc/>
|
||||
public DateTime LastUpdate => this.frameworkService.LastUpdate;
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
public DateTime LastUpdateUTC => this.frameworkService.LastUpdateUTC;
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
public TimeSpan UpdateDelta => this.frameworkService.UpdateDelta;
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsInFrameworkUpdateThread => this.frameworkService.IsInFrameworkUpdateThread;
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsFrameworkUnloading => this.frameworkService.IsFrameworkUnloading;
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
void IInternalDisposableService.DisposeService()
|
||||
{
|
||||
this.frameworkService.Update -= this.OnUpdateForward;
|
||||
|
||||
this.Update = null;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public TaskFactory GetTaskFactory() => this.frameworkService.GetTaskFactory();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task DelayTicks(long numTicks, CancellationToken cancellationToken = default) =>
|
||||
this.frameworkService.DelayTicks(numTicks, cancellationToken);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task Run(Action action, CancellationToken cancellationToken = default) =>
|
||||
this.frameworkService.Run(action, cancellationToken);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<T> Run<T>(Func<T> action, CancellationToken cancellationToken = default) =>
|
||||
this.frameworkService.Run(action, cancellationToken);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task Run(Func<Task> action, CancellationToken cancellationToken = default) =>
|
||||
this.frameworkService.Run(action, cancellationToken);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<T> Run<T>(Func<Task<T>> action, CancellationToken cancellationToken = default) =>
|
||||
this.frameworkService.Run(action, cancellationToken);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<T> RunOnFrameworkThread<T>(Func<T> func)
|
||||
=> this.frameworkService.RunOnFrameworkThread(func);
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task RunOnFrameworkThread(Action action)
|
||||
=> this.frameworkService.RunOnFrameworkThread(action);
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<T> RunOnFrameworkThread<T>(Func<Task<T>> func)
|
||||
=> this.frameworkService.RunOnFrameworkThread(func);
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task RunOnFrameworkThread(Func<Task> func)
|
||||
=> this.frameworkService.RunOnFrameworkThread(func);
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<T> RunOnTick<T>(Func<T> func, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default)
|
||||
=> this.frameworkService.RunOnTick(func, delay, delayTicks, cancellationToken);
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task RunOnTick(Action action, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default)
|
||||
=> this.frameworkService.RunOnTick(action, delay, delayTicks, cancellationToken);
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<T> RunOnTick<T>(Func<Task<T>> func, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default)
|
||||
=> this.frameworkService.RunOnTick(func, delay, delayTicks, cancellationToken);
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ namespace Dalamud.Game;
|
|||
/// </summary>
|
||||
[PluginInterface]
|
||||
[InterfaceVersion("1.0")]
|
||||
[ServiceManager.BlockingEarlyLoadedService]
|
||||
[ServiceManager.EarlyLoadedService]
|
||||
#pragma warning disable SA1015
|
||||
[ResolveVia<IGameLifecycle>]
|
||||
#pragma warning restore SA1015
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ using Dalamud.IoC;
|
|||
using Dalamud.IoC.Internal;
|
||||
using Dalamud.Logging.Internal;
|
||||
using Dalamud.Memory;
|
||||
using Dalamud.Plugin.Internal;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Dalamud.Utility;
|
||||
using FFXIVClientStructs.FFXIV.Client.System.String;
|
||||
|
|
@ -28,8 +29,8 @@ namespace Dalamud.Game.Gui;
|
|||
/// This class handles interacting with the native chat UI.
|
||||
/// </summary>
|
||||
[InterfaceVersion("1.0")]
|
||||
[ServiceManager.BlockingEarlyLoadedService]
|
||||
internal sealed unsafe class ChatGui : IDisposable, IServiceType, IChatGui
|
||||
[ServiceManager.EarlyLoadedService]
|
||||
internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
|
||||
{
|
||||
private static readonly ModuleLog Log = new("ChatGui");
|
||||
|
||||
|
|
@ -61,7 +62,7 @@ internal sealed unsafe class ChatGui : IDisposable, IServiceType, IChatGui
|
|||
this.populateItemLinkHook.Enable();
|
||||
this.interactableLinkClickedHook.Enable();
|
||||
}
|
||||
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
|
||||
private delegate uint PrintMessageDelegate(RaptureLogModule* manager, XivChatType chatType, Utf8String* sender, Utf8String* message, int timestamp, byte silent);
|
||||
|
||||
|
|
@ -109,7 +110,7 @@ internal sealed unsafe class ChatGui : IDisposable, IServiceType, IChatGui
|
|||
/// <summary>
|
||||
/// Dispose of managed and unmanaged resources.
|
||||
/// </summary>
|
||||
void IDisposable.Dispose()
|
||||
void IInternalDisposableService.DisposeService()
|
||||
{
|
||||
this.printMessageHook.Dispose();
|
||||
this.populateItemLinkHook.Dispose();
|
||||
|
|
@ -121,31 +122,31 @@ internal sealed unsafe class ChatGui : IDisposable, IServiceType, IChatGui
|
|||
{
|
||||
this.chatQueue.Enqueue(chat);
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Print(string message, string? messageTag = null, ushort? tagColor = null)
|
||||
{
|
||||
this.PrintTagged(message, this.configuration.GeneralChatType, messageTag, tagColor);
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Print(SeString message, string? messageTag = null, ushort? tagColor = null)
|
||||
{
|
||||
this.PrintTagged(message, this.configuration.GeneralChatType, messageTag, tagColor);
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void PrintError(string message, string? messageTag = null, ushort? tagColor = null)
|
||||
{
|
||||
this.PrintTagged(message, XivChatType.Urgent, messageTag, tagColor);
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void PrintError(SeString message, string? messageTag = null, ushort? tagColor = null)
|
||||
{
|
||||
this.PrintTagged(message, XivChatType.Urgent, messageTag, tagColor);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Process a chat queue.
|
||||
/// </summary>
|
||||
|
|
@ -154,9 +155,29 @@ internal sealed unsafe class ChatGui : IDisposable, IServiceType, IChatGui
|
|||
while (this.chatQueue.Count > 0)
|
||||
{
|
||||
var chat = this.chatQueue.Dequeue();
|
||||
var replacedMessage = new SeStringBuilder();
|
||||
|
||||
// Normalize Unicode NBSP to the built-in one, as the former won't renderl
|
||||
foreach (var payload in chat.Message.Payloads)
|
||||
{
|
||||
if (payload is TextPayload { Text: not null } textPayload)
|
||||
{
|
||||
var split = textPayload.Text.Split("\u202f"); // NARROW NO-BREAK SPACE
|
||||
for (var i = 0; i < split.Length; i++)
|
||||
{
|
||||
replacedMessage.AddText(split[i]);
|
||||
if (i + 1 < split.Length)
|
||||
replacedMessage.Add(new RawPayload([0x02, (byte)Lumina.Text.Payloads.PayloadType.Indent, 0x01, 0x03]));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
replacedMessage.Add(payload);
|
||||
}
|
||||
}
|
||||
|
||||
var sender = Utf8String.FromSequence(chat.Name.Encode());
|
||||
var message = Utf8String.FromSequence(chat.Message.Encode());
|
||||
var message = Utf8String.FromSequence(replacedMessage.BuiltString.Encode());
|
||||
|
||||
this.HandlePrintMessageDetour(RaptureLogModule.Instance(), chat.Type, sender, message, (int)chat.SenderId, (byte)(chat.Parameters != 0 ? 1 : 0));
|
||||
|
||||
|
|
@ -193,7 +214,7 @@ internal sealed unsafe class ChatGui : IDisposable, IServiceType, IChatGui
|
|||
lock (this.dalamudLinkHandlers)
|
||||
{
|
||||
var changed = false;
|
||||
|
||||
|
||||
foreach (var handler in this.RegisteredLinkHandlers.Keys.Where(k => k.PluginName == pluginName))
|
||||
changed |= this.dalamudLinkHandlers.Remove(handler);
|
||||
if (changed)
|
||||
|
|
@ -230,18 +251,18 @@ internal sealed unsafe class ChatGui : IDisposable, IServiceType, IChatGui
|
|||
builder.AddText($"[{tag}] ");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
this.Print(new XivChatEntry
|
||||
{
|
||||
Message = builder.AddText(message).Build(),
|
||||
Type = channel,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private void PrintTagged(SeString message, XivChatType channel, string? tag, ushort? color)
|
||||
{
|
||||
var builder = new SeStringBuilder();
|
||||
|
||||
|
||||
if (!tag.IsNullOrEmpty())
|
||||
{
|
||||
if (color is not null)
|
||||
|
|
@ -253,7 +274,7 @@ internal sealed unsafe class ChatGui : IDisposable, IServiceType, IChatGui
|
|||
builder.AddText($"[{tag}] ");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
this.Print(new XivChatEntry
|
||||
{
|
||||
Message = builder.Build().Append(message),
|
||||
|
|
@ -409,7 +430,7 @@ internal sealed unsafe class ChatGui : IDisposable, IServiceType, IChatGui
|
|||
#pragma warning disable SA1015
|
||||
[ResolveVia<IChatGui>]
|
||||
#pragma warning restore SA1015
|
||||
internal class ChatGuiPluginScoped : IDisposable, IServiceType, IChatGui
|
||||
internal class ChatGuiPluginScoped : IInternalDisposableService, IChatGui
|
||||
{
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly ChatGui chatGuiService = Service<ChatGui>.Get();
|
||||
|
|
@ -424,16 +445,16 @@ internal class ChatGuiPluginScoped : IDisposable, IServiceType, IChatGui
|
|||
this.chatGuiService.ChatMessageHandled += this.OnMessageHandledForward;
|
||||
this.chatGuiService.ChatMessageUnhandled += this.OnMessageUnhandledForward;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event IChatGui.OnMessageDelegate? ChatMessage;
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event IChatGui.OnCheckMessageHandledDelegate? CheckMessageHandled;
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event IChatGui.OnMessageHandledDelegate? ChatMessageHandled;
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event IChatGui.OnMessageUnhandledDelegate? ChatMessageUnhandled;
|
||||
|
||||
|
|
@ -447,7 +468,7 @@ internal class ChatGuiPluginScoped : IDisposable, IServiceType, IChatGui
|
|||
public IReadOnlyDictionary<(string PluginName, uint CommandId), Action<uint, SeString>> RegisteredLinkHandlers => this.chatGuiService.RegisteredLinkHandlers;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
void IInternalDisposableService.DisposeService()
|
||||
{
|
||||
this.chatGuiService.ChatMessage -= this.OnMessageForward;
|
||||
this.chatGuiService.CheckMessageHandled -= this.OnCheckMessageForward;
|
||||
|
|
@ -459,23 +480,23 @@ internal class ChatGuiPluginScoped : IDisposable, IServiceType, IChatGui
|
|||
this.ChatMessageHandled = null;
|
||||
this.ChatMessageUnhandled = null;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Print(XivChatEntry chat)
|
||||
=> this.chatGuiService.Print(chat);
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Print(string message, string? messageTag = null, ushort? tagColor = null)
|
||||
public void Print(string message, string? messageTag = null, ushort? tagColor = null)
|
||||
=> this.chatGuiService.Print(message, messageTag, tagColor);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Print(SeString message, string? messageTag = null, ushort? tagColor = null)
|
||||
=> this.chatGuiService.Print(message, messageTag, tagColor);
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void PrintError(string message, string? messageTag = null, ushort? tagColor = null)
|
||||
=> this.chatGuiService.PrintError(message, messageTag, tagColor);
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void PrintError(SeString message, string? messageTag = null, ushort? tagColor = null)
|
||||
=> this.chatGuiService.PrintError(message, messageTag, tagColor);
|
||||
|
|
|
|||
563
Dalamud/Game/Gui/ContextMenu/ContextMenu.cs
Normal file
563
Dalamud/Game/Gui/ContextMenu/ContextMenu.cs
Normal file
|
|
@ -0,0 +1,563 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
using Dalamud.Game.Text;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using Dalamud.Hooking;
|
||||
using Dalamud.IoC;
|
||||
using Dalamud.IoC.Internal;
|
||||
using Dalamud.Logging.Internal;
|
||||
using Dalamud.Memory;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Dalamud.Utility;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Client.System.Memory;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using FFXIVClientStructs.Interop;
|
||||
|
||||
using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType;
|
||||
|
||||
namespace Dalamud.Game.Gui.ContextMenu;
|
||||
|
||||
/// <summary>
|
||||
/// This class handles interacting with the game's (right-click) context menu.
|
||||
/// </summary>
|
||||
[InterfaceVersion("1.0")]
|
||||
[ServiceManager.EarlyLoadedService]
|
||||
internal sealed unsafe class ContextMenu : IInternalDisposableService, IContextMenu
|
||||
{
|
||||
private static readonly ModuleLog Log = new("ContextMenu");
|
||||
|
||||
private readonly Hook<RaptureAtkModuleOpenAddonByAgentDelegate> raptureAtkModuleOpenAddonByAgentHook;
|
||||
private readonly Hook<AddonContextMenuOnMenuSelectedDelegate> addonContextMenuOnMenuSelectedHook;
|
||||
private readonly RaptureAtkModuleOpenAddonDelegate raptureAtkModuleOpenAddon;
|
||||
|
||||
[ServiceManager.ServiceConstructor]
|
||||
private ContextMenu()
|
||||
{
|
||||
this.raptureAtkModuleOpenAddonByAgentHook = Hook<RaptureAtkModuleOpenAddonByAgentDelegate>.FromAddress((nint)RaptureAtkModule.Addresses.OpenAddonByAgent.Value, this.RaptureAtkModuleOpenAddonByAgentDetour);
|
||||
this.addonContextMenuOnMenuSelectedHook = Hook<AddonContextMenuOnMenuSelectedDelegate>.FromAddress((nint)AddonContextMenu.StaticVTable.OnMenuSelected, this.AddonContextMenuOnMenuSelectedDetour);
|
||||
this.raptureAtkModuleOpenAddon = Marshal.GetDelegateForFunctionPointer<RaptureAtkModuleOpenAddonDelegate>((nint)RaptureAtkModule.Addresses.OpenAddon.Value);
|
||||
|
||||
this.raptureAtkModuleOpenAddonByAgentHook.Enable();
|
||||
this.addonContextMenuOnMenuSelectedHook.Enable();
|
||||
}
|
||||
|
||||
private delegate ushort RaptureAtkModuleOpenAddonByAgentDelegate(RaptureAtkModule* module, byte* addonName, AtkUnitBase* addon, int valueCount, AtkValue* values, AgentInterface* agent, nint a7, ushort parentAddonId);
|
||||
|
||||
private delegate bool AddonContextMenuOnMenuSelectedDelegate(AddonContextMenu* addon, int selectedIdx, byte a3);
|
||||
|
||||
private delegate ushort RaptureAtkModuleOpenAddonDelegate(RaptureAtkModule* a1, uint addonNameId, uint valueCount, AtkValue* values, AgentInterface* parentAgent, ulong unk, ushort parentAddonId, int unk2);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event IContextMenu.OnMenuOpenedDelegate? OnMenuOpened;
|
||||
|
||||
private Dictionary<ContextMenuType, List<MenuItem>> MenuItems { get; } = new();
|
||||
|
||||
private object MenuItemsLock { get; } = new();
|
||||
|
||||
private AgentInterface* SelectedAgent { get; set; }
|
||||
|
||||
private ContextMenuType? SelectedMenuType { get; set; }
|
||||
|
||||
private List<MenuItem>? SelectedItems { get; set; }
|
||||
|
||||
private HashSet<nint> SelectedEventInterfaces { get; } = new();
|
||||
|
||||
private AtkUnitBase* SelectedParentAddon { get; set; }
|
||||
|
||||
// -1 -> -inf: native items
|
||||
// 0 -> inf: selected items
|
||||
private List<int> MenuCallbackIds { get; } = new();
|
||||
|
||||
private IReadOnlyList<MenuItem>? SubmenuItems { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
void IInternalDisposableService.DisposeService()
|
||||
{
|
||||
var manager = RaptureAtkUnitManager.Instance();
|
||||
var menu = manager->GetAddonByName("ContextMenu");
|
||||
var submenu = manager->GetAddonByName("AddonContextSub");
|
||||
if (menu->IsVisible)
|
||||
menu->FireCallbackInt(-1);
|
||||
if (submenu->IsVisible)
|
||||
submenu->FireCallbackInt(-1);
|
||||
|
||||
this.raptureAtkModuleOpenAddonByAgentHook.Dispose();
|
||||
this.addonContextMenuOnMenuSelectedHook.Dispose();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void AddMenuItem(ContextMenuType menuType, MenuItem item)
|
||||
{
|
||||
lock (this.MenuItemsLock)
|
||||
{
|
||||
if (!this.MenuItems.TryGetValue(menuType, out var items))
|
||||
this.MenuItems[menuType] = items = new();
|
||||
items.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool RemoveMenuItem(ContextMenuType menuType, MenuItem item)
|
||||
{
|
||||
lock (this.MenuItemsLock)
|
||||
{
|
||||
if (!this.MenuItems.TryGetValue(menuType, out var items))
|
||||
return false;
|
||||
return items.Remove(item);
|
||||
}
|
||||
}
|
||||
|
||||
private AtkValue* ExpandContextMenuArray(Span<AtkValue> oldValues, int newSize)
|
||||
{
|
||||
// if the array has enough room, don't reallocate
|
||||
if (oldValues.Length >= newSize)
|
||||
return (AtkValue*)Unsafe.AsPointer(ref oldValues[0]);
|
||||
|
||||
var size = (sizeof(AtkValue) * newSize) + 8;
|
||||
var newArray = (nint)IMemorySpace.GetUISpace()->Malloc((ulong)size, 0);
|
||||
if (newArray == nint.Zero)
|
||||
throw new OutOfMemoryException();
|
||||
NativeMemory.Fill((void*)newArray, (nuint)size, 0);
|
||||
|
||||
*(ulong*)newArray = (ulong)newSize;
|
||||
|
||||
// copy old memory if existing
|
||||
if (!oldValues.IsEmpty)
|
||||
oldValues.CopyTo(new((void*)(newArray + 8), oldValues.Length));
|
||||
|
||||
return (AtkValue*)(newArray + 8);
|
||||
}
|
||||
|
||||
private void FreeExpandedContextMenuArray(AtkValue* newValues, int newSize) =>
|
||||
IMemorySpace.Free((void*)((nint)newValues - 8), (ulong)((newSize * sizeof(AtkValue)) + 8));
|
||||
|
||||
private AtkValue* CreateEmptySubmenuContextMenuArray(SeString name, int x, int y, out int valueCount)
|
||||
{
|
||||
// 0: UInt = ContextItemCount
|
||||
// 1: String = Name
|
||||
// 2: Int = PositionX
|
||||
// 3: Int = PositionY
|
||||
// 4: Bool = false
|
||||
// 5: UInt = ContextItemSubmenuMask
|
||||
// 6: UInt = ReturnArrowMask (_gap_0x6BC ? 1 << (ContextItemCount - 1) : 0)
|
||||
// 7: UInt = 1
|
||||
|
||||
valueCount = 8;
|
||||
var values = this.ExpandContextMenuArray(Span<AtkValue>.Empty, valueCount);
|
||||
values[0].ChangeType(ValueType.UInt);
|
||||
values[0].UInt = 0;
|
||||
values[1].ChangeType(ValueType.String);
|
||||
values[1].SetString(name.Encode().NullTerminate());
|
||||
values[2].ChangeType(ValueType.Int);
|
||||
values[2].Int = x;
|
||||
values[3].ChangeType(ValueType.Int);
|
||||
values[3].Int = y;
|
||||
values[4].ChangeType(ValueType.Bool);
|
||||
values[4].Byte = 0;
|
||||
values[5].ChangeType(ValueType.UInt);
|
||||
values[5].UInt = 0;
|
||||
values[6].ChangeType(ValueType.UInt);
|
||||
values[6].UInt = 0;
|
||||
values[7].ChangeType(ValueType.UInt);
|
||||
values[7].UInt = 1;
|
||||
return values;
|
||||
}
|
||||
|
||||
private void SetupGenericMenu(int headerCount, int sizeHeaderIdx, int returnHeaderIdx, int submenuHeaderIdx, IReadOnlyList<MenuItem> items, ref int valueCount, ref AtkValue* values)
|
||||
{
|
||||
var itemsWithIdx = items.Select((item, idx) => (item, idx)).OrderBy(i => i.item.Priority).ToArray();
|
||||
var prefixItems = itemsWithIdx.Where(i => i.item.Priority < 0).ToArray();
|
||||
var suffixItems = itemsWithIdx.Where(i => i.item.Priority >= 0).ToArray();
|
||||
|
||||
var nativeMenuSize = (int)values[sizeHeaderIdx].UInt;
|
||||
var prefixMenuSize = prefixItems.Length;
|
||||
var suffixMenuSize = suffixItems.Length;
|
||||
|
||||
var hasGameDisabled = valueCount - headerCount - nativeMenuSize > 0;
|
||||
|
||||
var hasCustomDisabled = items.Any(item => !item.IsEnabled);
|
||||
var hasAnyDisabled = hasGameDisabled || hasCustomDisabled;
|
||||
|
||||
values = this.ExpandContextMenuArray(
|
||||
new(values, valueCount),
|
||||
valueCount = (nativeMenuSize + items.Count) * (hasAnyDisabled ? 2 : 1) + headerCount);
|
||||
var offsetData = new Span<AtkValue>(values, headerCount);
|
||||
var nameData = new Span<AtkValue>(values + headerCount, nativeMenuSize + items.Count);
|
||||
var disabledData = hasAnyDisabled ? new Span<AtkValue>(values + headerCount + nativeMenuSize + items.Count, nativeMenuSize + items.Count) : Span<AtkValue>.Empty;
|
||||
|
||||
var returnMask = offsetData[returnHeaderIdx].UInt;
|
||||
var submenuMask = offsetData[submenuHeaderIdx].UInt;
|
||||
|
||||
nameData[..nativeMenuSize].CopyTo(nameData.Slice(prefixMenuSize, nativeMenuSize));
|
||||
if (hasAnyDisabled)
|
||||
{
|
||||
if (hasGameDisabled)
|
||||
{
|
||||
// copy old disabled data
|
||||
var oldDisabledData = new Span<AtkValue>(values + headerCount + nativeMenuSize, nativeMenuSize);
|
||||
oldDisabledData.CopyTo(disabledData.Slice(prefixMenuSize, nativeMenuSize));
|
||||
}
|
||||
else
|
||||
{
|
||||
// enable all
|
||||
for (var i = prefixMenuSize; i < prefixMenuSize + nativeMenuSize; ++i)
|
||||
{
|
||||
disabledData[i].ChangeType(ValueType.Int);
|
||||
disabledData[i].Int = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
returnMask <<= prefixMenuSize;
|
||||
submenuMask <<= prefixMenuSize;
|
||||
|
||||
void FillData(Span<AtkValue> disabledData, Span<AtkValue> nameData, int i, MenuItem item, int idx)
|
||||
{
|
||||
this.MenuCallbackIds.Add(idx);
|
||||
|
||||
if (hasAnyDisabled)
|
||||
{
|
||||
disabledData[i].ChangeType(ValueType.Int);
|
||||
disabledData[i].Int = item.IsEnabled ? 0 : 1;
|
||||
}
|
||||
|
||||
if (item.IsReturn)
|
||||
returnMask |= 1u << i;
|
||||
if (item.IsSubmenu)
|
||||
submenuMask |= 1u << i;
|
||||
|
||||
nameData[i].ChangeType(ValueType.String);
|
||||
nameData[i].SetString(item.PrefixedName.Encode().NullTerminate());
|
||||
}
|
||||
|
||||
for (var i = 0; i < prefixMenuSize; ++i)
|
||||
{
|
||||
var (item, idx) = prefixItems[i];
|
||||
FillData(disabledData, nameData, i, item, idx);
|
||||
}
|
||||
|
||||
this.MenuCallbackIds.AddRange(Enumerable.Range(0, nativeMenuSize).Select(i => -i - 1));
|
||||
|
||||
for (var i = prefixMenuSize + nativeMenuSize; i < prefixMenuSize + nativeMenuSize + suffixMenuSize; ++i)
|
||||
{
|
||||
var (item, idx) = suffixItems[i - prefixMenuSize - nativeMenuSize];
|
||||
FillData(disabledData, nameData, i, item, idx);
|
||||
}
|
||||
|
||||
offsetData[returnHeaderIdx].UInt = returnMask;
|
||||
offsetData[submenuHeaderIdx].UInt = submenuMask;
|
||||
|
||||
offsetData[sizeHeaderIdx].UInt += (uint)items.Count;
|
||||
}
|
||||
|
||||
private void SetupContextMenu(IReadOnlyList<MenuItem> items, ref int valueCount, ref AtkValue* values)
|
||||
{
|
||||
// 0: UInt = Item Count
|
||||
// 1: UInt = 0 (probably window name, just unused)
|
||||
// 2: UInt = Return Mask (?)
|
||||
// 3: UInt = Submenu Mask
|
||||
// 4: UInt = OpenAtCursorPosition ? 2 : 1
|
||||
// 5: UInt = 0
|
||||
// 6: UInt = 0
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (!item.Prefix.HasValue && !item.UseDefaultPrefix)
|
||||
{
|
||||
item.Prefix = MenuItem.DalamudDefaultPrefix;
|
||||
item.PrefixColor = MenuItem.DalamudDefaultPrefixColor;
|
||||
Log.Warning($"Menu item \"{item.Name}\" has no prefix, defaulting to Dalamud's. Menu items outside of a submenu must have a prefix.");
|
||||
}
|
||||
}
|
||||
|
||||
this.SetupGenericMenu(7, 0, 2, 3, items, ref valueCount, ref values);
|
||||
}
|
||||
|
||||
private void SetupContextSubMenu(IReadOnlyList<MenuItem> items, ref int valueCount, ref AtkValue* values)
|
||||
{
|
||||
// 0: UInt = ContextItemCount
|
||||
// 1: skipped?
|
||||
// 2: Int = PositionX
|
||||
// 3: Int = PositionY
|
||||
// 4: Bool = false
|
||||
// 5: UInt = ContextItemSubmenuMask
|
||||
// 6: UInt = _gap_0x6BC ? 1 << (ContextItemCount - 1) : 0
|
||||
// 7: UInt = 1
|
||||
|
||||
this.SetupGenericMenu(8, 0, 6, 5, items, ref valueCount, ref values);
|
||||
}
|
||||
|
||||
private ushort RaptureAtkModuleOpenAddonByAgentDetour(RaptureAtkModule* module, byte* addonName, AtkUnitBase* addon, int valueCount, AtkValue* values, AgentInterface* agent, nint a7, ushort parentAddonId)
|
||||
{
|
||||
var oldValues = values;
|
||||
|
||||
if (MemoryHelper.EqualsZeroTerminatedString("ContextMenu", (nint)addonName))
|
||||
{
|
||||
this.MenuCallbackIds.Clear();
|
||||
this.SelectedAgent = agent;
|
||||
this.SelectedParentAddon = module->RaptureAtkUnitManager.GetAddonById(parentAddonId);
|
||||
this.SelectedEventInterfaces.Clear();
|
||||
if (this.SelectedAgent == AgentInventoryContext.Instance())
|
||||
{
|
||||
this.SelectedMenuType = ContextMenuType.Inventory;
|
||||
}
|
||||
else if (this.SelectedAgent == AgentContext.Instance())
|
||||
{
|
||||
this.SelectedMenuType = ContextMenuType.Default;
|
||||
|
||||
var menu = AgentContext.Instance()->CurrentContextMenu;
|
||||
var handlers = new Span<Pointer<AtkEventInterface>>(menu->EventHandlerArray, 32);
|
||||
var ids = new Span<byte>(menu->EventIdArray, 32);
|
||||
var count = (int)values[0].UInt;
|
||||
handlers = handlers.Slice(7, count);
|
||||
ids = ids.Slice(7, count);
|
||||
for (var i = 0; i < count; ++i)
|
||||
{
|
||||
if (ids[i] <= 106)
|
||||
continue;
|
||||
this.SelectedEventInterfaces.Add((nint)handlers[i].Value);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
this.SelectedMenuType = null;
|
||||
}
|
||||
|
||||
this.SubmenuItems = null;
|
||||
|
||||
if (this.SelectedMenuType is { } menuType)
|
||||
{
|
||||
lock (this.MenuItemsLock)
|
||||
{
|
||||
if (this.MenuItems.TryGetValue(menuType, out var items))
|
||||
this.SelectedItems = new(items);
|
||||
else
|
||||
this.SelectedItems = new();
|
||||
}
|
||||
|
||||
var args = new MenuOpenedArgs(this.SelectedItems.Add, this.SelectedParentAddon, this.SelectedAgent, this.SelectedMenuType.Value, this.SelectedEventInterfaces);
|
||||
this.OnMenuOpened?.InvokeSafely(args);
|
||||
this.SelectedItems = this.FixupMenuList(this.SelectedItems, (int)values[0].UInt);
|
||||
this.SetupContextMenu(this.SelectedItems, ref valueCount, ref values);
|
||||
Log.Verbose($"Opening {this.SelectedMenuType} context menu with {this.SelectedItems.Count} custom items.");
|
||||
}
|
||||
else
|
||||
{
|
||||
this.SelectedItems = null;
|
||||
}
|
||||
}
|
||||
else if (MemoryHelper.EqualsZeroTerminatedString("AddonContextSub", (nint)addonName))
|
||||
{
|
||||
this.MenuCallbackIds.Clear();
|
||||
if (this.SubmenuItems != null)
|
||||
{
|
||||
this.SubmenuItems = this.FixupMenuList(this.SubmenuItems.ToList(), (int)values[0].UInt);
|
||||
|
||||
this.SetupContextSubMenu(this.SubmenuItems, ref valueCount, ref values);
|
||||
Log.Verbose($"Opening {this.SelectedMenuType} submenu with {this.SubmenuItems.Count} custom items.");
|
||||
}
|
||||
}
|
||||
else if (MemoryHelper.EqualsZeroTerminatedString("AddonContextMenuTitle", (nint)addonName))
|
||||
{
|
||||
this.MenuCallbackIds.Clear();
|
||||
}
|
||||
|
||||
var ret = this.raptureAtkModuleOpenAddonByAgentHook.Original(module, addonName, addon, valueCount, values, agent, a7, parentAddonId);
|
||||
if (values != oldValues)
|
||||
this.FreeExpandedContextMenuArray(values, valueCount);
|
||||
return ret;
|
||||
}
|
||||
|
||||
private List<MenuItem> FixupMenuList(List<MenuItem> items, int nativeMenuSize)
|
||||
{
|
||||
// The in game menu actually supports 32 items, but the last item can't have a visible submenu arrow.
|
||||
// As such, we'll only work with 31 items.
|
||||
const int maxMenuItems = 31;
|
||||
if (items.Count + nativeMenuSize > maxMenuItems)
|
||||
{
|
||||
Log.Warning($"Menu size exceeds {maxMenuItems} items, truncating.");
|
||||
var orderedItems = items.OrderBy(i => i.Priority).ToArray();
|
||||
var newItems = orderedItems[..(maxMenuItems - nativeMenuSize - 1)];
|
||||
var submenuItems = orderedItems[(maxMenuItems - nativeMenuSize - 1)..];
|
||||
return newItems.Append(new MenuItem
|
||||
{
|
||||
Prefix = SeIconChar.BoxedLetterD,
|
||||
PrefixColor = 539,
|
||||
IsSubmenu = true,
|
||||
Priority = int.MaxValue,
|
||||
Name = $"See More ({submenuItems.Length})",
|
||||
OnClicked = a => a.OpenSubmenu(submenuItems),
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
private void OpenSubmenu(SeString name, IReadOnlyList<MenuItem> submenuItems, int posX, int posY)
|
||||
{
|
||||
if (submenuItems.Count == 0)
|
||||
throw new ArgumentException("Submenu must not be empty", nameof(submenuItems));
|
||||
|
||||
this.SubmenuItems = submenuItems;
|
||||
|
||||
var module = RaptureAtkModule.Instance();
|
||||
var values = this.CreateEmptySubmenuContextMenuArray(name, posX, posY, out var valueCount);
|
||||
|
||||
switch (this.SelectedMenuType)
|
||||
{
|
||||
case ContextMenuType.Default:
|
||||
{
|
||||
var ownerAddonId = ((AgentContext*)this.SelectedAgent)->OwnerAddon;
|
||||
this.raptureAtkModuleOpenAddon(module, 445, (uint)valueCount, values, this.SelectedAgent, 71, checked((ushort)ownerAddonId), 4);
|
||||
break;
|
||||
}
|
||||
|
||||
case ContextMenuType.Inventory:
|
||||
{
|
||||
var ownerAddonId = ((AgentInventoryContext*)this.SelectedAgent)->OwnerAddonId;
|
||||
this.raptureAtkModuleOpenAddon(module, 445, (uint)valueCount, values, this.SelectedAgent, 0, checked((ushort)ownerAddonId), 4);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
Log.Warning($"Unknown context menu type (agent: {(nint)this.SelectedAgent}, cannot open submenu");
|
||||
break;
|
||||
}
|
||||
|
||||
this.FreeExpandedContextMenuArray(values, valueCount);
|
||||
}
|
||||
|
||||
private bool AddonContextMenuOnMenuSelectedDetour(AddonContextMenu* addon, int selectedIdx, byte a3)
|
||||
{
|
||||
var items = this.SubmenuItems ?? this.SelectedItems;
|
||||
if (items == null)
|
||||
goto original;
|
||||
if (this.MenuCallbackIds.Count == 0)
|
||||
goto original;
|
||||
if (selectedIdx < 0)
|
||||
goto original;
|
||||
if (selectedIdx >= this.MenuCallbackIds.Count)
|
||||
goto original;
|
||||
|
||||
var callbackId = this.MenuCallbackIds[selectedIdx];
|
||||
|
||||
if (callbackId < 0)
|
||||
{
|
||||
selectedIdx = -callbackId - 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
var item = items[callbackId];
|
||||
var openedSubmenu = false;
|
||||
|
||||
try
|
||||
{
|
||||
if (item.OnClicked == null)
|
||||
throw new InvalidOperationException("Item has no OnClicked handler");
|
||||
item.OnClicked.InvokeSafely(new MenuItemClickedArgs(
|
||||
(name, submenuItems) =>
|
||||
{
|
||||
short x, y;
|
||||
addon->AtkUnitBase.GetPosition(&x, &y);
|
||||
this.OpenSubmenu(name ?? item.Name, submenuItems, x, y);
|
||||
openedSubmenu = true;
|
||||
},
|
||||
this.SelectedParentAddon,
|
||||
this.SelectedAgent,
|
||||
this.SelectedMenuType ?? default,
|
||||
this.SelectedEventInterfaces));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Error while handling context menu click");
|
||||
}
|
||||
|
||||
// Close with click sound
|
||||
if (!openedSubmenu)
|
||||
addon->AtkUnitBase.FireCallbackInt(-2);
|
||||
return false;
|
||||
}
|
||||
|
||||
original:
|
||||
// Eventually handled by inventory context here: 14022BBD0 (6.51)
|
||||
return this.addonContextMenuOnMenuSelectedHook.Original(addon, selectedIdx, a3);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plugin-scoped version of a <see cref="ContextMenu"/> service.
|
||||
/// </summary>
|
||||
[PluginInterface]
|
||||
[InterfaceVersion("1.0")]
|
||||
[ServiceManager.ScopedService]
|
||||
#pragma warning disable SA1015
|
||||
[ResolveVia<IContextMenu>]
|
||||
#pragma warning restore SA1015
|
||||
internal class ContextMenuPluginScoped : IInternalDisposableService, IContextMenu
|
||||
{
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly ContextMenu parentService = Service<ContextMenu>.Get();
|
||||
|
||||
private ContextMenuPluginScoped()
|
||||
{
|
||||
this.parentService.OnMenuOpened += this.OnMenuOpenedForward;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event IContextMenu.OnMenuOpenedDelegate? OnMenuOpened;
|
||||
|
||||
private Dictionary<ContextMenuType, List<MenuItem>> MenuItems { get; } = new();
|
||||
|
||||
private object MenuItemsLock { get; } = new();
|
||||
|
||||
/// <inheritdoc/>
|
||||
void IInternalDisposableService.DisposeService()
|
||||
{
|
||||
this.parentService.OnMenuOpened -= this.OnMenuOpenedForward;
|
||||
|
||||
this.OnMenuOpened = null;
|
||||
|
||||
lock (this.MenuItemsLock)
|
||||
{
|
||||
foreach (var (menuType, items) in this.MenuItems)
|
||||
{
|
||||
foreach (var item in items)
|
||||
this.parentService.RemoveMenuItem(menuType, item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void AddMenuItem(ContextMenuType menuType, MenuItem item)
|
||||
{
|
||||
lock (this.MenuItemsLock)
|
||||
{
|
||||
if (!this.MenuItems.TryGetValue(menuType, out var items))
|
||||
this.MenuItems[menuType] = items = new();
|
||||
items.Add(item);
|
||||
}
|
||||
|
||||
this.parentService.AddMenuItem(menuType, item);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool RemoveMenuItem(ContextMenuType menuType, MenuItem item)
|
||||
{
|
||||
lock (this.MenuItemsLock)
|
||||
{
|
||||
if (this.MenuItems.TryGetValue(menuType, out var items))
|
||||
items.Remove(item);
|
||||
}
|
||||
|
||||
return this.parentService.RemoveMenuItem(menuType, item);
|
||||
}
|
||||
|
||||
private void OnMenuOpenedForward(MenuOpenedArgs args) =>
|
||||
this.OnMenuOpened?.Invoke(args);
|
||||
}
|
||||
18
Dalamud/Game/Gui/ContextMenu/ContextMenuType.cs
Normal file
18
Dalamud/Game/Gui/ContextMenu/ContextMenuType.cs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
namespace Dalamud.Game.Gui.ContextMenu;
|
||||
|
||||
/// <summary>
|
||||
/// The type of context menu.
|
||||
/// Each one has a different associated <see cref="MenuTarget"/>.
|
||||
/// </summary>
|
||||
public enum ContextMenuType
|
||||
{
|
||||
/// <summary>
|
||||
/// The default context menu.
|
||||
/// </summary>
|
||||
Default,
|
||||
|
||||
/// <summary>
|
||||
/// The inventory context menu. Used when right-clicked on an item.
|
||||
/// </summary>
|
||||
Inventory,
|
||||
}
|
||||
87
Dalamud/Game/Gui/ContextMenu/MenuArgs.cs
Normal file
87
Dalamud/Game/Gui/ContextMenu/MenuArgs.cs
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
using Dalamud.Memory;
|
||||
using Dalamud.Plugin.Services;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
|
||||
namespace Dalamud.Game.Gui.ContextMenu;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for <see cref="IContextMenu"/> menu args.
|
||||
/// </summary>
|
||||
public abstract unsafe class MenuArgs
|
||||
{
|
||||
private IReadOnlySet<nint>? eventInterfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MenuArgs"/> class.
|
||||
/// </summary>
|
||||
/// <param name="addon">Addon associated with the context menu.</param>
|
||||
/// <param name="agent">Agent associated with the context menu.</param>
|
||||
/// <param name="type">The type of context menu.</param>
|
||||
/// <param name="eventInterfaces">List of AtkEventInterfaces associated with the context menu.</param>
|
||||
protected internal MenuArgs(AtkUnitBase* addon, AgentInterface* agent, ContextMenuType type, IReadOnlySet<nint>? eventInterfaces)
|
||||
{
|
||||
this.AddonName = addon != null ? MemoryHelper.ReadString((nint)addon->Name, 32) : null;
|
||||
this.AddonPtr = (nint)addon;
|
||||
this.AgentPtr = (nint)agent;
|
||||
this.MenuType = type;
|
||||
this.eventInterfaces = eventInterfaces;
|
||||
this.Target = type switch
|
||||
{
|
||||
ContextMenuType.Default => new MenuTargetDefault((AgentContext*)agent),
|
||||
ContextMenuType.Inventory => new MenuTargetInventory((AgentInventoryContext*)agent),
|
||||
_ => throw new ArgumentException("Invalid context menu type", nameof(type)),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the addon that opened the context menu.
|
||||
/// </summary>
|
||||
public string? AddonName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the memory pointer of the addon that opened the context menu.
|
||||
/// </summary>
|
||||
public nint AddonPtr { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the memory pointer of the agent that opened the context menu.
|
||||
/// </summary>
|
||||
public nint AgentPtr { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of the context menu.
|
||||
/// </summary>
|
||||
public ContextMenuType MenuType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the target info of the context menu. The actual type depends on <see cref="MenuType"/>.
|
||||
/// <see cref="ContextMenuType.Default"/> signifies a <see cref="MenuTargetDefault"/>.
|
||||
/// <see cref="ContextMenuType.Inventory"/> signifies a <see cref="MenuTargetInventory"/>.
|
||||
/// </summary>
|
||||
public MenuTarget Target { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a list of AtkEventInterface pointers associated with the context menu.
|
||||
/// Only available with <see cref="ContextMenuType.Default"/>.
|
||||
/// Almost always an agent pointer. You can use this to find out what type of context menu it is.
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException">Thrown when the context menu is not a <see cref="ContextMenuType.Default"/>.</exception>
|
||||
public IReadOnlySet<nint> EventInterfaces
|
||||
{
|
||||
get
|
||||
{
|
||||
if (this.MenuType is ContextMenuType.Default)
|
||||
{
|
||||
return this.eventInterfaces ?? new HashSet<nint>();
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException("Not a default context menu");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
106
Dalamud/Game/Gui/ContextMenu/MenuItem.cs
Normal file
106
Dalamud/Game/Gui/ContextMenu/MenuItem.cs
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
using Dalamud.Game.Text;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
|
||||
using Lumina.Excel.GeneratedSheets;
|
||||
|
||||
namespace Dalamud.Game.Gui.ContextMenu;
|
||||
|
||||
/// <summary>
|
||||
/// A menu item that can be added to a context menu.
|
||||
/// </summary>
|
||||
public sealed record MenuItem
|
||||
{
|
||||
/// <summary>
|
||||
/// The default prefix used if no specific preset is specified.
|
||||
/// </summary>
|
||||
public const SeIconChar DalamudDefaultPrefix = SeIconChar.BoxedLetterD;
|
||||
|
||||
/// <summary>
|
||||
/// The default prefix color used if no specific preset is specified.
|
||||
/// </summary>
|
||||
public const ushort DalamudDefaultPrefixColor = 539;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the display name of the menu item.
|
||||
/// </summary>
|
||||
public SeString Name { get; set; } = SeString.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the prefix attached to the beginning of <see cref="Name"/>.
|
||||
/// </summary>
|
||||
public SeIconChar? Prefix { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Sets the character to prefix the <see cref="Name"/> with. Will be converted into a fancy boxed letter icon. Must be an uppercase letter.
|
||||
/// </summary>
|
||||
/// <exception cref="ArgumentException"><paramref name="value"/> must be an uppercase letter.</exception>
|
||||
public char? PrefixChar
|
||||
{
|
||||
set
|
||||
{
|
||||
if (value is { } prefix)
|
||||
{
|
||||
if (!char.IsAsciiLetterUpper(prefix))
|
||||
throw new ArgumentException("Prefix must be an uppercase letter", nameof(value));
|
||||
|
||||
this.Prefix = SeIconChar.BoxedLetterA + prefix - 'A';
|
||||
}
|
||||
else
|
||||
{
|
||||
this.Prefix = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the color of the <see cref="Prefix"/>. Specifies a <see cref="UIColor"/> row id.
|
||||
/// </summary>
|
||||
public ushort PrefixColor { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the dev wishes to intentionally use the default prefix symbol and color.
|
||||
/// </summary>
|
||||
public bool UseDefaultPrefix { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the callback to be invoked when the menu item is clicked.
|
||||
/// </summary>
|
||||
public Action<MenuItemClickedArgs>? OnClicked { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the priority (or order) with which the menu item should be displayed in descending order.
|
||||
/// Priorities below 0 will be displayed above the native menu items.
|
||||
/// Other priorities will be displayed below the native menu items.
|
||||
/// </summary>
|
||||
public int Priority { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the menu item is enabled.
|
||||
/// Disabled items will be faded and cannot be clicked on.
|
||||
/// </summary>
|
||||
public bool IsEnabled { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the menu item is a submenu.
|
||||
/// This value is purely visual. Submenu items will have an arrow to its right.
|
||||
/// </summary>
|
||||
public bool IsSubmenu { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the menu item is a return item.
|
||||
/// This value is purely visual. Return items will have a back arrow to its left.
|
||||
/// If both <see cref="IsSubmenu"/> and <see cref="IsReturn"/> are true, the return arrow will take precedence.
|
||||
/// </summary>
|
||||
public bool IsReturn { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name with the given prefix.
|
||||
/// </summary>
|
||||
internal SeString PrefixedName =>
|
||||
this.Prefix is { } prefix
|
||||
? new SeStringBuilder()
|
||||
.AddUiForeground($"{prefix.ToIconString()} ", this.PrefixColor)
|
||||
.Append(this.Name)
|
||||
.Build()
|
||||
: this.Name;
|
||||
}
|
||||
44
Dalamud/Game/Gui/ContextMenu/MenuItemClickedArgs.cs
Normal file
44
Dalamud/Game/Gui/ContextMenu/MenuItemClickedArgs.cs
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
|
||||
namespace Dalamud.Game.Gui.ContextMenu;
|
||||
|
||||
/// <summary>
|
||||
/// Callback args used when a menu item is clicked.
|
||||
/// </summary>
|
||||
public sealed unsafe class MenuItemClickedArgs : MenuArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MenuItemClickedArgs"/> class.
|
||||
/// </summary>
|
||||
/// <param name="openSubmenu">Callback for opening a submenu.</param>
|
||||
/// <param name="addon">Addon associated with the context menu.</param>
|
||||
/// <param name="agent">Agent associated with the context menu.</param>
|
||||
/// <param name="type">The type of context menu.</param>
|
||||
/// <param name="eventInterfaces">List of AtkEventInterfaces associated with the context menu.</param>
|
||||
internal MenuItemClickedArgs(Action<SeString?, IReadOnlyList<MenuItem>> openSubmenu, AtkUnitBase* addon, AgentInterface* agent, ContextMenuType type, IReadOnlySet<nint> eventInterfaces)
|
||||
: base(addon, agent, type, eventInterfaces)
|
||||
{
|
||||
this.OnOpenSubmenu = openSubmenu;
|
||||
}
|
||||
|
||||
private Action<SeString?, IReadOnlyList<MenuItem>> OnOpenSubmenu { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Opens a submenu with the given name and items.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the submenu, displayed at the top.</param>
|
||||
/// <param name="items">The items to display in the submenu.</param>
|
||||
public void OpenSubmenu(SeString name, IReadOnlyList<MenuItem> items) =>
|
||||
this.OnOpenSubmenu(name, items);
|
||||
|
||||
/// <summary>
|
||||
/// Opens a submenu with the given items.
|
||||
/// </summary>
|
||||
/// <param name="items">The items to display in the submenu.</param>
|
||||
public void OpenSubmenu(IReadOnlyList<MenuItem> items) =>
|
||||
this.OnOpenSubmenu(null, items);
|
||||
}
|
||||
34
Dalamud/Game/Gui/ContextMenu/MenuOpenedArgs.cs
Normal file
34
Dalamud/Game/Gui/ContextMenu/MenuOpenedArgs.cs
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
|
||||
namespace Dalamud.Game.Gui.ContextMenu;
|
||||
|
||||
/// <summary>
|
||||
/// Callback args used when a menu item is opened.
|
||||
/// </summary>
|
||||
public sealed unsafe class MenuOpenedArgs : MenuArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MenuOpenedArgs"/> class.
|
||||
/// </summary>
|
||||
/// <param name="addMenuItem">Callback for adding a custom menu item.</param>
|
||||
/// <param name="addon">Addon associated with the context menu.</param>
|
||||
/// <param name="agent">Agent associated with the context menu.</param>
|
||||
/// <param name="type">The type of context menu.</param>
|
||||
/// <param name="eventInterfaces">List of AtkEventInterfaces associated with the context menu.</param>
|
||||
internal MenuOpenedArgs(Action<MenuItem> addMenuItem, AtkUnitBase* addon, AgentInterface* agent, ContextMenuType type, IReadOnlySet<nint> eventInterfaces)
|
||||
: base(addon, agent, type, eventInterfaces)
|
||||
{
|
||||
this.OnAddMenuItem = addMenuItem;
|
||||
}
|
||||
|
||||
private Action<MenuItem> OnAddMenuItem { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Adds a custom menu item to the context menu.
|
||||
/// </summary>
|
||||
/// <param name="item">The menu item to add.</param>
|
||||
public void AddMenuItem(MenuItem item) =>
|
||||
this.OnAddMenuItem(item);
|
||||
}
|
||||
9
Dalamud/Game/Gui/ContextMenu/MenuTarget.cs
Normal file
9
Dalamud/Game/Gui/ContextMenu/MenuTarget.cs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
namespace Dalamud.Game.Gui.ContextMenu;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for <see cref="MenuArgs"/> contexts.
|
||||
/// Discriminated based on <see cref="ContextMenuType"/>.
|
||||
/// </summary>
|
||||
public abstract class MenuTarget
|
||||
{
|
||||
}
|
||||
67
Dalamud/Game/Gui/ContextMenu/MenuTargetDefault.cs
Normal file
67
Dalamud/Game/Gui/ContextMenu/MenuTargetDefault.cs
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
using Dalamud.Game.ClientState.Objects;
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Dalamud.Game.ClientState.Resolvers;
|
||||
using Dalamud.Game.Network.Structures.InfoProxy;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
|
||||
using Lumina.Excel.GeneratedSheets;
|
||||
|
||||
namespace Dalamud.Game.Gui.ContextMenu;
|
||||
|
||||
/// <summary>
|
||||
/// Target information on a default context menu.
|
||||
/// </summary>
|
||||
public sealed unsafe class MenuTargetDefault : MenuTarget
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MenuTargetDefault"/> class.
|
||||
/// </summary>
|
||||
/// <param name="context">The agent associated with the context menu.</param>
|
||||
internal MenuTargetDefault(AgentContext* context)
|
||||
{
|
||||
this.Context = context;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the target.
|
||||
/// </summary>
|
||||
public string TargetName => this.Context->TargetName.ToString();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the object id of the target.
|
||||
/// </summary>
|
||||
public ulong TargetObjectId => this.Context->TargetObjectId;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the target object.
|
||||
/// </summary>
|
||||
public GameObject? TargetObject => Service<ObjectTable>.Get().SearchById(this.TargetObjectId);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the content id of the target.
|
||||
/// </summary>
|
||||
public ulong TargetContentId => this.Context->TargetContentId;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the home world id of the target.
|
||||
/// </summary>
|
||||
public ExcelResolver<World> TargetHomeWorld => new((uint)this.Context->TargetHomeWorldId);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the currently targeted character. Only shows up for specific targets, like friends, party finder listings, or party members.
|
||||
/// Just because this is <see langword="null"/> doesn't mean the target isn't a character.
|
||||
/// </summary>
|
||||
public CharacterData? TargetCharacter
|
||||
{
|
||||
get
|
||||
{
|
||||
var target = this.Context->CurrentContextMenuTarget;
|
||||
if (target != null)
|
||||
return new(target);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private AgentContext* Context { get; }
|
||||
}
|
||||
36
Dalamud/Game/Gui/ContextMenu/MenuTargetInventory.cs
Normal file
36
Dalamud/Game/Gui/ContextMenu/MenuTargetInventory.cs
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
using Dalamud.Game.Inventory;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
|
||||
namespace Dalamud.Game.Gui.ContextMenu;
|
||||
|
||||
/// <summary>
|
||||
/// Target information on an inventory context menu.
|
||||
/// </summary>
|
||||
public sealed unsafe class MenuTargetInventory : MenuTarget
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MenuTargetInventory"/> class.
|
||||
/// </summary>
|
||||
/// <param name="context">The agent associated with the context menu.</param>
|
||||
internal MenuTargetInventory(AgentInventoryContext* context)
|
||||
{
|
||||
this.Context = context;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the target item.
|
||||
/// </summary>
|
||||
public GameInventoryItem? TargetItem
|
||||
{
|
||||
get
|
||||
{
|
||||
var target = this.Context->TargetInventorySlot;
|
||||
if (target != null)
|
||||
return new(*target);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private AgentInventoryContext* Context { get; }
|
||||
}
|
||||
|
|
@ -21,8 +21,8 @@ namespace Dalamud.Game.Gui.Dtr;
|
|||
/// Class used to interface with the server info bar.
|
||||
/// </summary>
|
||||
[InterfaceVersion("1.0")]
|
||||
[ServiceManager.BlockingEarlyLoadedService]
|
||||
internal sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar
|
||||
[ServiceManager.EarlyLoadedService]
|
||||
internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
|
||||
{
|
||||
private const uint BaseNodeId = 1000;
|
||||
|
||||
|
|
@ -101,7 +101,7 @@ internal sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar
|
|||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
void IDisposable.Dispose()
|
||||
void IInternalDisposableService.DisposeService()
|
||||
{
|
||||
this.addonLifecycle.UnregisterListener(this.dtrPostDrawListener);
|
||||
this.addonLifecycle.UnregisterListener(this.dtrPostRequestedUpdateListener);
|
||||
|
|
@ -493,7 +493,7 @@ internal sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar
|
|||
#pragma warning disable SA1015
|
||||
[ResolveVia<IDtrBar>]
|
||||
#pragma warning restore SA1015
|
||||
internal class DtrBarPluginScoped : IDisposable, IServiceType, IDtrBar
|
||||
internal class DtrBarPluginScoped : IInternalDisposableService, IDtrBar
|
||||
{
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly DtrBar dtrBarService = Service<DtrBar>.Get();
|
||||
|
|
@ -501,7 +501,7 @@ internal class DtrBarPluginScoped : IDisposable, IServiceType, IDtrBar
|
|||
private readonly Dictionary<string, DtrBarEntry> pluginEntries = new();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
void IInternalDisposableService.DisposeService()
|
||||
{
|
||||
foreach (var entry in this.pluginEntries)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@ namespace Dalamud.Game.Gui.FlyText;
|
|||
/// This class facilitates interacting with and creating native in-game "fly text".
|
||||
/// </summary>
|
||||
[InterfaceVersion("1.0")]
|
||||
[ServiceManager.BlockingEarlyLoadedService]
|
||||
internal sealed class FlyTextGui : IDisposable, IServiceType, IFlyTextGui
|
||||
[ServiceManager.EarlyLoadedService]
|
||||
internal sealed class FlyTextGui : IInternalDisposableService, IFlyTextGui
|
||||
{
|
||||
/// <summary>
|
||||
/// The native function responsible for adding fly text to the UI. See <see cref="FlyTextGuiAddressResolver.AddFlyText"/>.
|
||||
|
|
@ -78,7 +78,7 @@ internal sealed class FlyTextGui : IDisposable, IServiceType, IFlyTextGui
|
|||
/// <summary>
|
||||
/// Disposes of managed and unmanaged resources.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
void IInternalDisposableService.DisposeService()
|
||||
{
|
||||
this.createFlyTextHook.Dispose();
|
||||
}
|
||||
|
|
@ -277,7 +277,7 @@ internal sealed class FlyTextGui : IDisposable, IServiceType, IFlyTextGui
|
|||
#pragma warning disable SA1015
|
||||
[ResolveVia<IFlyTextGui>]
|
||||
#pragma warning restore SA1015
|
||||
internal class FlyTextGuiPluginScoped : IDisposable, IServiceType, IFlyTextGui
|
||||
internal class FlyTextGuiPluginScoped : IInternalDisposableService, IFlyTextGui
|
||||
{
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly FlyTextGui flyTextGuiService = Service<FlyTextGui>.Get();
|
||||
|
|
@ -294,7 +294,7 @@ internal class FlyTextGuiPluginScoped : IDisposable, IServiceType, IFlyTextGui
|
|||
public event IFlyTextGui.OnFlyTextCreatedDelegate? FlyTextCreated;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
void IInternalDisposableService.DisposeService()
|
||||
{
|
||||
this.flyTextGuiService.FlyTextCreated -= this.FlyTextCreatedForward;
|
||||
|
||||
|
|
|
|||
|
|
@ -26,8 +26,8 @@ namespace Dalamud.Game.Gui;
|
|||
/// A class handling many aspects of the in-game UI.
|
||||
/// </summary>
|
||||
[InterfaceVersion("1.0")]
|
||||
[ServiceManager.BlockingEarlyLoadedService]
|
||||
internal sealed unsafe class GameGui : IDisposable, IServiceType, IGameGui
|
||||
[ServiceManager.EarlyLoadedService]
|
||||
internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui
|
||||
{
|
||||
private static readonly ModuleLog Log = new("GameGui");
|
||||
|
||||
|
|
@ -344,7 +344,7 @@ internal sealed unsafe class GameGui : IDisposable, IServiceType, IGameGui
|
|||
/// <summary>
|
||||
/// Disables the hooks and submodules of this module.
|
||||
/// </summary>
|
||||
void IDisposable.Dispose()
|
||||
void IInternalDisposableService.DisposeService()
|
||||
{
|
||||
this.setGlobalBgmHook.Dispose();
|
||||
this.handleItemHoverHook.Dispose();
|
||||
|
|
@ -520,7 +520,7 @@ internal sealed unsafe class GameGui : IDisposable, IServiceType, IGameGui
|
|||
#pragma warning disable SA1015
|
||||
[ResolveVia<IGameGui>]
|
||||
#pragma warning restore SA1015
|
||||
internal class GameGuiPluginScoped : IDisposable, IServiceType, IGameGui
|
||||
internal class GameGuiPluginScoped : IInternalDisposableService, IGameGui
|
||||
{
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly GameGui gameGuiService = Service<GameGui>.Get();
|
||||
|
|
@ -558,7 +558,7 @@ internal class GameGuiPluginScoped : IDisposable, IServiceType, IGameGui
|
|||
public HoveredAction HoveredAction => this.gameGuiService.HoveredAction;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
void IInternalDisposableService.DisposeService()
|
||||
{
|
||||
this.gameGuiService.UiHideToggled -= this.UiHideToggledForward;
|
||||
this.gameGuiService.HoveredItemChanged -= this.HoveredItemForward;
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@ namespace Dalamud.Game.Gui.PartyFinder;
|
|||
/// This class handles interacting with the native PartyFinder window.
|
||||
/// </summary>
|
||||
[InterfaceVersion("1.0")]
|
||||
[ServiceManager.BlockingEarlyLoadedService]
|
||||
internal sealed class PartyFinderGui : IDisposable, IServiceType, IPartyFinderGui
|
||||
[ServiceManager.EarlyLoadedService]
|
||||
internal sealed class PartyFinderGui : IInternalDisposableService, IPartyFinderGui
|
||||
{
|
||||
private readonly PartyFinderAddressResolver address;
|
||||
private readonly IntPtr memory;
|
||||
|
|
@ -47,7 +47,7 @@ internal sealed class PartyFinderGui : IDisposable, IServiceType, IPartyFinderGu
|
|||
/// <summary>
|
||||
/// Dispose of managed and unmanaged resources.
|
||||
/// </summary>
|
||||
void IDisposable.Dispose()
|
||||
void IInternalDisposableService.DisposeService()
|
||||
{
|
||||
this.receiveListingHook.Dispose();
|
||||
|
||||
|
|
@ -131,7 +131,7 @@ internal sealed class PartyFinderGui : IDisposable, IServiceType, IPartyFinderGu
|
|||
#pragma warning disable SA1015
|
||||
[ResolveVia<IPartyFinderGui>]
|
||||
#pragma warning restore SA1015
|
||||
internal class PartyFinderGuiPluginScoped : IDisposable, IServiceType, IPartyFinderGui
|
||||
internal class PartyFinderGuiPluginScoped : IInternalDisposableService, IPartyFinderGui
|
||||
{
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly PartyFinderGui partyFinderGuiService = Service<PartyFinderGui>.Get();
|
||||
|
|
@ -148,7 +148,7 @@ internal class PartyFinderGuiPluginScoped : IDisposable, IServiceType, IPartyFin
|
|||
public event IPartyFinderGui.PartyFinderListingEventDelegate? ReceiveListing;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
void IInternalDisposableService.DisposeService()
|
||||
{
|
||||
this.partyFinderGuiService.ReceiveListing -= this.ReceiveListingForward;
|
||||
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@ namespace Dalamud.Game.Gui.Toast;
|
|||
/// This class facilitates interacting with and creating native toast windows.
|
||||
/// </summary>
|
||||
[InterfaceVersion("1.0")]
|
||||
[ServiceManager.BlockingEarlyLoadedService]
|
||||
internal sealed partial class ToastGui : IDisposable, IServiceType, IToastGui
|
||||
[ServiceManager.EarlyLoadedService]
|
||||
internal sealed partial class ToastGui : IInternalDisposableService, IToastGui
|
||||
{
|
||||
private const uint QuestToastCheckmarkMagic = 60081;
|
||||
|
||||
|
|
@ -73,7 +73,7 @@ internal sealed partial class ToastGui : IDisposable, IServiceType, IToastGui
|
|||
/// <summary>
|
||||
/// Disposes of managed and unmanaged resources.
|
||||
/// </summary>
|
||||
void IDisposable.Dispose()
|
||||
void IInternalDisposableService.DisposeService()
|
||||
{
|
||||
this.showNormalToastHook.Dispose();
|
||||
this.showQuestToastHook.Dispose();
|
||||
|
|
@ -383,7 +383,7 @@ internal sealed partial class ToastGui
|
|||
#pragma warning disable SA1015
|
||||
[ResolveVia<IToastGui>]
|
||||
#pragma warning restore SA1015
|
||||
internal class ToastGuiPluginScoped : IDisposable, IServiceType, IToastGui
|
||||
internal class ToastGuiPluginScoped : IInternalDisposableService, IToastGui
|
||||
{
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly ToastGui toastGuiService = Service<ToastGui>.Get();
|
||||
|
|
@ -408,7 +408,7 @@ internal class ToastGuiPluginScoped : IDisposable, IServiceType, IToastGui
|
|||
public event IToastGui.OnErrorToastDelegate? ErrorToast;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
void IInternalDisposableService.DisposeService()
|
||||
{
|
||||
this.toastGuiService.Toast -= this.ToastForward;
|
||||
this.toastGuiService.QuestToast -= this.QuestToastForward;
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ namespace Dalamud.Game.Internal;
|
|||
/// This class disables anti-debug functionality in the game client.
|
||||
/// </summary>
|
||||
[ServiceManager.EarlyLoadedService]
|
||||
internal sealed partial class AntiDebug : IServiceType
|
||||
internal sealed class AntiDebug : IInternalDisposableService
|
||||
{
|
||||
private readonly byte[] nop = new byte[] { 0x31, 0xC0, 0x90, 0x90, 0x90, 0x90 };
|
||||
private byte[] original;
|
||||
|
|
@ -43,16 +43,25 @@ internal sealed partial class AntiDebug : IServiceType
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>Finalizes an instance of the <see cref="AntiDebug"/> class.</summary>
|
||||
~AntiDebug() => this.Disable();
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the anti-debugging is enabled.
|
||||
/// </summary>
|
||||
public bool IsEnabled { get; private set; } = false;
|
||||
|
||||
/// <inheritdoc />
|
||||
void IInternalDisposableService.DisposeService() => this.Disable();
|
||||
|
||||
/// <summary>
|
||||
/// Enables the anti-debugging by overwriting code in memory.
|
||||
/// </summary>
|
||||
public void Enable()
|
||||
{
|
||||
if (this.IsEnabled)
|
||||
return;
|
||||
|
||||
this.original = new byte[this.nop.Length];
|
||||
if (this.debugCheckAddress != IntPtr.Zero && !this.IsEnabled)
|
||||
{
|
||||
|
|
@ -73,6 +82,9 @@ internal sealed partial class AntiDebug : IServiceType
|
|||
/// </summary>
|
||||
public void Disable()
|
||||
{
|
||||
if (!this.IsEnabled)
|
||||
return;
|
||||
|
||||
if (this.debugCheckAddress != IntPtr.Zero && this.original != null)
|
||||
{
|
||||
Log.Information($"Reverting debug check at 0x{this.debugCheckAddress.ToInt64():X}");
|
||||
|
|
@ -86,45 +98,3 @@ internal sealed partial class AntiDebug : IServiceType
|
|||
this.IsEnabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implementing IDisposable.
|
||||
/// </summary>
|
||||
internal sealed partial class AntiDebug : IDisposable
|
||||
{
|
||||
private bool disposed = false;
|
||||
|
||||
/// <summary>
|
||||
/// Finalizes an instance of the <see cref="AntiDebug"/> class.
|
||||
/// </summary>
|
||||
~AntiDebug() => this.Dispose(false);
|
||||
|
||||
/// <summary>
|
||||
/// Disposes of managed and unmanaged resources.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
this.Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes of managed and unmanaged resources.
|
||||
/// </summary>
|
||||
/// <param name="disposing">If this was disposed through calling Dispose() or from being finalized.</param>
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (this.disposed)
|
||||
return;
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
// If anti-debug is enabled and is being disposed, odds are either the game is exiting, or Dalamud is being reloaded.
|
||||
// If it is the latter, there's half a chance a debugger is currently attached. There's no real need to disable the
|
||||
// check in either situation anyways. However if Dalamud is being reloaded, the sig may fail so may as well undo it.
|
||||
this.Disable();
|
||||
}
|
||||
|
||||
this.disposed = true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ namespace Dalamud.Game.Internal;
|
|||
/// This class implements in-game Dalamud options in the in-game System menu.
|
||||
/// </summary>
|
||||
[ServiceManager.EarlyLoadedService]
|
||||
internal sealed unsafe partial class DalamudAtkTweaks : IServiceType
|
||||
internal sealed unsafe class DalamudAtkTweaks : IInternalDisposableService
|
||||
{
|
||||
private readonly AtkValueChangeType atkValueChangeType;
|
||||
private readonly AtkValueSetString atkValueSetString;
|
||||
|
|
@ -40,6 +40,8 @@ internal sealed unsafe partial class DalamudAtkTweaks : IServiceType
|
|||
private readonly string locDalamudPlugins;
|
||||
private readonly string locDalamudSettings;
|
||||
|
||||
private bool disposed = false;
|
||||
|
||||
[ServiceManager.ServiceConstructor]
|
||||
private DalamudAtkTweaks(TargetSigScanner sigScanner)
|
||||
{
|
||||
|
|
@ -69,6 +71,9 @@ internal sealed unsafe partial class DalamudAtkTweaks : IServiceType
|
|||
this.hookAtkUnitBaseReceiveGlobalEvent.Enable();
|
||||
}
|
||||
|
||||
/// <summary>Finalizes an instance of the <see cref="DalamudAtkTweaks"/> class.</summary>
|
||||
~DalamudAtkTweaks() => this.Dispose(false);
|
||||
|
||||
private delegate void AgentHudOpenSystemMenuPrototype(void* thisPtr, AtkValue* atkValueArgs, uint menuSize);
|
||||
|
||||
private delegate void AtkValueChangeType(AtkValue* thisPtr, ValueType type);
|
||||
|
|
@ -79,6 +84,26 @@ internal sealed unsafe partial class DalamudAtkTweaks : IServiceType
|
|||
|
||||
private delegate IntPtr AtkUnitBaseReceiveGlobalEvent(AtkUnitBase* thisPtr, ushort cmd, uint a3, IntPtr a4, uint* a5);
|
||||
|
||||
/// <inheritdoc/>
|
||||
void IInternalDisposableService.DisposeService() => this.Dispose(true);
|
||||
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (this.disposed)
|
||||
return;
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
this.hookAgentHudOpenSystemMenu.Dispose();
|
||||
this.hookUiModuleRequestMainCommand.Dispose();
|
||||
this.hookAtkUnitBaseReceiveGlobalEvent.Dispose();
|
||||
|
||||
// this.contextMenu.ContextMenuOpened -= this.ContextMenuOnContextMenuOpened;
|
||||
}
|
||||
|
||||
this.disposed = true;
|
||||
}
|
||||
|
||||
/*
|
||||
private void ContextMenuOnContextMenuOpened(ContextMenuOpenedArgs args)
|
||||
{
|
||||
|
|
@ -229,45 +254,3 @@ internal sealed unsafe partial class DalamudAtkTweaks : IServiceType
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implements IDisposable.
|
||||
/// </summary>
|
||||
internal sealed partial class DalamudAtkTweaks : IDisposable
|
||||
{
|
||||
private bool disposed = false;
|
||||
|
||||
/// <summary>
|
||||
/// Finalizes an instance of the <see cref="DalamudAtkTweaks"/> class.
|
||||
/// </summary>
|
||||
~DalamudAtkTweaks() => this.Dispose(false);
|
||||
|
||||
/// <summary>
|
||||
/// Dispose of managed and unmanaged resources.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
this.Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose of managed and unmanaged resources.
|
||||
/// </summary>
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (this.disposed)
|
||||
return;
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
this.hookAgentHudOpenSystemMenu.Dispose();
|
||||
this.hookUiModuleRequestMainCommand.Dispose();
|
||||
this.hookAtkUnitBaseReceiveGlobalEvent.Dispose();
|
||||
|
||||
// this.contextMenu.ContextMenuOpened -= this.ContextMenuOnContextMenuOpened;
|
||||
}
|
||||
|
||||
this.disposed = true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@ namespace Dalamud.Game.Inventory;
|
|||
/// This class provides events for the players in-game inventory.
|
||||
/// </summary>
|
||||
[InterfaceVersion("1.0")]
|
||||
[ServiceManager.BlockingEarlyLoadedService]
|
||||
internal class GameInventory : IDisposable, IServiceType
|
||||
[ServiceManager.EarlyLoadedService]
|
||||
internal class GameInventory : IInternalDisposableService
|
||||
{
|
||||
private readonly List<GameInventoryPluginScoped> subscribersPendingChange = new();
|
||||
private readonly List<GameInventoryPluginScoped> subscribers = new();
|
||||
|
|
@ -61,7 +61,7 @@ internal class GameInventory : IDisposable, IServiceType
|
|||
private unsafe delegate void RaptureAtkModuleUpdateDelegate(RaptureAtkModule* ram, float f1);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
void IInternalDisposableService.DisposeService()
|
||||
{
|
||||
lock (this.subscribersPendingChange)
|
||||
{
|
||||
|
|
@ -351,7 +351,7 @@ internal class GameInventory : IDisposable, IServiceType
|
|||
#pragma warning disable SA1015
|
||||
[ResolveVia<IGameInventory>]
|
||||
#pragma warning restore SA1015
|
||||
internal class GameInventoryPluginScoped : IDisposable, IServiceType, IGameInventory
|
||||
internal class GameInventoryPluginScoped : IInternalDisposableService, IGameInventory
|
||||
{
|
||||
private static readonly ModuleLog Log = new(nameof(GameInventoryPluginScoped));
|
||||
|
||||
|
|
@ -406,7 +406,7 @@ internal class GameInventoryPluginScoped : IDisposable, IServiceType, IGameInven
|
|||
public event IGameInventory.InventoryChangedDelegate<InventoryItemMergedArgs>? ItemMergedExplicit;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
void IInternalDisposableService.DisposeService()
|
||||
{
|
||||
this.gameInventoryService.Unsubscribe(this);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
using System.Diagnostics;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
using Dalamud.Utility;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||
|
||||
namespace Dalamud.Game.Inventory;
|
||||
|
|
@ -103,7 +106,7 @@ public unsafe struct GameInventoryItem : IEquatable<GameInventoryItem>
|
|||
/// <summary>
|
||||
/// Gets the array of materia grades.
|
||||
/// </summary>
|
||||
public ReadOnlySpan<ushort> MateriaGrade =>
|
||||
public ReadOnlySpan<byte> MateriaGrade =>
|
||||
new(Unsafe.AsPointer(ref Unsafe.AsRef(in this.InternalItem.MateriaGrade[0])), 5);
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -1,72 +0,0 @@
|
|||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
using Dalamud.IoC;
|
||||
using Dalamud.IoC.Internal;
|
||||
using Dalamud.Plugin.Services;
|
||||
|
||||
namespace Dalamud.Game.Libc;
|
||||
|
||||
/// <summary>
|
||||
/// This class handles creating cstrings utilizing native game methods.
|
||||
/// </summary>
|
||||
[PluginInterface]
|
||||
[InterfaceVersion("1.0")]
|
||||
[ServiceManager.BlockingEarlyLoadedService]
|
||||
#pragma warning disable SA1015
|
||||
[ResolveVia<ILibcFunction>]
|
||||
#pragma warning restore SA1015
|
||||
internal sealed class LibcFunction : IServiceType, ILibcFunction
|
||||
{
|
||||
private readonly LibcFunctionAddressResolver address;
|
||||
private readonly StdStringFromCStringDelegate stdStringCtorCString;
|
||||
private readonly StdStringDeallocateDelegate stdStringDeallocate;
|
||||
|
||||
[ServiceManager.ServiceConstructor]
|
||||
private LibcFunction(TargetSigScanner sigScanner)
|
||||
{
|
||||
this.address = new LibcFunctionAddressResolver();
|
||||
this.address.Setup(sigScanner);
|
||||
|
||||
this.stdStringCtorCString = Marshal.GetDelegateForFunctionPointer<StdStringFromCStringDelegate>(this.address.StdStringFromCstring);
|
||||
this.stdStringDeallocate = Marshal.GetDelegateForFunctionPointer<StdStringDeallocateDelegate>(this.address.StdStringDeallocate);
|
||||
}
|
||||
|
||||
// TODO: prolly callconv is not okay in x86
|
||||
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
|
||||
private delegate IntPtr StdStringFromCStringDelegate(IntPtr pStdString, [MarshalAs(UnmanagedType.LPArray)] byte[] content, IntPtr size);
|
||||
|
||||
// TODO: prolly callconv is not okay in x86
|
||||
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
|
||||
private delegate IntPtr StdStringDeallocateDelegate(IntPtr address);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public OwnedStdString NewString(byte[] content)
|
||||
{
|
||||
// While 0x70 bytes in the memory should be enough in DX11 version,
|
||||
// I don't trust my analysis so we're just going to allocate almost two times more than that.
|
||||
var pString = Marshal.AllocHGlobal(256);
|
||||
|
||||
// Initialize a string
|
||||
var size = new IntPtr(content.Length);
|
||||
var pReallocString = this.stdStringCtorCString(pString, content, size);
|
||||
|
||||
// Log.Verbose("Prev: {Prev} Now: {Now}", pString, pReallocString);
|
||||
|
||||
return new OwnedStdString(pReallocString, this.DeallocateStdString);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public OwnedStdString NewString(string content, Encoding? encoding = null)
|
||||
{
|
||||
encoding ??= Encoding.UTF8;
|
||||
|
||||
return this.NewString(encoding.GetBytes(content));
|
||||
}
|
||||
|
||||
private void DeallocateStdString(IntPtr address)
|
||||
{
|
||||
this.stdStringDeallocate(address);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
using System;
|
||||
|
||||
namespace Dalamud.Game.Libc;
|
||||
|
||||
/// <summary>
|
||||
/// The address resolver for the <see cref="LibcFunction"/> class.
|
||||
/// </summary>
|
||||
internal sealed class LibcFunctionAddressResolver : BaseAddressResolver
|
||||
{
|
||||
private delegate IntPtr StringFromCString();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the address of the native StdStringFromCstring method.
|
||||
/// </summary>
|
||||
public IntPtr StdStringFromCstring { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the address of the native StdStringDeallocate method.
|
||||
/// </summary>
|
||||
public IntPtr StdStringDeallocate { get; private set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void Setup64Bit(ISigScanner sig)
|
||||
{
|
||||
this.StdStringFromCstring = sig.ScanText("48 89 5C 24 08 48 89 74 24 10 57 48 83 EC 20 48 8D 41 22 66 C7 41 20 01 01 48 89 01 49 8B D8");
|
||||
this.StdStringDeallocate = sig.ScanText("80 79 21 00 75 12 48 8B 51 08 41 B8 33 00 00 00 48 8B 09 E9 ?? ?? ?? 00 C3");
|
||||
}
|
||||
}
|
||||
|
|
@ -1,100 +0,0 @@
|
|||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Dalamud.Game.Libc;
|
||||
|
||||
/// <summary>
|
||||
/// An address wrapper around the <see cref="StdString"/> class.
|
||||
/// </summary>
|
||||
public sealed partial class OwnedStdString
|
||||
{
|
||||
private readonly DeallocatorDelegate dealloc;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="OwnedStdString"/> class.
|
||||
/// Construct a wrapper around std::string.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Violating any of these might cause an undefined hehaviour.
|
||||
/// 1. This function takes the ownership of the address.
|
||||
/// 2. A memory pointed by address argument is assumed to be allocated by Marshal.AllocHGlobal thus will try to call Marshal.FreeHGlobal on the address.
|
||||
/// 3. std::string object pointed by address must be initialized before calling this function.
|
||||
/// </remarks>
|
||||
/// <param name="address">The address of the owned std string.</param>
|
||||
/// <param name="dealloc">A deallocator function.</param>
|
||||
internal OwnedStdString(IntPtr address, DeallocatorDelegate dealloc)
|
||||
{
|
||||
this.Address = address;
|
||||
this.dealloc = dealloc;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The delegate type that deallocates a std string.
|
||||
/// </summary>
|
||||
/// <param name="address">Address to deallocate.</param>
|
||||
internal delegate void DeallocatorDelegate(IntPtr address);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the address of the std string.
|
||||
/// </summary>
|
||||
public IntPtr Address { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Read the wrapped StdString.
|
||||
/// </summary>
|
||||
/// <returns>The StdString.</returns>
|
||||
public StdString Read() => StdString.ReadFromPointer(this.Address);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implements IDisposable.
|
||||
/// </summary>
|
||||
public sealed partial class OwnedStdString : IDisposable
|
||||
{
|
||||
private bool isDisposed;
|
||||
|
||||
/// <summary>
|
||||
/// Finalizes an instance of the <see cref="OwnedStdString"/> class.
|
||||
/// </summary>
|
||||
~OwnedStdString() => this.Dispose(false);
|
||||
|
||||
/// <summary>
|
||||
/// Dispose of managed and unmanaged resources.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
GC.SuppressFinalize(this);
|
||||
this.Dispose(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose of managed and unmanaged resources.
|
||||
/// </summary>
|
||||
/// <param name="disposing">A value indicating whether this was called via Dispose or finalized.</param>
|
||||
public void Dispose(bool disposing)
|
||||
{
|
||||
if (this.isDisposed)
|
||||
return;
|
||||
|
||||
this.isDisposed = true;
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
}
|
||||
|
||||
if (this.Address == IntPtr.Zero)
|
||||
{
|
||||
// Something got seriously fucked.
|
||||
throw new AccessViolationException();
|
||||
}
|
||||
|
||||
// Deallocate inner string first
|
||||
this.dealloc(this.Address);
|
||||
|
||||
// Free the heap
|
||||
Marshal.FreeHGlobal(this.Address);
|
||||
|
||||
// Better safe (running on a nullptr) than sorry. (running on a dangling pointer)
|
||||
this.Address = IntPtr.Zero;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace Dalamud.Game.Libc;
|
||||
|
||||
/// <summary>
|
||||
/// Interation with std::string.
|
||||
/// </summary>
|
||||
public class StdString
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StdString"/> class.
|
||||
/// </summary>
|
||||
private StdString()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the value of the cstring.
|
||||
/// </summary>
|
||||
public string Value { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the raw byte representation of the cstring.
|
||||
/// </summary>
|
||||
public byte[] RawData { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Marshal a null terminated cstring from memory to a UTF-8 encoded string.
|
||||
/// </summary>
|
||||
/// <param name="cstring">Address of the cstring.</param>
|
||||
/// <returns>A UTF-8 encoded string.</returns>
|
||||
public static StdString ReadFromPointer(IntPtr cstring)
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
if (cstring == IntPtr.Zero)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(cstring));
|
||||
}
|
||||
|
||||
var innerAddress = Marshal.ReadIntPtr(cstring);
|
||||
if (innerAddress == IntPtr.Zero)
|
||||
{
|
||||
throw new NullReferenceException("Inner reference to the cstring is null.");
|
||||
}
|
||||
|
||||
// Count the number of chars. String is assumed to be zero-terminated.
|
||||
|
||||
var count = 0;
|
||||
while (Marshal.ReadByte(innerAddress, count) != 0)
|
||||
{
|
||||
count += 1;
|
||||
}
|
||||
|
||||
// raw copy, as UTF8 string conversion is lossy
|
||||
var rawData = new byte[count];
|
||||
Marshal.Copy(innerAddress, rawData, 0, count);
|
||||
|
||||
return new StdString
|
||||
{
|
||||
RawData = rawData,
|
||||
Value = Encoding.UTF8.GetString(rawData),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -14,8 +14,8 @@ namespace Dalamud.Game.Network;
|
|||
/// This class handles interacting with game network events.
|
||||
/// </summary>
|
||||
[InterfaceVersion("1.0")]
|
||||
[ServiceManager.BlockingEarlyLoadedService]
|
||||
internal sealed class GameNetwork : IDisposable, IServiceType, IGameNetwork
|
||||
[ServiceManager.EarlyLoadedService]
|
||||
internal sealed class GameNetwork : IInternalDisposableService, IGameNetwork
|
||||
{
|
||||
private readonly GameNetworkAddressResolver address;
|
||||
private readonly Hook<ProcessZonePacketDownDelegate> processZonePacketDownHook;
|
||||
|
|
@ -59,7 +59,7 @@ internal sealed class GameNetwork : IDisposable, IServiceType, IGameNetwork
|
|||
public event IGameNetwork.OnNetworkMessageDelegate? NetworkMessage;
|
||||
|
||||
/// <inheritdoc/>
|
||||
void IDisposable.Dispose()
|
||||
void IInternalDisposableService.DisposeService()
|
||||
{
|
||||
this.processZonePacketDownHook.Dispose();
|
||||
this.processZonePacketUpHook.Dispose();
|
||||
|
|
@ -145,7 +145,7 @@ internal sealed class GameNetwork : IDisposable, IServiceType, IGameNetwork
|
|||
#pragma warning disable SA1015
|
||||
[ResolveVia<IGameNetwork>]
|
||||
#pragma warning restore SA1015
|
||||
internal class GameNetworkPluginScoped : IDisposable, IServiceType, IGameNetwork
|
||||
internal class GameNetworkPluginScoped : IInternalDisposableService, IGameNetwork
|
||||
{
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly GameNetwork gameNetworkService = Service<GameNetwork>.Get();
|
||||
|
|
@ -162,7 +162,7 @@ internal class GameNetworkPluginScoped : IDisposable, IServiceType, IGameNetwork
|
|||
public event IGameNetwork.OnNetworkMessageDelegate? NetworkMessage;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
void IInternalDisposableService.DisposeService()
|
||||
{
|
||||
this.gameNetworkService.NetworkMessage -= this.NetworkMessageForward;
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ using Dalamud.Game.Gui;
|
|||
using Dalamud.Game.Network.Internal.MarketBoardUploaders;
|
||||
using Dalamud.Game.Network.Internal.MarketBoardUploaders.Universalis;
|
||||
using Dalamud.Game.Network.Structures;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using Dalamud.Hooking;
|
||||
using Dalamud.Networking.Http;
|
||||
using Dalamud.Utility;
|
||||
|
|
@ -24,8 +25,8 @@ namespace Dalamud.Game.Network.Internal;
|
|||
/// <summary>
|
||||
/// This class handles network notifications and uploading market board data.
|
||||
/// </summary>
|
||||
[ServiceManager.BlockingEarlyLoadedService]
|
||||
internal unsafe class NetworkHandlers : IDisposable, IServiceType
|
||||
[ServiceManager.EarlyLoadedService]
|
||||
internal unsafe class NetworkHandlers : IInternalDisposableService
|
||||
{
|
||||
private readonly IMarketBoardUploader uploader;
|
||||
|
||||
|
|
@ -212,7 +213,7 @@ internal unsafe class NetworkHandlers : IDisposable, IServiceType
|
|||
/// <summary>
|
||||
/// Disposes of managed and unmanaged resources.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
void IInternalDisposableService.DisposeService()
|
||||
{
|
||||
this.disposing = true;
|
||||
this.Dispose(this.disposing);
|
||||
|
|
@ -268,8 +269,8 @@ internal unsafe class NetworkHandlers : IDisposable, IServiceType
|
|||
return result;
|
||||
}
|
||||
|
||||
var cfcName = cfCondition.Name.ToString();
|
||||
if (cfcName.IsNullOrEmpty())
|
||||
var cfcName = cfCondition.Name.ToDalamudString();
|
||||
if (cfcName.Payloads.Count == 0)
|
||||
{
|
||||
cfcName = "Duty Roulette";
|
||||
cfCondition.Image = 112324;
|
||||
|
|
@ -279,7 +280,10 @@ internal unsafe class NetworkHandlers : IDisposable, IServiceType
|
|||
{
|
||||
if (this.configuration.DutyFinderChatMessage)
|
||||
{
|
||||
Service<ChatGui>.GetNullable()?.Print($"Duty pop: {cfcName}");
|
||||
var b = new SeStringBuilder();
|
||||
b.Append("Duty pop: ");
|
||||
b.Append(cfcName);
|
||||
Service<ChatGui>.GetNullable()?.Print(b.Build());
|
||||
}
|
||||
|
||||
this.CfPop.InvokeSafely(cfCondition);
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ namespace Dalamud.Game.Network.Internal;
|
|||
/// This class enables TCP optimizations in the game socket for better performance.
|
||||
/// </summary>
|
||||
[ServiceManager.EarlyLoadedService]
|
||||
internal sealed class WinSockHandlers : IDisposable, IServiceType
|
||||
internal sealed class WinSockHandlers : IInternalDisposableService
|
||||
{
|
||||
private Hook<SocketDelegate> ws2SocketHook;
|
||||
|
||||
|
|
@ -27,7 +27,7 @@ internal sealed class WinSockHandlers : IDisposable, IServiceType
|
|||
/// <summary>
|
||||
/// Disposes of managed and unmanaged resources.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
void IInternalDisposableService.DisposeService()
|
||||
{
|
||||
this.ws2SocketHook?.Dispose();
|
||||
}
|
||||
|
|
|
|||
197
Dalamud/Game/Network/Structures/InfoProxy/CharacterData.cs
Normal file
197
Dalamud/Game/Network/Structures/InfoProxy/CharacterData.cs
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
using Dalamud.Game.ClientState.Resolvers;
|
||||
using Dalamud.Memory;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Info;
|
||||
|
||||
using Lumina.Excel.GeneratedSheets;
|
||||
|
||||
namespace Dalamud.Game.Network.Structures.InfoProxy;
|
||||
|
||||
/// <summary>
|
||||
/// Dalamud wrapper around a client structs <see cref="InfoProxyCommonList.CharacterData"/>.
|
||||
/// </summary>
|
||||
public unsafe class CharacterData
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CharacterData"/> class.
|
||||
/// </summary>
|
||||
/// <param name="data">Character data to wrap.</param>
|
||||
internal CharacterData(InfoProxyCommonList.CharacterData* data)
|
||||
{
|
||||
this.Address = (nint)data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the address of the <see cref="InfoProxyCommonList.CharacterData"/> in memory.
|
||||
/// </summary>
|
||||
public nint Address { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the content id of the character.
|
||||
/// </summary>
|
||||
public ulong ContentId => this.Struct->ContentId;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the status mask of the character.
|
||||
/// </summary>
|
||||
public ulong StatusMask => (ulong)this.Struct->State;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the applicable statues of the character.
|
||||
/// </summary>
|
||||
public IReadOnlyList<ExcelResolver<OnlineStatus>> Statuses
|
||||
{
|
||||
get
|
||||
{
|
||||
var statuses = new List<ExcelResolver<OnlineStatus>>();
|
||||
for (var i = 0; i < 64; i++)
|
||||
{
|
||||
if ((this.StatusMask & (1UL << i)) != 0)
|
||||
statuses.Add(new((uint)i));
|
||||
}
|
||||
|
||||
return statuses;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the display group of the character.
|
||||
/// </summary>
|
||||
public DisplayGroup DisplayGroup => (DisplayGroup)this.Struct->Group;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the character's home world is different from the current world.
|
||||
/// </summary>
|
||||
public bool IsFromOtherServer => this.Struct->IsOtherServer;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the sort order of the character.
|
||||
/// </summary>
|
||||
public byte Sort => this.Struct->Sort;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current world of the character.
|
||||
/// </summary>
|
||||
public ExcelResolver<World> CurrentWorld => new(this.Struct->CurrentWorld);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the home world of the character.
|
||||
/// </summary>
|
||||
public ExcelResolver<World> HomeWorld => new(this.Struct->HomeWorld);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the location of the character.
|
||||
/// </summary>
|
||||
public ExcelResolver<TerritoryType> Location => new(this.Struct->Location);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the grand company of the character.
|
||||
/// </summary>
|
||||
public ExcelResolver<GrandCompany> GrandCompany => new((uint)this.Struct->GrandCompany);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the primary client language of the character.
|
||||
/// </summary>
|
||||
public ClientLanguage ClientLanguage => (ClientLanguage)this.Struct->ClientLanguage;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the supported language mask of the character.
|
||||
/// </summary>
|
||||
public byte LanguageMask => (byte)this.Struct->Languages;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the supported languages the character supports.
|
||||
/// </summary>
|
||||
public IReadOnlyList<ClientLanguage> Languages
|
||||
{
|
||||
get
|
||||
{
|
||||
var languages = new List<ClientLanguage>();
|
||||
for (var i = 0; i < 4; i++)
|
||||
{
|
||||
if ((this.LanguageMask & (1 << i)) != 0)
|
||||
languages.Add((ClientLanguage)i);
|
||||
}
|
||||
|
||||
return languages;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the gender of the character.
|
||||
/// </summary>
|
||||
public byte Gender => this.Struct->Sex;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the job of the character.
|
||||
/// </summary>
|
||||
public ExcelResolver<ClassJob> ClassJob => new(this.Struct->Job);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the character.
|
||||
/// </summary>
|
||||
public string Name => MemoryHelper.ReadString((nint)this.Struct->Name, 32);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the free company tag of the character.
|
||||
/// </summary>
|
||||
public string FCTag => MemoryHelper.ReadString((nint)this.Struct->Name, 6);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the underlying <see cref="InfoProxyCommonList.CharacterData"/> struct.
|
||||
/// </summary>
|
||||
internal InfoProxyCommonList.CharacterData* Struct => (InfoProxyCommonList.CharacterData*)this.Address;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Display group of a character. Used for friends.
|
||||
/// </summary>
|
||||
public enum DisplayGroup : sbyte
|
||||
{
|
||||
/// <summary>
|
||||
/// All display groups.
|
||||
/// </summary>
|
||||
All = -1,
|
||||
|
||||
/// <summary>
|
||||
/// No display group.
|
||||
/// </summary>
|
||||
None,
|
||||
|
||||
/// <summary>
|
||||
/// Star display group.
|
||||
/// </summary>
|
||||
Star,
|
||||
|
||||
/// <summary>
|
||||
/// Circle display group.
|
||||
/// </summary>
|
||||
Circle,
|
||||
|
||||
/// <summary>
|
||||
/// Triangle display group.
|
||||
/// </summary>
|
||||
Triangle,
|
||||
|
||||
/// <summary>
|
||||
/// Diamond display group.
|
||||
/// </summary>
|
||||
Diamond,
|
||||
|
||||
/// <summary>
|
||||
/// Heart display group.
|
||||
/// </summary>
|
||||
Heart,
|
||||
|
||||
/// <summary>
|
||||
/// Spade display group.
|
||||
/// </summary>
|
||||
Spade,
|
||||
|
||||
/// <summary>
|
||||
/// Club display group.
|
||||
/// </summary>
|
||||
Club,
|
||||
}
|
||||
|
|
@ -104,6 +104,10 @@ public class SigScanner : IDisposable, ISigScanner
|
|||
/// <inheritdoc/>
|
||||
public ProcessModule Module { get; }
|
||||
|
||||
/// <summary>Gets or sets a value indicating whether this instance of <see cref="SigScanner"/> is meant to be a
|
||||
/// Dalamud service.</summary>
|
||||
private protected bool IsService { get; set; }
|
||||
|
||||
private IntPtr TextSectionTop => this.TextSectionBase + this.TextSectionSize;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -309,13 +313,11 @@ public class SigScanner : IDisposable, ISigScanner
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Free the memory of the copied module search area on object disposal, if applicable.
|
||||
/// </summary>
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
this.Save();
|
||||
Marshal.FreeHGlobal(this.moduleCopyPtr);
|
||||
if (!this.IsService)
|
||||
this.DisposeCore();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -337,6 +339,15 @@ public class SigScanner : IDisposable, ISigScanner
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Free the memory of the copied module search area on object disposal, if applicable.
|
||||
/// </summary>
|
||||
private protected void DisposeCore()
|
||||
{
|
||||
this.Save();
|
||||
Marshal.FreeHGlobal(this.moduleCopyPtr);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper for ScanText to get the correct address for IDA sigs that mark the first JMP or CALL location.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ namespace Dalamud.Game;
|
|||
#pragma warning disable SA1015
|
||||
[ResolveVia<ISigScanner>]
|
||||
#pragma warning restore SA1015
|
||||
internal class TargetSigScanner : SigScanner, IServiceType
|
||||
internal class TargetSigScanner : SigScanner, IPublicDisposableService
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TargetSigScanner"/> class.
|
||||
|
|
@ -26,4 +26,14 @@ internal class TargetSigScanner : SigScanner, IServiceType
|
|||
: base(Process.GetCurrentProcess().MainModule!, doCopy, cacheFile)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
void IInternalDisposableService.DisposeService()
|
||||
{
|
||||
if (this.IsService)
|
||||
this.DisposeCore();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
void IPublicDisposableService.MarkDisposeOnlyFromService() => this.IsService = true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -381,7 +381,7 @@ public class SeString
|
|||
{
|
||||
new PartyFinderPayload(listingId, isCrossWorld ? PartyFinderPayload.PartyFinderLinkType.NotSpecified : PartyFinderPayload.PartyFinderLinkType.LimitedToHomeWorld),
|
||||
// ->
|
||||
new TextPayload($"Looking for Party ({recruiterName})"),
|
||||
new TextPayload($"Looking for Party ({recruiterName})" + (isCrossWorld ? " " : string.Empty)),
|
||||
};
|
||||
|
||||
payloads.InsertRange(1, TextArrowPayloads);
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ namespace Dalamud.Hooking.Internal;
|
|||
#pragma warning disable SA1015
|
||||
[ResolveVia<IGameInteropProvider>]
|
||||
#pragma warning restore SA1015
|
||||
internal class GameInteropProviderPluginScoped : IGameInteropProvider, IServiceType, IDisposable
|
||||
internal class GameInteropProviderPluginScoped : IGameInteropProvider, IInternalDisposableService
|
||||
{
|
||||
private readonly LocalPlugin plugin;
|
||||
private readonly SigScanner scanner;
|
||||
|
|
@ -83,7 +83,7 @@ internal class GameInteropProviderPluginScoped : IGameInteropProvider, IServiceT
|
|||
=> this.HookFromAddress(this.scanner.ScanText(signature), detour, backend);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
void IInternalDisposableService.DisposeService()
|
||||
{
|
||||
var notDisposed = this.trackedHooks.Where(x => !x.IsDisposed).ToArray();
|
||||
if (notDisposed.Length != 0)
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ namespace Dalamud.Hooking.Internal;
|
|||
/// This class manages the final disposition of hooks, cleaning up any that have not reverted their changes.
|
||||
/// </summary>
|
||||
[ServiceManager.EarlyLoadedService]
|
||||
internal class HookManager : IDisposable, IServiceType
|
||||
internal class HookManager : IInternalDisposableService
|
||||
{
|
||||
/// <summary>
|
||||
/// Logger shared with <see cref="Unhooker"/>.
|
||||
|
|
@ -74,7 +74,7 @@ internal class HookManager : IDisposable, IServiceType
|
|||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
void IInternalDisposableService.DisposeService()
|
||||
{
|
||||
RevertHooks();
|
||||
TrackedHooks.Clear();
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@ namespace Dalamud.Hooking.WndProcHook;
|
|||
/// <summary>
|
||||
/// Manages WndProc hooks for game main window and extra ImGui viewport windows.
|
||||
/// </summary>
|
||||
[ServiceManager.BlockingEarlyLoadedService]
|
||||
internal sealed class WndProcHookManager : IServiceType, IDisposable
|
||||
[ServiceManager.EarlyLoadedService]
|
||||
internal sealed class WndProcHookManager : IInternalDisposableService
|
||||
{
|
||||
private static readonly ModuleLog Log = new(nameof(WndProcHookManager));
|
||||
|
||||
|
|
@ -56,7 +56,7 @@ internal sealed class WndProcHookManager : IServiceType, IDisposable
|
|||
public event WndProcEventDelegate? PostWndProc;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
void IInternalDisposableService.DisposeService()
|
||||
{
|
||||
if (this.dispatchMessageWHook.IsDisposed)
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -6,3 +6,20 @@
|
|||
public interface IServiceType
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary><see cref="IDisposable"/>, but for <see cref="IServiceType"/>.</summary>
|
||||
/// <remarks>Use this to prevent services from accidentally being disposed by plugins or <c>using</c> clauses.</remarks>
|
||||
internal interface IInternalDisposableService : IServiceType
|
||||
{
|
||||
/// <summary>Disposes the service.</summary>
|
||||
void DisposeService();
|
||||
}
|
||||
|
||||
/// <summary>An <see cref="IInternalDisposableService"/> which happens to be public and needs to expose
|
||||
/// <see cref="IDisposable.Dispose"/>.</summary>
|
||||
internal interface IPublicDisposableService : IInternalDisposableService, IDisposable
|
||||
{
|
||||
/// <summary>Marks that only <see cref="IInternalDisposableService.DisposeService"/> should respond,
|
||||
/// while suppressing <see cref="IDisposable.Dispose"/>.</summary>
|
||||
void MarkDisposeOnlyFromService();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,11 +11,18 @@ public static partial class ImGuiComponents
|
|||
/// HelpMarker component to add a help icon with text on hover.
|
||||
/// </summary>
|
||||
/// <param name="helpText">The text to display on hover.</param>
|
||||
public static void HelpMarker(string helpText)
|
||||
public static void HelpMarker(string helpText) => HelpMarker(helpText, FontAwesomeIcon.InfoCircle);
|
||||
|
||||
/// <summary>
|
||||
/// HelpMarker component to add a custom icon with text on hover.
|
||||
/// </summary>
|
||||
/// <param name="helpText">The text to display on hover.</param>
|
||||
/// <param name="icon">The icon to use.</param>
|
||||
public static void HelpMarker(string helpText, FontAwesomeIcon icon)
|
||||
{
|
||||
ImGui.SameLine();
|
||||
ImGui.PushFont(UiBuilder.IconFont);
|
||||
ImGui.TextDisabled(FontAwesomeIcon.InfoCircle.ToIconString());
|
||||
ImGui.TextDisabled(icon.ToIconString());
|
||||
ImGui.PopFont();
|
||||
if (!ImGui.IsItemHovered()) return;
|
||||
ImGui.BeginTooltip();
|
||||
|
|
|
|||
|
|
@ -15,11 +15,11 @@ namespace Dalamud.Interface.DragDrop;
|
|||
/// and can be used to create ImGui drag and drop sources and targets for those external events.
|
||||
/// </summary>
|
||||
[PluginInterface]
|
||||
[ServiceManager.BlockingEarlyLoadedService]
|
||||
[ServiceManager.EarlyLoadedService]
|
||||
#pragma warning disable SA1015
|
||||
[ResolveVia<IDragDropManager>]
|
||||
#pragma warning restore SA1015
|
||||
internal partial class DragDropManager : IDisposable, IDragDropManager, IServiceType
|
||||
internal partial class DragDropManager : IInternalDisposableService, IDragDropManager
|
||||
{
|
||||
private nint windowHandlePtr = nint.Zero;
|
||||
|
||||
|
|
@ -56,6 +56,9 @@ internal partial class DragDropManager : IDisposable, IDragDropManager, IService
|
|||
/// <summary> Gets the list of directory paths currently being dragged from an external application over any FFXIV-related viewport or stored from the last drop. </summary>
|
||||
public IReadOnlyList<string> Directories { get; private set; } = Array.Empty<string>();
|
||||
|
||||
/// <inheritdoc/>
|
||||
void IInternalDisposableService.DisposeService() => this.Disable();
|
||||
|
||||
/// <summary> Enable external drag and drop. </summary>
|
||||
public void Enable()
|
||||
{
|
||||
|
|
@ -99,10 +102,6 @@ internal partial class DragDropManager : IDisposable, IDragDropManager, IService
|
|||
this.ServiceAvailable = false;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Disable"/>
|
||||
public void Dispose()
|
||||
=> this.Disable();
|
||||
|
||||
/// <inheritdoc cref="IDragDropManager.CreateImGuiSource(string, Func{IDragDropManager, bool}, Func{IDragDropManager, bool})"/>
|
||||
public void CreateImGuiSource(string label, Func<IDragDropManager, bool> validityCheck, Func<IDragDropManager, bool> tooltipBuilder)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,143 +0,0 @@
|
|||
using System.Numerics;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Dalamud.Interface.ManagedFontAtlas;
|
||||
using Dalamud.Interface.ManagedFontAtlas.Internals;
|
||||
using Dalamud.Utility;
|
||||
|
||||
using ImGuiNET;
|
||||
|
||||
namespace Dalamud.Interface.GameFonts;
|
||||
|
||||
/// <summary>
|
||||
/// ABI-compatible wrapper for <see cref="IFontHandle"/>.
|
||||
/// </summary>
|
||||
[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)]
|
||||
public sealed class GameFontHandle : IFontHandle
|
||||
{
|
||||
private readonly GamePrebakedFontHandle fontHandle;
|
||||
private readonly FontAtlasFactory fontAtlasFactory;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GameFontHandle"/> class.<br />
|
||||
/// Ownership of <paramref name="fontHandle"/> is transferred.
|
||||
/// </summary>
|
||||
/// <param name="fontHandle">The wrapped <see cref="GamePrebakedFontHandle"/>.</param>
|
||||
/// <param name="fontAtlasFactory">An instance of <see cref="FontAtlasFactory"/>.</param>
|
||||
internal GameFontHandle(GamePrebakedFontHandle fontHandle, FontAtlasFactory fontAtlasFactory)
|
||||
{
|
||||
this.fontHandle = fontHandle;
|
||||
this.fontAtlasFactory = fontAtlasFactory;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public event IFontHandle.ImFontChangedDelegate ImFontChanged
|
||||
{
|
||||
add => this.fontHandle.ImFontChanged += value;
|
||||
remove => this.fontHandle.ImFontChanged -= value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Exception? LoadException => this.fontHandle.LoadException;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Available => this.fontHandle.Available;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the font.<br />
|
||||
/// Use of this properly is safe only from the UI thread.<br />
|
||||
/// Use <see cref="IFontHandle.Push"/> if the intended purpose of this property is <see cref="ImGui.PushFont"/>.<br />
|
||||
/// Futures changes may make simple <see cref="ImGui.PushFont"/> not enough.<br />
|
||||
/// If you need to access a font outside the UI thread, use <see cref="IFontHandle.Lock"/>.
|
||||
/// </summary>
|
||||
[Obsolete($"Use {nameof(Push)}-{nameof(ImGui.GetFont)} or {nameof(Lock)} instead.", false)]
|
||||
public ImFontPtr ImFont => this.fontHandle.LockUntilPostFrame();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the font style. Only applicable for <see cref="GameFontHandle"/>.
|
||||
/// </summary>
|
||||
[Obsolete("If you use this, let the fact that you use this be known at Dalamud Discord.", false)]
|
||||
public GameFontStyle Style => ((GamePrebakedFontHandle)this.fontHandle).FontStyle;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the relevant <see cref="FdtReader"/>.<br />
|
||||
/// <br />
|
||||
/// Only applicable for game fonts. Otherwise it will throw.
|
||||
/// </summary>
|
||||
[Obsolete("If you use this, let the fact that you use this be known at Dalamud Discord.", false)]
|
||||
public FdtReader FdtReader => this.fontAtlasFactory.GetFdtReader(this.Style.FamilyAndSize)!;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose() => this.fontHandle.Dispose();
|
||||
|
||||
/// <inheritdoc />
|
||||
public ILockedImFont Lock() => this.fontHandle.Lock();
|
||||
|
||||
/// <inheritdoc />
|
||||
public IDisposable Push() => this.fontHandle.Push();
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Pop() => this.fontHandle.Pop();
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IFontHandle> WaitAsync() => this.fontHandle.WaitAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="GameFontLayoutPlan.Builder"/>.<br />
|
||||
/// <br />
|
||||
/// Only applicable for game fonts. Otherwise it will throw.
|
||||
/// </summary>
|
||||
/// <param name="text">Text.</param>
|
||||
/// <returns>A new builder for GameFontLayoutPlan.</returns>
|
||||
[Obsolete("If you use this, let the fact that you use this be known at Dalamud Discord.", false)]
|
||||
public GameFontLayoutPlan.Builder LayoutBuilder(string text) => new(this.ImFont, this.FdtReader, text);
|
||||
|
||||
/// <summary>
|
||||
/// Draws text.
|
||||
/// </summary>
|
||||
/// <param name="text">Text to draw.</param>
|
||||
[Obsolete("If you use this, let the fact that you use this be known at Dalamud Discord.", false)]
|
||||
public void Text(string text)
|
||||
{
|
||||
if (!this.Available)
|
||||
{
|
||||
ImGui.TextUnformatted(text);
|
||||
}
|
||||
else
|
||||
{
|
||||
var pos = ImGui.GetWindowPos() + ImGui.GetCursorPos();
|
||||
pos.X -= ImGui.GetScrollX();
|
||||
pos.Y -= ImGui.GetScrollY();
|
||||
|
||||
var layout = this.LayoutBuilder(text).Build();
|
||||
layout.Draw(ImGui.GetWindowDrawList(), pos, ImGui.GetColorU32(ImGuiCol.Text));
|
||||
ImGui.Dummy(new Vector2(layout.Width, layout.Height));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws text in given color.
|
||||
/// </summary>
|
||||
/// <param name="col">Color.</param>
|
||||
/// <param name="text">Text to draw.</param>
|
||||
[Obsolete("If you use this, let the fact that you use this be known at Dalamud Discord.", false)]
|
||||
public void TextColored(Vector4 col, string text)
|
||||
{
|
||||
ImGui.PushStyleColor(ImGuiCol.Text, col);
|
||||
this.Text(text);
|
||||
ImGui.PopStyleColor();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws disabled text.
|
||||
/// </summary>
|
||||
/// <param name="text">Text to draw.</param>
|
||||
[Obsolete("If you use this, let the fact that you use this be known at Dalamud Discord.", false)]
|
||||
public void TextDisabled(string text)
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
this.TextColored(*ImGui.GetStyleColorVec4(ImGuiCol.TextDisabled), text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ using Dalamud.Configuration.Internal;
|
|||
using Dalamud.Interface.Colors;
|
||||
using Dalamud.Interface.FontIdentifier;
|
||||
using Dalamud.Interface.ManagedFontAtlas;
|
||||
using Dalamud.Interface.ManagedFontAtlas.Internals;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Utility;
|
||||
|
||||
|
|
@ -84,12 +85,45 @@ public sealed class SingleFontChooserDialog : IDisposable
|
|||
private IFontHandle? fontHandle;
|
||||
private SingleFontSpec selectedFont;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SingleFontChooserDialog"/> class.
|
||||
/// </summary>
|
||||
private bool popupPositionChanged;
|
||||
private bool popupSizeChanged;
|
||||
private Vector2 popupPosition = new(float.NaN);
|
||||
private Vector2 popupSize = new(float.NaN);
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="SingleFontChooserDialog"/> class.</summary>
|
||||
/// <param name="uiBuilder">The relevant instance of UiBuilder.</param>
|
||||
/// <param name="isGlobalScaled">Whether the fonts in the atlas is global scaled.</param>
|
||||
/// <param name="debugAtlasName">Atlas name for debugging purposes.</param>
|
||||
/// <remarks>
|
||||
/// <para>The passed <see cref="UiBuilder"/> is only used for creating a temporary font atlas. It will not
|
||||
/// automatically register a hander for <see cref="UiBuilder.Draw"/>.</para>
|
||||
/// <para>Consider using <see cref="CreateAuto"/> for automatic registration and unregistration of
|
||||
/// <see cref="Draw"/> event handler in addition to automatic disposal of this class and the temporary font atlas
|
||||
/// for this font chooser dialog.</para>
|
||||
/// </remarks>
|
||||
public SingleFontChooserDialog(UiBuilder uiBuilder, bool isGlobalScaled = true, string? debugAtlasName = null)
|
||||
: this(uiBuilder.CreateFontAtlas(FontAtlasAutoRebuildMode.Async, isGlobalScaled, debugAtlasName))
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="SingleFontChooserDialog"/> class.</summary>
|
||||
/// <param name="factory">An instance of <see cref="FontAtlasFactory"/>.</param>
|
||||
/// <param name="debugAtlasName">The temporary atlas name.</param>
|
||||
internal SingleFontChooserDialog(FontAtlasFactory factory, string debugAtlasName)
|
||||
: this(factory.CreateFontAtlas(debugAtlasName, FontAtlasAutoRebuildMode.Async))
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="SingleFontChooserDialog"/> class.</summary>
|
||||
/// <param name="newAsyncAtlas">A new instance of <see cref="IFontAtlas"/> created using
|
||||
/// <see cref="FontAtlasAutoRebuildMode.Async"/> as its auto-rebuild mode.</param>
|
||||
public SingleFontChooserDialog(IFontAtlas newAsyncAtlas)
|
||||
/// <remarks>The passed instance of <paramref see="newAsyncAtlas"/> will be disposed after use. If you pass an atlas
|
||||
/// that is already being used, then all the font handles under the passed atlas will be invalidated upon disposing
|
||||
/// this font chooser. Consider using <see cref="SingleFontChooserDialog(UiBuilder, bool, string?)"/> for automatic
|
||||
/// handling of font atlas derived from a <see cref="UiBuilder"/>, or even <see cref="CreateAuto"/> for automatic
|
||||
/// registration and unregistration of <see cref="Draw"/> event handler in addition to automatic disposal of this
|
||||
/// class and the temporary font atlas for this font chooser dialog.</remarks>
|
||||
private SingleFontChooserDialog(IFontAtlas newAsyncAtlas)
|
||||
{
|
||||
this.counter = Interlocked.Increment(ref counterStatic);
|
||||
this.title = "Choose a font...";
|
||||
|
|
@ -99,6 +133,9 @@ public sealed class SingleFontChooserDialog : IDisposable
|
|||
Encoding.UTF8.GetBytes("Font preview.\n0123456789!", this.fontPreviewText);
|
||||
}
|
||||
|
||||
/// <summary>Called when the selected font spec has changed.</summary>
|
||||
public event Action<SingleFontSpec>? SelectedFontSpecChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the title of this font chooser dialog popup.
|
||||
/// </summary>
|
||||
|
|
@ -153,6 +190,8 @@ public sealed class SingleFontChooserDialog : IDisposable
|
|||
this.useAdvancedOptions |= Math.Abs(value.LineHeight - 1f) > 0.000001;
|
||||
this.useAdvancedOptions |= value.GlyphOffset != default;
|
||||
this.useAdvancedOptions |= value.LetterSpacing != 0f;
|
||||
|
||||
this.SelectedFontSpecChanged?.Invoke(this.selectedFont);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -166,15 +205,55 @@ public sealed class SingleFontChooserDialog : IDisposable
|
|||
/// </summary>
|
||||
public bool IgnorePreviewGlobalScale { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of <see cref="SingleFontChooserDialog"/> that will automatically draw and dispose itself as
|
||||
/// needed.
|
||||
/// <summary>Gets or sets a value indicating whether this popup should be modal, blocking everything behind from
|
||||
/// being interacted.</summary>
|
||||
/// <remarks>If <c>true</c>, then <see cref="ImGui.BeginPopupModal(string, ref bool, ImGuiWindowFlags)"/> will be
|
||||
/// used. Otherwise, <see cref="ImGui.Begin(string, ref bool, ImGuiWindowFlags)"/> will be used.</remarks>
|
||||
public bool IsModal { get; set; } = true;
|
||||
|
||||
/// <summary>Gets or sets the window flags.</summary>
|
||||
public ImGuiWindowFlags WindowFlags { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the popup window position.</summary>
|
||||
/// <remarks>
|
||||
/// <para>Setting the position only works before the first call to <see cref="Draw"/>.</para>
|
||||
/// <para>If any of the coordinates are <see cref="float.NaN"/>, default position will be used.</para>
|
||||
/// <para>The position will be clamped into the work area of the selected monitor.</para>
|
||||
/// </remarks>
|
||||
public Vector2 PopupPosition
|
||||
{
|
||||
get => this.popupPosition;
|
||||
set
|
||||
{
|
||||
this.popupPositionChanged = true;
|
||||
this.popupPosition = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Gets or sets the popup window size.</summary>
|
||||
/// <remarks>
|
||||
/// <para>Setting the size only works before the first call to <see cref="Draw"/>.</para>
|
||||
/// <para>If any of the coordinates are <see cref="float.NaN"/>, default size will be used.</para>
|
||||
/// <para>The size will be clamped into the work area of the selected monitor.</para>
|
||||
/// </remarks>
|
||||
public Vector2 PopupSize
|
||||
{
|
||||
get => this.popupSize;
|
||||
set
|
||||
{
|
||||
this.popupSizeChanged = true;
|
||||
this.popupSize = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Creates a new instance of <see cref="SingleFontChooserDialog"/> that will automatically draw and
|
||||
/// dispose itself as needed; calling <see cref="Draw"/> and <see cref="Dispose"/> are handled automatically.
|
||||
/// </summary>
|
||||
/// <param name="uiBuilder">An instance of <see cref="UiBuilder"/>.</param>
|
||||
/// <returns>The new instance of <see cref="SingleFontChooserDialog"/>.</returns>
|
||||
public static SingleFontChooserDialog CreateAuto(UiBuilder uiBuilder)
|
||||
{
|
||||
var fcd = new SingleFontChooserDialog(uiBuilder.CreateFontAtlas(FontAtlasAutoRebuildMode.Async));
|
||||
var fcd = new SingleFontChooserDialog(uiBuilder);
|
||||
uiBuilder.Draw += fcd.Draw;
|
||||
fcd.tcs.Task.ContinueWith(
|
||||
r =>
|
||||
|
|
@ -187,6 +266,14 @@ public sealed class SingleFontChooserDialog : IDisposable
|
|||
return fcd;
|
||||
}
|
||||
|
||||
/// <summary>Gets the default popup size before clamping to monitor work area.</summary>
|
||||
/// <returns>The default popup size.</returns>
|
||||
public static Vector2 GetDefaultPopupSizeNonClamped()
|
||||
{
|
||||
ThreadSafety.AssertMainThread();
|
||||
return new Vector2(40, 30) * ImGui.GetTextLineHeight();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
|
|
@ -204,13 +291,28 @@ public sealed class SingleFontChooserDialog : IDisposable
|
|||
ImGui.GetIO().WantTextInput = false;
|
||||
}
|
||||
|
||||
/// <summary>Sets <see cref="PopupSize"/> and <see cref="PopupPosition"/> to be at the center of the current window
|
||||
/// being drawn.</summary>
|
||||
/// <param name="preferredPopupSize">The preferred popup size.</param>
|
||||
public void SetPopupPositionAndSizeToCurrentWindowCenter(Vector2 preferredPopupSize)
|
||||
{
|
||||
ThreadSafety.AssertMainThread();
|
||||
this.PopupSize = preferredPopupSize;
|
||||
this.PopupPosition = ImGui.GetWindowPos() + ((ImGui.GetWindowSize() - preferredPopupSize) / 2);
|
||||
}
|
||||
|
||||
/// <summary>Sets <see cref="PopupSize"/> and <see cref="PopupPosition"/> to be at the center of the current window
|
||||
/// being drawn.</summary>
|
||||
public void SetPopupPositionAndSizeToCurrentWindowCenter() =>
|
||||
this.SetPopupPositionAndSizeToCurrentWindowCenter(GetDefaultPopupSizeNonClamped());
|
||||
|
||||
/// <summary>
|
||||
/// Draws this dialog.
|
||||
/// </summary>
|
||||
public void Draw()
|
||||
{
|
||||
if (this.firstDraw)
|
||||
ImGui.OpenPopup(this.popupImGuiName);
|
||||
const float popupMinWidth = 320;
|
||||
const float popupMinHeight = 240;
|
||||
|
||||
ImGui.GetIO().WantCaptureKeyboard = true;
|
||||
ImGui.GetIO().WantTextInput = true;
|
||||
|
|
@ -220,12 +322,70 @@ public sealed class SingleFontChooserDialog : IDisposable
|
|||
return;
|
||||
}
|
||||
|
||||
var open = true;
|
||||
ImGui.SetNextWindowSize(new(640, 480), ImGuiCond.Appearing);
|
||||
if (!ImGui.BeginPopupModal(this.popupImGuiName, ref open) || !open)
|
||||
if (this.firstDraw)
|
||||
{
|
||||
this.Cancel();
|
||||
return;
|
||||
if (this.IsModal)
|
||||
ImGui.OpenPopup(this.popupImGuiName);
|
||||
}
|
||||
|
||||
if (this.firstDraw || this.popupPositionChanged || this.popupSizeChanged)
|
||||
{
|
||||
var preferProvidedSize = !float.IsNaN(this.popupSize.X) && !float.IsNaN(this.popupSize.Y);
|
||||
var size = preferProvidedSize ? this.popupSize : GetDefaultPopupSizeNonClamped();
|
||||
size.X = Math.Max(size.X, popupMinWidth);
|
||||
size.Y = Math.Max(size.Y, popupMinHeight);
|
||||
|
||||
var preferProvidedPos = !float.IsNaN(this.popupPosition.X) && !float.IsNaN(this.popupPosition.Y);
|
||||
var monitorLocatorPos = preferProvidedPos ? this.popupPosition + (size / 2) : ImGui.GetMousePos();
|
||||
|
||||
var monitors = ImGui.GetPlatformIO().Monitors;
|
||||
var preferredMonitor = 0;
|
||||
var preferredDistance = GetDistanceFromMonitor(monitorLocatorPos, monitors[0]);
|
||||
for (var i = 1; i < monitors.Size; i++)
|
||||
{
|
||||
var distance = GetDistanceFromMonitor(monitorLocatorPos, monitors[i]);
|
||||
if (distance < preferredDistance)
|
||||
{
|
||||
preferredMonitor = i;
|
||||
preferredDistance = distance;
|
||||
}
|
||||
}
|
||||
|
||||
var lt = monitors[preferredMonitor].WorkPos;
|
||||
var workSize = monitors[preferredMonitor].WorkSize;
|
||||
size.X = Math.Min(size.X, workSize.X);
|
||||
size.Y = Math.Min(size.Y, workSize.Y);
|
||||
var rb = (lt + workSize) - size;
|
||||
|
||||
var pos =
|
||||
preferProvidedPos
|
||||
? new(Math.Clamp(this.PopupPosition.X, lt.X, rb.X), Math.Clamp(this.PopupPosition.Y, lt.Y, rb.Y))
|
||||
: (lt + rb) / 2;
|
||||
|
||||
ImGui.SetNextWindowSize(size, ImGuiCond.Always);
|
||||
ImGui.SetNextWindowPos(pos, ImGuiCond.Always);
|
||||
this.popupPositionChanged = this.popupSizeChanged = false;
|
||||
}
|
||||
|
||||
ImGui.SetNextWindowSizeConstraints(new(popupMinWidth, popupMinHeight), new(float.MaxValue));
|
||||
if (this.IsModal)
|
||||
{
|
||||
var open = true;
|
||||
if (!ImGui.BeginPopupModal(this.popupImGuiName, ref open, this.WindowFlags) || !open)
|
||||
{
|
||||
this.Cancel();
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var open = true;
|
||||
if (!ImGui.Begin(this.popupImGuiName, ref open, this.WindowFlags) || !open)
|
||||
{
|
||||
ImGui.End();
|
||||
this.Cancel();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var framePad = ImGui.GetStyle().FramePadding;
|
||||
|
|
@ -261,12 +421,36 @@ public sealed class SingleFontChooserDialog : IDisposable
|
|||
|
||||
ImGui.EndChild();
|
||||
|
||||
ImGui.EndPopup();
|
||||
this.popupPosition = ImGui.GetWindowPos();
|
||||
this.popupSize = ImGui.GetWindowSize();
|
||||
if (this.IsModal)
|
||||
ImGui.EndPopup();
|
||||
else
|
||||
ImGui.End();
|
||||
|
||||
this.firstDraw = false;
|
||||
this.firstDrawAfterRefresh = false;
|
||||
}
|
||||
|
||||
private static float GetDistanceFromMonitor(Vector2 point, ImGuiPlatformMonitorPtr monitor)
|
||||
{
|
||||
var lt = monitor.MainPos;
|
||||
var rb = monitor.MainPos + monitor.MainSize;
|
||||
var xoff =
|
||||
point.X < lt.X
|
||||
? lt.X - point.X
|
||||
: point.X > rb.X
|
||||
? point.X - rb.X
|
||||
: 0;
|
||||
var yoff =
|
||||
point.Y < lt.Y
|
||||
? lt.Y - point.Y
|
||||
: point.Y > rb.Y
|
||||
? point.Y - rb.Y
|
||||
: 0;
|
||||
return MathF.Sqrt((xoff * xoff) + (yoff * yoff));
|
||||
}
|
||||
|
||||
private void DrawChoices()
|
||||
{
|
||||
var lineHeight = ImGui.GetTextLineHeight();
|
||||
|
|
@ -338,15 +522,20 @@ public sealed class SingleFontChooserDialog : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
if (this.IgnorePreviewGlobalScale)
|
||||
if (this.fontHandle is null)
|
||||
{
|
||||
this.fontHandle ??= this.selectedFont.CreateFontHandle(
|
||||
this.atlas,
|
||||
tk => tk.OnPreBuild(e => e.SetFontScaleMode(e.Font, FontScaleMode.UndoGlobalScale)));
|
||||
}
|
||||
else
|
||||
{
|
||||
this.fontHandle ??= this.selectedFont.CreateFontHandle(this.atlas);
|
||||
if (this.IgnorePreviewGlobalScale)
|
||||
{
|
||||
this.fontHandle = this.selectedFont.CreateFontHandle(
|
||||
this.atlas,
|
||||
tk => tk.OnPreBuild(e => e.SetFontScaleMode(e.Font, FontScaleMode.UndoGlobalScale)));
|
||||
}
|
||||
else
|
||||
{
|
||||
this.fontHandle = this.selectedFont.CreateFontHandle(this.atlas);
|
||||
}
|
||||
|
||||
this.SelectedFontSpecChanged?.InvokeSafely(this.selectedFont);
|
||||
}
|
||||
|
||||
if (this.fontHandle is null)
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue