diff --git a/.editorconfig b/.editorconfig
index d88c7ce7a..141e8c9c9 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -35,7 +35,7 @@ dotnet_naming_rule.private_instance_fields_rule.severity = warning
dotnet_naming_rule.private_instance_fields_rule.style = lower_camel_case_style
dotnet_naming_rule.private_instance_fields_rule.symbols = private_instance_fields_symbols
dotnet_naming_rule.private_static_fields_rule.severity = warning
-dotnet_naming_rule.private_static_fields_rule.style = upper_camel_case_style
+dotnet_naming_rule.private_static_fields_rule.style = lower_camel_case_style
dotnet_naming_rule.private_static_fields_rule.symbols = private_static_fields_symbols
dotnet_naming_rule.private_static_readonly_rule.severity = warning
dotnet_naming_rule.private_static_readonly_rule.style = upper_camel_case_style
@@ -57,6 +57,7 @@ dotnet_naming_symbols.private_static_fields_symbols.required_modifiers = static
dotnet_naming_symbols.private_static_readonly_symbols.applicable_accessibilities = private
dotnet_naming_symbols.private_static_readonly_symbols.applicable_kinds = field
dotnet_naming_symbols.private_static_readonly_symbols.required_modifiers = static,readonly
+dotnet_separate_import_directive_groups = true
dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:suggestion
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:suggestion
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:suggestion
@@ -97,22 +98,32 @@ resharper_apply_on_completion = true
resharper_auto_property_can_be_made_get_only_global_highlighting = none
resharper_auto_property_can_be_made_get_only_local_highlighting = none
resharper_autodetect_indent_settings = true
+resharper_blank_lines_around_single_line_auto_property = 1
resharper_braces_for_ifelse = required_for_multiline
resharper_can_use_global_alias = false
resharper_csharp_align_multiline_parameter = true
resharper_csharp_align_multiple_declaration = true
resharper_csharp_empty_block_style = multiline
-resharper_csharp_int_align_comments = true
+resharper_csharp_int_align_comments = false
resharper_csharp_new_line_before_while = true
resharper_csharp_wrap_after_declaration_lpar = true
+resharper_csharp_wrap_after_invocation_lpar = true
+resharper_csharp_wrap_arguments_style = chop_if_long
resharper_enforce_line_ending_style = true
+resharper_instance_members_qualify_declared_in = this_class, base_class
+resharper_int_align = false
resharper_member_can_be_private_global_highlighting = none
resharper_member_can_be_private_local_highlighting = none
-resharper_new_line_before_finally = false
+resharper_new_line_before_finally = true
+resharper_parentheses_non_obvious_operations = none, multiplicative, additive, arithmetic, shift, bitwise_and, bitwise_exclusive_or, bitwise_inclusive_or, bitwise
+resharper_parentheses_redundancy_style = remove_if_not_clarifies_precedence
resharper_place_accessorholder_attribute_on_same_line = false
resharper_place_field_attribute_on_same_line = false
+resharper_place_simple_initializer_on_single_line = true
resharper_show_autodetect_configure_formatting_tip = false
+resharper_space_within_single_line_array_initializer_braces = true
resharper_use_indent_from_vs = false
+resharper_wrap_array_initializer_style = chop_if_long
# ReSharper inspection severities
resharper_arrange_missing_parentheses_highlighting = hint
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 7ada48e50..8a4fdf2e3 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -42,7 +42,48 @@ jobs:
with:
name: dalamud-artifact
path: bin\Release
-
+
+ check_api_compat:
+ name: "Check API Compatibility"
+ if: ${{ github.event_name == 'pull_request' }}
+ needs: build
+ runs-on: windows-latest
+ steps:
+ - name: "Install .NET SDK"
+ uses: actions/setup-dotnet@v3
+ with:
+ dotnet-version: 7
+ - name: "Install ApiCompat"
+ run: |
+ dotnet tool install -g Microsoft.DotNet.ApiCompat.Tool
+ - name: "Download Proposed Artifacts"
+ uses: actions/download-artifact@v2
+ with:
+ name: dalamud-artifact
+ path: .\right
+ - name: "Download Live (Stg) Artifacts"
+ run: |
+ Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -OutFile latest.zip
+ Expand-Archive -Force latest.zip "left"
+ - name: "Verify Compatibility"
+ run: |
+ $FILES_TO_VALIDATE = "Dalamud.dll","FFXIVClientStructs.dll","Lumina.dll","Lumina.Excel.dll"
+
+ $retcode = 0
+
+ foreach ($file in $FILES_TO_VALIDATE) {
+ $testout = ""
+ Write-Output "::group::=== API COMPATIBILITY CHECK: ${file} ==="
+ apicompat -l "left\${file}" -r "right\${file}" | Tee-Object -Variable testout
+ Write-Output "::endgroup::"
+ if ($testout -ne "APICompat ran successfully without finding any breaking changes.") {
+ Write-Output "::error::${file} did not pass. Please review it for problems."
+ $retcode = 1
+ }
+ }
+
+ exit $retcode
+
deploy_stg:
name: Deploy dalamud-distrib staging
if: ${{ github.repository_owner == 'goatcorp' && github.event_name == 'push' }}
diff --git a/.github/workflows/rollup.yml b/.github/workflows/rollup.yml
index 25b558711..44116e7b2 100644
--- a/.github/workflows/rollup.yml
+++ b/.github/workflows/rollup.yml
@@ -11,7 +11,8 @@ jobs:
strategy:
matrix:
branches:
- - v9
+ - net8
+ #- new_im_hooks # Unmergeable
defaults:
run:
diff --git a/Dalamud.Boot/Dalamud.Boot.vcxproj b/Dalamud.Boot/Dalamud.Boot.vcxproj
index 07c034b8f..e75b31f19 100644
--- a/Dalamud.Boot/Dalamud.Boot.vcxproj
+++ b/Dalamud.Boot/Dalamud.Boot.vcxproj
@@ -32,6 +32,9 @@
obj\$(Configuration)\
+
+
+
true
$(SolutionDir)bin\lib\$(Configuration)\libMinHook\;$(VC_LibraryPath_x64);$(WindowsSDK_LibraryPath_x64)
@@ -56,7 +59,7 @@
Windows
true
false
- Version.lib;%(AdditionalDependencies)
+ Version.lib;Shlwapi.lib;%(AdditionalDependencies)
..\lib\CoreCLR;%(AdditionalLibraryDirectories)
@@ -72,6 +75,7 @@
false
false
+ module.def
@@ -85,9 +89,13 @@
true
true
+ module.def
+
+
+
nethost.dll
@@ -131,6 +139,7 @@
NotUsing
NotUsing
+
NotUsing
NotUsing
@@ -178,6 +187,7 @@
+
@@ -191,8 +201,14 @@
+
+
+
+
+
+
-
\ No newline at end of file
+
diff --git a/Dalamud.Boot/Dalamud.Boot.vcxproj.filters b/Dalamud.Boot/Dalamud.Boot.vcxproj.filters
index eeb4c8ab2..6a9d14a58 100644
--- a/Dalamud.Boot/Dalamud.Boot.vcxproj.filters
+++ b/Dalamud.Boot/Dalamud.Boot.vcxproj.filters
@@ -76,6 +76,9 @@
Dalamud.Boot DLL
+
+ Dalamud.Boot DLL
+
@@ -143,6 +146,9 @@
+
+ Dalamud.Boot DLL
+
ReshadePlugin
@@ -174,4 +180,14 @@
-
\ No newline at end of file
+
+
+ Dalamud.Boot DLL
+
+
+
+
+ Dalamud.Boot DLL
+
+
+
diff --git a/Dalamud.Boot/DalamudStartInfo.cpp b/Dalamud.Boot/DalamudStartInfo.cpp
index 15faf82ad..f5632a2ea 100644
--- a/Dalamud.Boot/DalamudStartInfo.cpp
+++ b/Dalamud.Boot/DalamudStartInfo.cpp
@@ -68,19 +68,37 @@ void from_json(const nlohmann::json& json, DalamudStartInfo::ClientLanguage& val
}
}
+void from_json(const nlohmann::json& json, DalamudStartInfo::LoadMethod& value) {
+ if (json.is_number_integer()) {
+ value = static_cast(json.get());
+
+ }
+ else if (json.is_string()) {
+ const auto langstr = unicode::convert(json.get(), &unicode::lower);
+ if (langstr == "entrypoint")
+ value = DalamudStartInfo::LoadMethod::Entrypoint;
+ else if (langstr == "inject")
+ value = DalamudStartInfo::LoadMethod::DllInject;
+ }
+}
+
void from_json(const nlohmann::json& json, DalamudStartInfo& config) {
if (!json.is_object())
return;
+ config.DalamudLoadMethod = json.value("LoadMethod", config.DalamudLoadMethod);
config.WorkingDirectory = json.value("WorkingDirectory", config.WorkingDirectory);
config.ConfigurationPath = json.value("ConfigurationPath", config.ConfigurationPath);
+ config.LogPath = json.value("LogPath", config.LogPath);
+ config.LogName = json.value("LogName", config.LogName);
config.PluginDirectory = json.value("PluginDirectory", config.PluginDirectory);
- config.DefaultPluginDirectory = json.value("DefaultPluginDirectory", config.DefaultPluginDirectory);
config.AssetDirectory = json.value("AssetDirectory", config.AssetDirectory);
config.Language = json.value("Language", config.Language);
config.GameVersion = json.value("GameVersion", config.GameVersion);
- config.DelayInitializeMs = json.value("DelayInitializeMs", config.DelayInitializeMs);
config.TroubleshootingPackData = json.value("TroubleshootingPackData", std::string{});
+ config.DelayInitializeMs = json.value("DelayInitializeMs", config.DelayInitializeMs);
+ config.NoLoadPlugins = json.value("NoLoadPlugins", config.NoLoadPlugins);
+ config.NoLoadThirdPartyPlugins = json.value("NoLoadThirdPartyPlugins", config.NoLoadThirdPartyPlugins);
config.BootLogPath = json.value("BootLogPath", config.BootLogPath);
config.BootShowConsole = json.value("BootShowConsole", config.BootShowConsole);
@@ -103,6 +121,7 @@ void from_json(const nlohmann::json& json, DalamudStartInfo& config) {
}
config.CrashHandlerShow = json.value("CrashHandlerShow", config.CrashHandlerShow);
+ config.NoExceptionHandlers = json.value("NoExceptionHandlers", config.NoExceptionHandlers);
}
void DalamudStartInfo::from_envvars() {
diff --git a/Dalamud.Boot/DalamudStartInfo.h b/Dalamud.Boot/DalamudStartInfo.h
index 66109abf7..e6cc54ab0 100644
--- a/Dalamud.Boot/DalamudStartInfo.h
+++ b/Dalamud.Boot/DalamudStartInfo.h
@@ -26,15 +26,25 @@ struct DalamudStartInfo {
};
friend void from_json(const nlohmann::json&, ClientLanguage&);
+ enum class LoadMethod : int {
+ Entrypoint,
+ DllInject,
+ };
+ friend void from_json(const nlohmann::json&, LoadMethod&);
+
+ LoadMethod DalamudLoadMethod = LoadMethod::Entrypoint;
std::string WorkingDirectory;
std::string ConfigurationPath;
+ std::string LogPath;
+ std::string LogName;
std::string PluginDirectory;
- std::string DefaultPluginDirectory;
std::string AssetDirectory;
ClientLanguage Language = ClientLanguage::English;
std::string GameVersion;
- int DelayInitializeMs = 0;
std::string TroubleshootingPackData;
+ int DelayInitializeMs = 0;
+ bool NoLoadPlugins;
+ bool NoLoadThirdPartyPlugins;
std::string BootLogPath;
bool BootShowConsole = false;
@@ -49,6 +59,7 @@ struct DalamudStartInfo {
std::set BootUnhookDlls{};
bool CrashHandlerShow = false;
+ bool NoExceptionHandlers = false;
friend void from_json(const nlohmann::json&, DalamudStartInfo&);
void from_envvars();
diff --git a/Dalamud.Boot/crashhandler_shared.h b/Dalamud.Boot/crashhandler_shared.h
index 4e8cbb520..8d93e4460 100644
--- a/Dalamud.Boot/crashhandler_shared.h
+++ b/Dalamud.Boot/crashhandler_shared.h
@@ -14,6 +14,7 @@ struct exception_info
CONTEXT ContextRecord;
uint64_t nLifetime;
HANDLE hThreadHandle;
+ HANDLE hEventHandle;
DWORD dwStackTraceLength;
DWORD dwTroubleshootingPackDataLength;
};
diff --git a/Dalamud.Boot/dllmain.cpp b/Dalamud.Boot/dllmain.cpp
index dd4fadd25..7d16f6e85 100644
--- a/Dalamud.Boot/dllmain.cpp
+++ b/Dalamud.Boot/dllmain.cpp
@@ -17,7 +17,7 @@ static void OnReshadeOverlay(reshade::api::effect_runtime *runtime) {
s_pfnReshadeOverlayCallback(reinterpret_cast(runtime->get_native()));
}
-DWORD WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) {
+HRESULT WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) {
g_startInfo.from_envvars();
std::string jsonParseError;
@@ -122,7 +122,7 @@ DWORD WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) {
logging::I("Calling InitializeClrAndGetEntryPoint");
void* entrypoint_vfn;
- int result = InitializeClrAndGetEntryPoint(
+ const auto result = InitializeClrAndGetEntryPoint(
g_hModule,
g_startInfo.BootEnableEtw,
runtimeconfig_path,
@@ -132,7 +132,7 @@ DWORD WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) {
L"Dalamud.EntryPoint+InitDelegate, Dalamud",
&entrypoint_vfn);
- if (result != 0)
+ if (FAILED(result))
return result;
using custom_component_entry_point_fn = void (CORECLR_DELEGATE_CALLTYPE*)(LPVOID, HANDLE, LPVOID);
@@ -141,8 +141,8 @@ DWORD WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) {
// ============================== VEH ======================================== //
logging::I("Initializing VEH...");
- if (utils::is_running_on_linux()) {
- logging::I("=> VEH was disabled, running on linux");
+ if (g_startInfo.NoExceptionHandlers) {
+ logging::W("=> Exception handlers are disabled from DalamudStartInfo.");
} else if (g_startInfo.BootVehEnabled) {
if (veh::add_handler(g_startInfo.BootVehFull, g_startInfo.WorkingDirectory))
logging::I("=> Done!");
@@ -164,10 +164,10 @@ DWORD WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) {
entrypoint_fn(lpParam, hMainThreadContinue, g_bReshadeAvailable ? &s_pfnReshadeOverlayCallback : nullptr);
logging::I("Done!");
- return 0;
+ return S_OK;
}
-DllExport DWORD WINAPI Initialize(LPVOID lpParam) {
+extern "C" DWORD WINAPI Initialize(LPVOID lpParam) {
return InitializeImpl(lpParam, CreateEvent(nullptr, TRUE, FALSE, nullptr));
}
diff --git a/Dalamud.Boot/hooks.cpp b/Dalamud.Boot/hooks.cpp
index 7cf489195..1b1280cf0 100644
--- a/Dalamud.Boot/hooks.cpp
+++ b/Dalamud.Boot/hooks.cpp
@@ -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("LdrRegisterDllNotification");
-static const auto LdrUnregisterDllNotification = utils::loaded_module(GetModuleHandleW(L"ntdll.dll")).get_exported_function("LdrUnregisterDllNotification");
-
hooks::getprocaddress_singleton_import_hook::getprocaddress_singleton_import_hook()
: m_pfnGetProcAddress(GetProcAddress)
, m_thunk("kernel32!GetProcAddress(Singleton Import Hook)",
diff --git a/Dalamud.Boot/hooks.h b/Dalamud.Boot/hooks.h
index ad3b2cc6c..f6ad370d1 100644
--- a/Dalamud.Boot/hooks.h
+++ b/Dalamud.Boot/hooks.h
@@ -1,6 +1,5 @@
#pragma once
-#include
#include
-
-
+
+
all
@@ -50,4 +50,10 @@
false
+
+
+
+ Always
+
+
diff --git a/Dalamud.CorePlugin/Dalamud.CorePlugin.json b/Dalamud.CorePlugin/Dalamud.CorePlugin.json
new file mode 100644
index 000000000..7db669a73
--- /dev/null
+++ b/Dalamud.CorePlugin/Dalamud.CorePlugin.json
@@ -0,0 +1,9 @@
+{
+ "Author": "Dalamud Maintainers",
+ "Name": "CorePlugin",
+ "Punchline": "Testbed for developing Dalamud features.",
+ "Description": "Develop and debug internal Dalamud features using CorePlugin. You have full access to all types in Dalamud assembly.",
+ "InternalName": "CorePlugin",
+ "ApplicableVersion": "any",
+ "Tags": []
+}
diff --git a/Dalamud.CorePlugin/PluginImpl.cs b/Dalamud.CorePlugin/PluginImpl.cs
index 9026ea0dd..7c9adc6a8 100644
--- a/Dalamud.CorePlugin/PluginImpl.cs
+++ b/Dalamud.CorePlugin/PluginImpl.cs
@@ -2,11 +2,13 @@ using System;
using System.IO;
using Dalamud.Configuration.Internal;
+using Dalamud.Game;
using Dalamud.Game.Command;
using Dalamud.Interface.Windowing;
-using Dalamud.Logging;
using Dalamud.Plugin;
+using Dalamud.Plugin.Services;
using Dalamud.Utility;
+using Serilog;
namespace Dalamud.CorePlugin
{
@@ -37,9 +39,6 @@ namespace Dalamud.CorePlugin
{
}
- ///
- public string Name => "Dalamud.CorePlugin";
-
///
public void Dispose()
{
@@ -50,36 +49,41 @@ namespace Dalamud.CorePlugin
private readonly WindowSystem windowSystem = new("Dalamud.CorePlugin");
private Localization localization;
+ private IPluginLog pluginLog;
+
///
/// Initializes a new instance of the class.
///
/// Dalamud plugin interface.
/// Logging service.
- public PluginImpl(DalamudPluginInterface pluginInterface, PluginLog log)
+ public PluginImpl(DalamudPluginInterface pluginInterface, IPluginLog log)
{
try
{
// this.InitLoc();
this.Interface = pluginInterface;
+ this.pluginLog = log;
this.windowSystem.AddWindow(new PluginWindow());
this.Interface.UiBuilder.Draw += this.OnDraw;
this.Interface.UiBuilder.OpenConfigUi += this.OnOpenConfigUi;
+ this.Interface.UiBuilder.OpenMainUi += this.OnOpenMainUi;
+ this.Interface.UiBuilder.DefaultFontHandle.ImFontChanged += (fc, _) =>
+ {
+ Log.Information($"CorePlugin : DefaultFontHandle.ImFontChanged called {fc}");
+ };
- Service.Get().AddHandler("/coreplug", new(this.OnCommand) { HelpMessage = $"Access the {this.Name} plugin." });
+ Service.Get().AddHandler("/coreplug", new(this.OnCommand) { HelpMessage = "Access the plugin." });
log.Information("CorePlugin ctor!");
}
catch (Exception ex)
{
- PluginLog.Error(ex, "kaboom");
+ log.Error(ex, "kaboom");
}
}
- ///
- public string Name => "Dalamud.CorePlugin";
-
///
/// Gets the plugin interface.
///
@@ -93,8 +97,6 @@ namespace Dalamud.CorePlugin
this.Interface.UiBuilder.Draw -= this.OnDraw;
this.windowSystem.RemoveAllWindows();
-
- this.Interface.ExplicitDispose();
}
///
@@ -127,13 +129,13 @@ namespace Dalamud.CorePlugin
}
catch (Exception ex)
{
- PluginLog.Error(ex, "Boom");
+ this.pluginLog.Error(ex, "Boom");
}
}
private void OnCommand(string command, string args)
{
- PluginLog.Information("Command called!");
+ this.pluginLog.Information("Command called!");
// this.window.IsOpen = true;
}
@@ -143,6 +145,11 @@ namespace Dalamud.CorePlugin
// this.window.IsOpen = true;
}
+ private void OnOpenMainUi()
+ {
+ Log.Verbose("Opened main UI");
+ }
+
#endif
}
}
diff --git a/Dalamud.Injector.Boot/main.cpp b/Dalamud.Injector.Boot/main.cpp
index 741505d08..7fc44f5e1 100644
--- a/Dalamud.Injector.Boot/main.cpp
+++ b/Dalamud.Injector.Boot/main.cpp
@@ -23,7 +23,7 @@ int wmain(int argc, wchar_t** argv)
// =========================================================================== //
void* entrypoint_vfn;
- int result = InitializeClrAndGetEntryPoint(
+ const auto result = InitializeClrAndGetEntryPoint(
GetModuleHandleW(nullptr),
false,
runtimeconfig_path,
@@ -33,15 +33,15 @@ int wmain(int argc, wchar_t** argv)
L"Dalamud.Injector.EntryPoint+MainDelegate, Dalamud.Injector",
&entrypoint_vfn);
- if (result != 0)
+ if (FAILED(result))
return result;
- typedef void (CORECLR_DELEGATE_CALLTYPE* custom_component_entry_point_fn)(int, wchar_t**);
+ typedef int (CORECLR_DELEGATE_CALLTYPE* custom_component_entry_point_fn)(int, wchar_t**);
custom_component_entry_point_fn entrypoint_fn = reinterpret_cast(entrypoint_vfn);
logging::I("Running Dalamud Injector...");
- entrypoint_fn(argc, argv);
+ const auto ret = entrypoint_fn(argc, argv);
logging::I("Done!");
- return 0;
+ return ret;
}
diff --git a/Dalamud.Injector/Dalamud.Injector.csproj b/Dalamud.Injector/Dalamud.Injector.csproj
index ea9e4f0a3..d8a74e58d 100644
--- a/Dalamud.Injector/Dalamud.Injector.csproj
+++ b/Dalamud.Injector/Dalamud.Injector.csproj
@@ -81,12 +81,6 @@
-
-
-
-
-
-
-
+
diff --git a/Dalamud.Injector/EntryPoint.cs b/Dalamud.Injector/EntryPoint.cs
index a35248062..9085eae04 100644
--- a/Dalamud.Injector/EntryPoint.cs
+++ b/Dalamud.Injector/EntryPoint.cs
@@ -9,7 +9,8 @@ using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
-using Dalamud.Game;
+using Dalamud.Common;
+using Dalamud.Common.Game;
using Newtonsoft.Json;
using Reloaded.Memory.Buffers;
using Serilog;
@@ -30,88 +31,100 @@ namespace Dalamud.Injector
///
/// Count of arguments.
/// char** string arguments.
- public delegate void MainDelegate(int argc, IntPtr argvPtr);
+ /// Return value (HRESULT).
+ public delegate int MainDelegate(int argc, IntPtr argvPtr);
///
/// Start the Dalamud injector.
///
/// Count of arguments.
/// byte** string arguments.
- public static void Main(int argc, IntPtr argvPtr)
+ /// Return value (HRESULT).
+ public static int Main(int argc, IntPtr argvPtr)
{
- List args = new(argc);
-
- unsafe
+ try
{
- var argv = (IntPtr*)argvPtr;
- for (var i = 0; i < argc; i++)
- args.Add(Marshal.PtrToStringUni(argv[i]));
- }
+ List args = new(argc);
- Init(args);
- args.Remove("-v"); // Remove "verbose" flag
-
- if (args.Count >= 2 && args[1].ToLowerInvariant() == "launch-test")
- {
- Environment.Exit(ProcessLaunchTestCommand(args));
- return;
- }
-
- DalamudStartInfo startInfo = null;
- if (args.Count == 1)
- {
- // No command defaults to inject
- args.Add("inject");
- args.Add("--all");
-
-#if !DEBUG
- args.Add("--warn");
-#endif
-
- }
- else if (int.TryParse(args[1], out var _))
- {
- // Assume that PID has been passed.
- args.Insert(1, "inject");
-
- // If originally second parameter exists, then assume that it's a base64 encoded start info.
- // Dalamud.Injector.exe inject [pid] [base64]
- if (args.Count == 4)
+ unsafe
{
- startInfo = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(Convert.FromBase64String(args[3])));
- args.RemoveAt(3);
+ var argv = (IntPtr*)argvPtr;
+ for (var i = 0; i < argc; i++)
+ args.Add(Marshal.PtrToStringUni(argv[i]));
+ }
+
+ Init(args);
+ args.Remove("-v"); // Remove "verbose" flag
+
+ if (args.Count >= 2 && args[1].ToLowerInvariant() == "launch-test")
+ {
+ return ProcessLaunchTestCommand(args);
+ }
+
+ DalamudStartInfo startInfo = null;
+ if (args.Count == 1)
+ {
+ // No command defaults to inject
+ args.Add("inject");
+ args.Add("--all");
+
+ #if !DEBUG
+ args.Add("--warn");
+ #endif
+
+ }
+ else if (int.TryParse(args[1], out var _))
+ {
+ // Assume that PID has been passed.
+ args.Insert(1, "inject");
+
+ // If originally second parameter exists, then assume that it's a base64 encoded start info.
+ // Dalamud.Injector.exe inject [pid] [base64]
+ if (args.Count == 4)
+ {
+ startInfo = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(Convert.FromBase64String(args[3])));
+ args.RemoveAt(3);
+ }
+ }
+
+ startInfo = ExtractAndInitializeStartInfoFromArguments(startInfo, args);
+ // Remove already handled arguments
+ args.Remove("--console");
+ args.Remove("--msgbox1");
+ args.Remove("--msgbox2");
+ args.Remove("--msgbox3");
+ args.Remove("--etw");
+ args.Remove("--veh");
+ args.Remove("--veh-full");
+ args.Remove("--no-plugin");
+ args.Remove("--no-3rd-plugin");
+ args.Remove("--crash-handler-console");
+ args.Remove("--no-exception-handlers");
+
+ var mainCommand = args[1].ToLowerInvariant();
+ if (mainCommand.Length > 0 && mainCommand.Length <= 6 && "inject"[..mainCommand.Length] == mainCommand)
+ {
+ return ProcessInjectCommand(args, startInfo);
+ }
+ else if (mainCommand.Length > 0 && mainCommand.Length <= 6 &&
+ "launch"[..mainCommand.Length] == mainCommand)
+ {
+ return ProcessLaunchCommand(args, startInfo);
+ }
+ else if (mainCommand.Length > 0 && mainCommand.Length <= 4 &&
+ "help"[..mainCommand.Length] == mainCommand)
+ {
+ return ProcessHelpCommand(args, args.Count >= 3 ? args[2] : null);
+ }
+ else
+ {
+ throw new CommandLineException($"\"{mainCommand}\" is not a valid command.");
}
}
-
- startInfo = ExtractAndInitializeStartInfoFromArguments(startInfo, args);
- // Remove already handled arguments
- args.Remove("--console");
- args.Remove("--msgbox1");
- args.Remove("--msgbox2");
- args.Remove("--msgbox3");
- args.Remove("--etw");
- args.Remove("--veh");
- args.Remove("--veh-full");
- args.Remove("--no-plugin");
- args.Remove("--no-3rd-plugin");
- args.Remove("--crash-handler-console");
-
- var mainCommand = args[1].ToLowerInvariant();
- if (mainCommand.Length > 0 && mainCommand.Length <= 6 && "inject"[..mainCommand.Length] == mainCommand)
+ catch (Exception e)
{
- Environment.Exit(ProcessInjectCommand(args, startInfo));
- }
- else if (mainCommand.Length > 0 && mainCommand.Length <= 6 && "launch"[..mainCommand.Length] == mainCommand)
- {
- Environment.Exit(ProcessLaunchCommand(args, startInfo));
- }
- else if (mainCommand.Length > 0 && mainCommand.Length <= 4 && "help"[..mainCommand.Length] == mainCommand)
- {
- Environment.Exit(ProcessHelpCommand(args, args.Count >= 3 ? args[2] : null));
- }
- else
- {
- throw new CommandLineException($"\"{mainCommand}\" is not a valid command.");
+ Log.Error(e, "Operation failed.");
+ return e.HResult;
}
}
@@ -187,6 +200,7 @@ namespace Dalamud.Injector
CullLogFile(logPath, 1 * 1024 * 1024);
Log.Logger = new LoggerConfiguration()
+ .WriteTo.Console(standardErrorFromLevel: LogEventLevel.Debug)
.WriteTo.File(logPath, fileSizeLimitBytes: null)
.MinimumLevel.ControlledBy(levelSwitch)
.CreateLogger();
@@ -375,12 +389,22 @@ namespace Dalamud.Injector
#else
startInfo.LogPath ??= xivlauncherDir;
#endif
+ startInfo.LogName ??= string.Empty;
// Set boot defaults
startInfo.BootShowConsole = args.Contains("--console");
startInfo.BootEnableEtw = args.Contains("--etw");
startInfo.BootLogPath = GetLogPath(startInfo.LogPath, "dalamud.boot", startInfo.LogName);
- startInfo.BootEnabledGameFixes = new List { "prevent_devicechange_crashes", "disable_game_openprocess_access_check", "redirect_openprocess", "backup_userdata_save", "prevent_icmphandle_crashes" };
+ startInfo.BootEnabledGameFixes = new()
+ {
+ // See: xivfixes.h, xivfixes.cpp
+ "prevent_devicechange_crashes",
+ "disable_game_openprocess_access_check",
+ "redirect_openprocess",
+ "backup_userdata_save",
+ "prevent_icmphandle_crashes",
+ "symbol_load_patches",
+ };
startInfo.BootDotnetOpenProcessHookMode = 0;
startInfo.BootWaitMessageBox |= args.Contains("--msgbox1") ? 1 : 0;
startInfo.BootWaitMessageBox |= args.Contains("--msgbox2") ? 2 : 0;
@@ -392,6 +416,7 @@ namespace Dalamud.Injector
startInfo.NoLoadThirdPartyPlugins = args.Contains("--no-3rd-plugin");
// startInfo.BootUnhookDlls = new List() { "kernel32.dll", "ntdll.dll", "user32.dll" };
startInfo.CrashHandlerShow = args.Contains("--crash-handler-console");
+ startInfo.NoExceptionHandlers = args.Contains("--no-exception-handlers");
return startInfo;
}
@@ -433,7 +458,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]");
+ Console.WriteLine("Enable VEH:\t[--veh], [--veh-full], [--no-exception-handlers]");
Console.WriteLine("Show messagebox:\t[--msgbox1], [--msgbox2], [--msgbox3]");
Console.WriteLine("No plugins:\t[--no-plugin] [--no-3rd-plugin]");
Console.WriteLine("Logging:\t[--logname=] [--logpath=]");
@@ -677,11 +702,11 @@ namespace Dalamud.Injector
mode = mode == null ? "entrypoint" : mode.ToLowerInvariant();
if (mode.Length > 0 && mode.Length <= 10 && "entrypoint"[0..mode.Length] == mode)
{
- mode = "entrypoint";
+ dalamudStartInfo.LoadMethod = LoadMethod.Entrypoint;
}
else if (mode.Length > 0 && mode.Length <= 6 && "inject"[0..mode.Length] == mode)
{
- mode = "inject";
+ dalamudStartInfo.LoadMethod = LoadMethod.DllInject;
}
else
{
@@ -793,16 +818,12 @@ namespace Dalamud.Injector
noFixAcl,
p =>
{
- if (!withoutDalamud && mode == "entrypoint")
+ if (!withoutDalamud && dalamudStartInfo.LoadMethod == LoadMethod.Entrypoint)
{
var startInfo = AdjustStartInfo(dalamudStartInfo, gamePath);
Log.Information("Using start info: {0}", JsonConvert.SerializeObject(startInfo));
- if (RewriteRemoteEntryPointW(p.Handle, gamePath, JsonConvert.SerializeObject(startInfo)) != 0)
- {
- Log.Error("[HOOKS] RewriteRemoteEntryPointW failed");
- throw new Exception("RewriteRemoteEntryPointW failed");
- }
-
+ Marshal.ThrowExceptionForHR(
+ RewriteRemoteEntryPointW(p.Handle, gamePath, JsonConvert.SerializeObject(startInfo)));
Log.Verbose("RewriteRemoteEntryPointW called!");
}
},
@@ -810,7 +831,7 @@ namespace Dalamud.Injector
Log.Verbose("Game process started with PID {0}", process.Id);
- if (!withoutDalamud && mode == "inject")
+ if (!withoutDalamud && dalamudStartInfo.LoadMethod == LoadMethod.DllInject)
{
var startInfo = AdjustStartInfo(dalamudStartInfo, gamePath);
Log.Information("Using start info: {0}", JsonConvert.SerializeObject(startInfo));
@@ -888,7 +909,7 @@ namespace Dalamud.Injector
var gameVerStr = File.ReadAllText(Path.Combine(ffxivDir, "ffxivgame.ver"));
var gameVer = GameVersion.Parse(gameVerStr);
- return new DalamudStartInfo(startInfo)
+ return startInfo with
{
GameVersion = gameVer,
};
diff --git a/Dalamud.Interface/ArrayExtensions.cs b/Dalamud.Interface/ArrayExtensions.cs
deleted file mode 100644
index 68bf52a29..000000000
--- a/Dalamud.Interface/ArrayExtensions.cs
+++ /dev/null
@@ -1,85 +0,0 @@
-using System.Diagnostics.CodeAnalysis;
-
-namespace Dalamud.Interface;
-
-internal static class ArrayExtensions
-{
- /// Iterate over enumerables with additional index.
- public static IEnumerable<(T Value, int Index)> WithIndex(this IEnumerable list)
- => list.Select((x, i) => (x, i));
-
- /// Remove an added index from an indexed enumerable.
- public static IEnumerable WithoutIndex(this IEnumerable<(T Value, int Index)> list)
- => list.Select(x => x.Value);
-
- /// Remove the value and only keep the index from an indexed enumerable.
- public static IEnumerable WithoutValue(this IEnumerable<(T Value, int Index)> list)
- => list.Select(x => x.Index);
-
-
- // Find the index of the first object fulfilling predicate's criteria in the given list.
- // Returns -1 if no such object is found.
- public static int IndexOf(this IEnumerable array, Predicate predicate)
- {
- var i = 0;
- foreach (var obj in array)
- {
- if (predicate(obj))
- return i;
-
- ++i;
- }
-
- return -1;
- }
-
- // Find the index of the first occurrence of needle in the given list.
- // Returns -1 if needle is not contained in the list.
- public static int IndexOf(this IEnumerable array, T needle) where T : notnull
- {
- var i = 0;
- foreach (var obj in array)
- {
- if (needle.Equals(obj))
- return i;
-
- ++i;
- }
-
- return -1;
- }
-
- // Find the first object fulfilling predicate's criteria in the given list, if one exists.
- // Returns true if an object is found, false otherwise.
- public static bool FindFirst(this IEnumerable array, Predicate predicate, [NotNullWhen(true)] out T? result)
- {
- foreach (var obj in array)
- {
- if (predicate(obj))
- {
- result = obj!;
- return true;
- }
- }
-
- result = default;
- return false;
- }
-
- // Find the first occurrence of needle in the given list and return the value contained in the list in result.
- // Returns true if an object is found, false otherwise.
- public static bool FindFirst(this IEnumerable array, T needle, [NotNullWhen(true)] out T? result) where T : notnull
- {
- foreach (var obj in array)
- {
- if (obj.Equals(needle))
- {
- result = obj;
- return true;
- }
- }
-
- result = default;
- return false;
- }
-}
diff --git a/Dalamud.Interface/Dalamud.Interface.csproj b/Dalamud.Interface/Dalamud.Interface.csproj
deleted file mode 100644
index 1dd8468be..000000000
--- a/Dalamud.Interface/Dalamud.Interface.csproj
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
- net7.0-windows
- x64
- x64;AnyCPU
- enable
- enable
- true
- Dalamud.Interface
-
-
-
-
-
-
-
diff --git a/Dalamud.Interface/ImGuiTable.cs b/Dalamud.Interface/ImGuiTable.cs
deleted file mode 100644
index 5ea6a2c9a..000000000
--- a/Dalamud.Interface/ImGuiTable.cs
+++ /dev/null
@@ -1,41 +0,0 @@
-using Dalamud.Interface.Raii;
-using ImGuiNET;
-
-namespace Dalamud.Interface;
-
-public static class ImGuiTable
-{
- // Draw a simple table with the given data using the drawRow action.
- // Headers and thus columns and column count are defined by columnTitles.
- public static void DrawTable(string label, IEnumerable data, Action drawRow, ImGuiTableFlags flags = ImGuiTableFlags.None,
- params string[] columnTitles)
- {
- if (columnTitles.Length == 0)
- return;
-
- using var table = ImRaii.Table(label, columnTitles.Length, flags);
- if (!table)
- return;
-
- foreach (var title in columnTitles)
- {
- ImGui.TableNextColumn();
- ImGui.TableHeader(title);
- }
-
- foreach (var datum in data)
- {
- ImGui.TableNextRow();
- drawRow(datum);
- }
- }
-
- // Draw a simple table with the given data using the drawRow action inside a collapsing header.
- // Headers and thus columns and column count are defined by columnTitles.
- public static void DrawTabbedTable(string label, IEnumerable data, Action drawRow, ImGuiTableFlags flags = ImGuiTableFlags.None,
- params string[] columnTitles)
- {
- if (ImGui.CollapsingHeader(label))
- DrawTable($"{label}##Table", data, drawRow, flags, columnTitles);
- }
-}
diff --git a/Dalamud.Interface/InterfaceHelpers.cs b/Dalamud.Interface/InterfaceHelpers.cs
deleted file mode 100644
index 26f09bedb..000000000
--- a/Dalamud.Interface/InterfaceHelpers.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-namespace Dalamud.Interface;
-
-public static class InterfaceHelpers
-{
- public static float GlobalScale = 1.0f;
-}
diff --git a/Dalamud.Test/Game/GameVersionTests.cs b/Dalamud.Test/Game/GameVersionTests.cs
index 44a5813c8..dcace4279 100644
--- a/Dalamud.Test/Game/GameVersionTests.cs
+++ b/Dalamud.Test/Game/GameVersionTests.cs
@@ -1,4 +1,4 @@
-using Dalamud.Game;
+using Dalamud.Common.Game;
using Xunit;
namespace Dalamud.Test.Game
diff --git a/Dalamud.sln b/Dalamud.sln
index 20442e52d..93089b9a6 100644
--- a/Dalamud.sln
+++ b/Dalamud.sln
@@ -6,8 +6,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
.gitignore = .gitignore
- targets\Dalamud.Plugin.targets = targets\Dalamud.Plugin.targets
targets\Dalamud.Plugin.Bootstrap.targets = targets\Dalamud.Plugin.Bootstrap.targets
+ targets\Dalamud.Plugin.targets = targets\Dalamud.Plugin.targets
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "build", "build\build.csproj", "{94E5B016-02B1-459B-97D9-E783F28764B2}"
@@ -38,184 +38,70 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FFXIVClientStructs.InteropS
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "DalamudCrashHandler", "DalamudCrashHandler\DalamudCrashHandler.vcxproj", "{317A264C-920B-44A1-8A34-F3A6827B0705}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dalamud.Interface", "Dalamud.Interface\Dalamud.Interface.csproj", "{757C997D-AA58-4241-8299-243C56514917}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dalamud.Common", "Dalamud.Common\Dalamud.Common.csproj", "{F21B13D2-D7D0-4456-B70F-3F8D695064E2}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
- Debug|x64 = Debug|x64
- Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
- Release|x64 = Release|x64
- Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{94E5B016-02B1-459B-97D9-E783F28764B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{94E5B016-02B1-459B-97D9-E783F28764B2}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {94E5B016-02B1-459B-97D9-E783F28764B2}.Debug|x64.ActiveCfg = Debug|Any CPU
- {94E5B016-02B1-459B-97D9-E783F28764B2}.Debug|x64.Build.0 = Debug|Any CPU
- {94E5B016-02B1-459B-97D9-E783F28764B2}.Debug|x86.ActiveCfg = Debug|Any CPU
{94E5B016-02B1-459B-97D9-E783F28764B2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{94E5B016-02B1-459B-97D9-E783F28764B2}.Release|Any CPU.Build.0 = Release|Any CPU
- {94E5B016-02B1-459B-97D9-E783F28764B2}.Release|x64.ActiveCfg = Release|Any CPU
- {94E5B016-02B1-459B-97D9-E783F28764B2}.Release|x64.Build.0 = Release|Any CPU
- {94E5B016-02B1-459B-97D9-E783F28764B2}.Release|x86.ActiveCfg = Release|Any CPU
{B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Debug|Any CPU.ActiveCfg = Debug|x64
{B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Debug|Any CPU.Build.0 = Debug|x64
- {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Debug|x64.ActiveCfg = Debug|x64
- {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Debug|x64.Build.0 = Debug|x64
- {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Debug|x86.ActiveCfg = Debug|Any CPU
- {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Debug|x86.Build.0 = Debug|Any CPU
{B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Release|Any CPU.ActiveCfg = Release|x64
{B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Release|Any CPU.Build.0 = Release|x64
- {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Release|x64.ActiveCfg = Release|x64
- {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Release|x64.Build.0 = Release|x64
- {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Release|x86.ActiveCfg = Release|Any CPU
- {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Release|x86.Build.0 = Release|Any CPU
{55198DC3-A03D-408E-A8EB-2077780C8576}.Debug|Any CPU.ActiveCfg = Debug|x64
{55198DC3-A03D-408E-A8EB-2077780C8576}.Debug|Any CPU.Build.0 = Debug|x64
- {55198DC3-A03D-408E-A8EB-2077780C8576}.Debug|x64.ActiveCfg = Debug|x64
- {55198DC3-A03D-408E-A8EB-2077780C8576}.Debug|x64.Build.0 = Debug|x64
- {55198DC3-A03D-408E-A8EB-2077780C8576}.Debug|x86.ActiveCfg = Debug|x64
- {55198DC3-A03D-408E-A8EB-2077780C8576}.Debug|x86.Build.0 = Debug|x64
{55198DC3-A03D-408E-A8EB-2077780C8576}.Release|Any CPU.ActiveCfg = Release|x64
{55198DC3-A03D-408E-A8EB-2077780C8576}.Release|Any CPU.Build.0 = Release|x64
- {55198DC3-A03D-408E-A8EB-2077780C8576}.Release|x64.ActiveCfg = Release|x64
- {55198DC3-A03D-408E-A8EB-2077780C8576}.Release|x64.Build.0 = Release|x64
- {55198DC3-A03D-408E-A8EB-2077780C8576}.Release|x86.ActiveCfg = Release|x64
- {55198DC3-A03D-408E-A8EB-2077780C8576}.Release|x86.Build.0 = Release|x64
{5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Debug|Any CPU.ActiveCfg = Debug|x64
{5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Debug|Any CPU.Build.0 = Debug|x64
- {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Debug|x64.ActiveCfg = Debug|x64
- {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Debug|x64.Build.0 = Debug|x64
- {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Debug|x86.ActiveCfg = Debug|Any CPU
- {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Debug|x86.Build.0 = Debug|Any CPU
{5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|Any CPU.ActiveCfg = Release|x64
{5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|Any CPU.Build.0 = Release|x64
- {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|x64.ActiveCfg = Release|x64
- {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|x64.Build.0 = Release|x64
- {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|x86.ActiveCfg = Release|Any CPU
- {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|x86.Build.0 = Release|Any CPU
{8874326B-E755-4D13-90B4-59AB263A3E6B}.Debug|Any CPU.ActiveCfg = Debug|x64
{8874326B-E755-4D13-90B4-59AB263A3E6B}.Debug|Any CPU.Build.0 = Debug|x64
- {8874326B-E755-4D13-90B4-59AB263A3E6B}.Debug|x64.ActiveCfg = Debug|x64
- {8874326B-E755-4D13-90B4-59AB263A3E6B}.Debug|x64.Build.0 = Debug|x64
- {8874326B-E755-4D13-90B4-59AB263A3E6B}.Debug|x86.ActiveCfg = Debug|x64
- {8874326B-E755-4D13-90B4-59AB263A3E6B}.Debug|x86.Build.0 = Debug|x64
{8874326B-E755-4D13-90B4-59AB263A3E6B}.Release|Any CPU.ActiveCfg = Release|x64
{8874326B-E755-4D13-90B4-59AB263A3E6B}.Release|Any CPU.Build.0 = Release|x64
- {8874326B-E755-4D13-90B4-59AB263A3E6B}.Release|x64.ActiveCfg = Release|x64
- {8874326B-E755-4D13-90B4-59AB263A3E6B}.Release|x64.Build.0 = Release|x64
- {8874326B-E755-4D13-90B4-59AB263A3E6B}.Release|x86.ActiveCfg = Release|x64
- {8874326B-E755-4D13-90B4-59AB263A3E6B}.Release|x86.Build.0 = Release|x64
{C8004563-1806-4329-844F-0EF6274291FC}.Debug|Any CPU.ActiveCfg = Debug|x64
{C8004563-1806-4329-844F-0EF6274291FC}.Debug|Any CPU.Build.0 = Debug|x64
- {C8004563-1806-4329-844F-0EF6274291FC}.Debug|x64.ActiveCfg = Debug|x64
- {C8004563-1806-4329-844F-0EF6274291FC}.Debug|x64.Build.0 = Debug|x64
- {C8004563-1806-4329-844F-0EF6274291FC}.Debug|x86.ActiveCfg = Debug|Any CPU
- {C8004563-1806-4329-844F-0EF6274291FC}.Debug|x86.Build.0 = Debug|Any CPU
{C8004563-1806-4329-844F-0EF6274291FC}.Release|Any CPU.ActiveCfg = Release|x64
{C8004563-1806-4329-844F-0EF6274291FC}.Release|Any CPU.Build.0 = Release|x64
- {C8004563-1806-4329-844F-0EF6274291FC}.Release|x64.ActiveCfg = Release|x64
- {C8004563-1806-4329-844F-0EF6274291FC}.Release|x64.Build.0 = Release|x64
- {C8004563-1806-4329-844F-0EF6274291FC}.Release|x86.ActiveCfg = Release|Any CPU
- {C8004563-1806-4329-844F-0EF6274291FC}.Release|x86.Build.0 = Release|Any CPU
{0483026E-C6CE-4B1A-AA68-46544C08140B}.Debug|Any CPU.ActiveCfg = Debug|x64
{0483026E-C6CE-4B1A-AA68-46544C08140B}.Debug|Any CPU.Build.0 = Debug|x64
- {0483026E-C6CE-4B1A-AA68-46544C08140B}.Debug|x64.ActiveCfg = Debug|x64
- {0483026E-C6CE-4B1A-AA68-46544C08140B}.Debug|x64.Build.0 = Debug|x64
- {0483026E-C6CE-4B1A-AA68-46544C08140B}.Debug|x86.ActiveCfg = Debug|x64
- {0483026E-C6CE-4B1A-AA68-46544C08140B}.Debug|x86.Build.0 = Debug|x64
{0483026E-C6CE-4B1A-AA68-46544C08140B}.Release|Any CPU.ActiveCfg = Release|x64
{0483026E-C6CE-4B1A-AA68-46544C08140B}.Release|Any CPU.Build.0 = Release|x64
- {0483026E-C6CE-4B1A-AA68-46544C08140B}.Release|x64.ActiveCfg = Release|x64
- {0483026E-C6CE-4B1A-AA68-46544C08140B}.Release|x64.Build.0 = Release|x64
- {0483026E-C6CE-4B1A-AA68-46544C08140B}.Release|x86.ActiveCfg = Release|x64
- {0483026E-C6CE-4B1A-AA68-46544C08140B}.Release|x86.Build.0 = Release|x64
{C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Debug|Any CPU.ActiveCfg = Debug|x64
{C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Debug|Any CPU.Build.0 = Debug|x64
- {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Debug|x64.ActiveCfg = Debug|x64
- {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Debug|x64.Build.0 = Debug|x64
- {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Debug|x86.ActiveCfg = Debug|x64
- {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Debug|x86.Build.0 = Debug|x64
{C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Release|Any CPU.ActiveCfg = Release|x64
{C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Release|Any CPU.Build.0 = Release|x64
- {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Release|x64.ActiveCfg = Release|x64
- {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Release|x64.Build.0 = Release|x64
- {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Release|x86.ActiveCfg = Release|x64
- {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Release|x86.Build.0 = Release|x64
{2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Debug|Any CPU.ActiveCfg = Debug|x64
{2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Debug|Any CPU.Build.0 = Debug|x64
- {2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Debug|x64.ActiveCfg = Debug|x64
- {2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Debug|x64.Build.0 = Debug|x64
- {2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Debug|x86.ActiveCfg = Debug|x64
- {2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Debug|x86.Build.0 = Debug|x64
{2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Release|Any CPU.ActiveCfg = Release|x64
{2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Release|Any CPU.Build.0 = Release|x64
- {2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Release|x64.ActiveCfg = Release|x64
- {2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Release|x64.Build.0 = Release|x64
- {2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Release|x86.ActiveCfg = Release|x64
- {2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Release|x86.Build.0 = Release|x64
{4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Debug|Any CPU.ActiveCfg = Debug|x64
{4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Debug|Any CPU.Build.0 = Debug|x64
- {4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Debug|x64.ActiveCfg = Debug|x64
- {4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Debug|x64.Build.0 = Debug|x64
- {4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Debug|x86.ActiveCfg = Debug|x64
- {4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Debug|x86.Build.0 = Debug|x64
{4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Release|Any CPU.ActiveCfg = Release|x64
{4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Release|Any CPU.Build.0 = Release|x64
- {4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Release|x64.ActiveCfg = Release|x64
- {4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Release|x64.Build.0 = Release|x64
- {4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Release|x86.ActiveCfg = Release|x64
- {4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Release|x86.Build.0 = Release|x64
{C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Debug|x64.ActiveCfg = Debug|Any CPU
- {C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Debug|x64.Build.0 = Debug|Any CPU
- {C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Debug|x86.ActiveCfg = Debug|Any CPU
- {C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Debug|x86.Build.0 = Debug|Any CPU
{C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Release|Any CPU.Build.0 = Release|Any CPU
- {C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Release|x64.ActiveCfg = Release|Any CPU
- {C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Release|x64.Build.0 = Release|Any CPU
- {C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Release|x86.ActiveCfg = Release|Any CPU
- {C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Release|x86.Build.0 = Release|Any CPU
{05AB2F46-268B-4915-806F-DDF813E2D59D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{05AB2F46-268B-4915-806F-DDF813E2D59D}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {05AB2F46-268B-4915-806F-DDF813E2D59D}.Debug|x64.ActiveCfg = Debug|Any CPU
- {05AB2F46-268B-4915-806F-DDF813E2D59D}.Debug|x64.Build.0 = Debug|Any CPU
- {05AB2F46-268B-4915-806F-DDF813E2D59D}.Debug|x86.ActiveCfg = Debug|Any CPU
- {05AB2F46-268B-4915-806F-DDF813E2D59D}.Debug|x86.Build.0 = Debug|Any CPU
{05AB2F46-268B-4915-806F-DDF813E2D59D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{05AB2F46-268B-4915-806F-DDF813E2D59D}.Release|Any CPU.Build.0 = Release|Any CPU
- {05AB2F46-268B-4915-806F-DDF813E2D59D}.Release|x64.ActiveCfg = Release|Any CPU
- {05AB2F46-268B-4915-806F-DDF813E2D59D}.Release|x64.Build.0 = Release|Any CPU
- {05AB2F46-268B-4915-806F-DDF813E2D59D}.Release|x86.ActiveCfg = Release|Any CPU
- {05AB2F46-268B-4915-806F-DDF813E2D59D}.Release|x86.Build.0 = Release|Any CPU
{317A264C-920B-44A1-8A34-F3A6827B0705}.Debug|Any CPU.ActiveCfg = Debug|x64
{317A264C-920B-44A1-8A34-F3A6827B0705}.Debug|Any CPU.Build.0 = Debug|x64
- {317A264C-920B-44A1-8A34-F3A6827B0705}.Debug|x64.ActiveCfg = Debug|x64
- {317A264C-920B-44A1-8A34-F3A6827B0705}.Debug|x64.Build.0 = Debug|x64
- {317A264C-920B-44A1-8A34-F3A6827B0705}.Debug|x86.ActiveCfg = Debug|x64
- {317A264C-920B-44A1-8A34-F3A6827B0705}.Debug|x86.Build.0 = Debug|x64
{317A264C-920B-44A1-8A34-F3A6827B0705}.Release|Any CPU.ActiveCfg = Release|x64
{317A264C-920B-44A1-8A34-F3A6827B0705}.Release|Any CPU.Build.0 = Release|x64
- {317A264C-920B-44A1-8A34-F3A6827B0705}.Release|x64.ActiveCfg = Release|x64
- {317A264C-920B-44A1-8A34-F3A6827B0705}.Release|x64.Build.0 = Release|x64
- {317A264C-920B-44A1-8A34-F3A6827B0705}.Release|x86.ActiveCfg = Release|x64
- {317A264C-920B-44A1-8A34-F3A6827B0705}.Release|x86.Build.0 = Release|x64
- {757C997D-AA58-4241-8299-243C56514917}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {757C997D-AA58-4241-8299-243C56514917}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {757C997D-AA58-4241-8299-243C56514917}.Debug|x64.ActiveCfg = Debug|Any CPU
- {757C997D-AA58-4241-8299-243C56514917}.Debug|x64.Build.0 = Debug|Any CPU
- {757C997D-AA58-4241-8299-243C56514917}.Debug|x86.ActiveCfg = Debug|Any CPU
- {757C997D-AA58-4241-8299-243C56514917}.Debug|x86.Build.0 = Debug|Any CPU
- {757C997D-AA58-4241-8299-243C56514917}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {757C997D-AA58-4241-8299-243C56514917}.Release|Any CPU.Build.0 = Release|Any CPU
- {757C997D-AA58-4241-8299-243C56514917}.Release|x64.ActiveCfg = Release|Any CPU
- {757C997D-AA58-4241-8299-243C56514917}.Release|x64.Build.0 = Release|Any CPU
- {757C997D-AA58-4241-8299-243C56514917}.Release|x86.ActiveCfg = Release|Any CPU
- {757C997D-AA58-4241-8299-243C56514917}.Release|x86.Build.0 = Release|Any CPU
+ {F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/Dalamud/ClientLanguage.cs b/Dalamud/ClientLanguage.cs
index 4e04d4a54..8f2c52456 100644
--- a/Dalamud/ClientLanguage.cs
+++ b/Dalamud/ClientLanguage.cs
@@ -1,5 +1,7 @@
namespace Dalamud;
+// TODO(v10): Delete this, and use Dalamud.Common.ClientLanguage instead for everything.
+
///
/// Enum describing the language the game loads in.
///
diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs
index ac410527c..70ed5dfde 100644
--- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs
+++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs
@@ -1,12 +1,16 @@
-using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
using Dalamud.Game.Text;
+using Dalamud.Interface.FontIdentifier;
+using Dalamud.Interface.Internal.Windows.PluginInstaller;
using Dalamud.Interface.Style;
+using Dalamud.IoC.Internal;
using Dalamud.Plugin.Internal.Profiles;
+using Dalamud.Storage;
using Dalamud.Utility;
using Newtonsoft.Json;
using Serilog;
@@ -18,7 +22,11 @@ namespace Dalamud.Configuration.Internal;
/// Class containing Dalamud settings.
///
[Serializable]
-internal sealed class DalamudConfiguration : IServiceType
+[ServiceManager.ProvidedService]
+#pragma warning disable SA1015
+[InherentDependency] // We must still have this when unloading
+#pragma warning restore SA1015
+internal sealed class DalamudConfiguration : IInternalDisposableService
{
private static readonly JsonSerializerSettings SerializerSettings = new()
{
@@ -28,7 +36,7 @@ internal sealed class DalamudConfiguration : IServiceType
};
[JsonIgnore]
- private string configPath;
+ private string? configPath;
[JsonIgnore]
private bool isSaveQueued;
@@ -42,12 +50,12 @@ internal sealed class DalamudConfiguration : IServiceType
///
/// Event that occurs when dalamud configuration is saved.
///
- public event DalamudConfigurationSavedDelegate DalamudConfigurationSaved;
+ public event DalamudConfigurationSavedDelegate? DalamudConfigurationSaved;
///
/// Gets or sets a list of muted works.
///
- public List BadWords { get; set; }
+ public List? BadWords { get; set; }
///
/// Gets or sets a value indicating whether or not the taskbar should flash once a duty is found.
@@ -62,12 +70,12 @@ internal sealed class DalamudConfiguration : IServiceType
///
/// Gets or sets the language code to load Dalamud localization with.
///
- public string LanguageOverride { get; set; } = null;
+ public string? LanguageOverride { get; set; } = null;
///
/// Gets or sets the last loaded Dalamud version.
///
- public string LastVersion { get; set; } = null;
+ public string? LastVersion { get; set; } = null;
///
/// Gets or sets a value indicating the last seen FTUE version.
@@ -78,7 +86,7 @@ internal sealed class DalamudConfiguration : IServiceType
///
/// Gets or sets the last loaded Dalamud version.
///
- public string LastChangelogMajorMinor { get; set; } = null;
+ public string? LastChangelogMajorMinor { get; set; } = null;
///
/// Gets or sets the chat type used by default for plugin messages.
@@ -100,6 +108,11 @@ internal sealed class DalamudConfiguration : IServiceType
///
public List ThirdRepoList { get; set; } = new();
+ ///
+ /// Gets or sets a value indicating whether or not a disclaimer regarding third-party repos has been dismissed.
+ ///
+ public bool? ThirdRepoSpeedbumpDismissed { get; set; } = null;
+
///
/// Gets or sets a list of hidden plugins.
///
@@ -133,15 +146,18 @@ internal sealed class DalamudConfiguration : IServiceType
///
/// Gets or sets a value indicating whether to use AXIS fonts from the game.
///
- public bool UseAxisFontsFromGame { get; set; } = false;
+ [Obsolete($"See {nameof(DefaultFontSpec)}")]
+ public bool UseAxisFontsFromGame { get; set; } = true;
///
- /// Gets or sets the gamma value to apply for Dalamud fonts. Effects text thickness.
- ///
- /// Before gamma is applied...
- /// * ...TTF fonts loaded with stb or FreeType are in linear space.
- /// * ...the game's prebaked AXIS fonts are in gamma space with gamma value of 1.4.
+ /// Gets or sets the default font spec.
///
+ public IFontSpec? DefaultFontSpec { get; set; }
+
+ ///
+ /// Gets or sets the gamma value to apply for Dalamud fonts. Do not use.
+ ///
+ [Obsolete("It happens that nobody touched this setting", true)]
public float FontGammaLevel { get; set; } = 1.4f;
///
@@ -199,6 +215,11 @@ internal sealed class DalamudConfiguration : IServiceType
///
public bool LogOpenAtStartup { get; set; }
+ ///
+ /// Gets or sets the number of lines to keep for the Dalamud Console window.
+ ///
+ public int LogLinesLimit { get; set; } = 10000;
+
///
/// Gets or sets a value indicating whether or not the dev bar should open at startup.
///
@@ -218,8 +239,16 @@ internal sealed class DalamudConfiguration : IServiceType
/// Gets or sets a value indicating whether or not plugin user interfaces should trigger sound effects.
/// This setting is effected by the in-game "System Sounds" option and volume.
///
+ [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "ABI")]
public bool EnablePluginUISoundEffects { get; set; }
+ ///
+ /// Gets or sets a value indicating whether or not an additional button allowing pinning and clickthrough options should be shown
+ /// on plugin title bars when using the Window System.
+ ///
+ [JsonProperty("EnablePluginUiAdditionalOptionsExperimental")]
+ public bool EnablePluginUiAdditionalOptions { get; set; } = false;
+
///
/// Gets or sets a value indicating whether viewports should always be disabled.
///
@@ -248,7 +277,7 @@ internal sealed class DalamudConfiguration : IServiceType
///
/// Gets or sets the kind of beta to download when matches the server value.
///
- public string DalamudBetaKind { get; set; }
+ public string? DalamudBetaKind { get; set; }
///
/// Gets or sets a value indicating whether or not any plugin should be loaded when the game is started.
@@ -414,26 +443,45 @@ internal sealed class DalamudConfiguration : IServiceType
///
public double UiBuilderHitch { get; set; } = 100;
+ ///
+ /// Gets or sets the page of the plugin installer that is shown by default when opened.
+ ///
+ public PluginInstallerWindow.PluginInstallerOpenKind PluginInstallerOpen { get; set; } = PluginInstallerWindow.PluginInstallerOpenKind.AllPlugins;
+
///
/// Load a configuration from the provided path.
///
- /// The path to load the configuration file from.
+ /// Path to read from.
+ /// File storage.
/// The deserialized configuration file.
- public static DalamudConfiguration Load(string path)
+ public static DalamudConfiguration Load(string path, ReliableFileStorage fs)
{
DalamudConfiguration deserialized = null;
+
try
{
- deserialized = JsonConvert.DeserializeObject(File.ReadAllText(path), SerializerSettings);
+ fs.ReadAllText(path, text =>
+ {
+ deserialized =
+ JsonConvert.DeserializeObject(text, SerializerSettings);
+
+ // If this reads as null, the file was empty, that's no good
+ if (deserialized == null)
+ throw new Exception("Read config was null.");
+ });
}
- catch (Exception ex)
+ catch (FileNotFoundException)
{
- Log.Warning(ex, "Failed to load DalamudConfiguration at {0}", path);
+ // ignored
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, "Could not load DalamudConfiguration at {Path}, creating new", path);
}
deserialized ??= new DalamudConfiguration();
deserialized.configPath = path;
-
+
return deserialized;
}
@@ -452,6 +500,13 @@ internal sealed class DalamudConfiguration : IServiceType
{
this.Save();
}
+
+ ///
+ void IInternalDisposableService.DisposeService()
+ {
+ // Make sure that we save, if a save is queued while we are shutting down
+ this.Update();
+ }
///
/// Save the file, if needed. Only needs to be done once a frame.
@@ -470,8 +525,11 @@ internal sealed class DalamudConfiguration : IServiceType
private void Save()
{
ThreadSafety.AssertMainThread();
+ if (this.configPath is null)
+ throw new InvalidOperationException("configPath is not set.");
- Util.WriteAllTextSafe(this.configPath, JsonConvert.SerializeObject(this, SerializerSettings));
+ Service.Get().WriteAllText(
+ this.configPath, JsonConvert.SerializeObject(this, SerializerSettings));
this.DalamudConfigurationSaved?.Invoke(this);
}
}
diff --git a/Dalamud/Configuration/Internal/DevPluginSettings.cs b/Dalamud/Configuration/Internal/DevPluginSettings.cs
index 939b03eca..cfe8ba411 100644
--- a/Dalamud/Configuration/Internal/DevPluginSettings.cs
+++ b/Dalamud/Configuration/Internal/DevPluginSettings.cs
@@ -1,3 +1,5 @@
+using System;
+
namespace Dalamud.Configuration.Internal;
///
@@ -14,4 +16,9 @@ internal sealed class DevPluginSettings
/// Gets or sets a value indicating whether this plugin should automatically reload on file change.
///
public bool AutomaticReloading { get; set; } = false;
+
+ ///
+ /// Gets or sets an ID uniquely identifying this specific instance of a devPlugin.
+ ///
+ public Guid WorkingPluginId { get; set; } = Guid.Empty;
}
diff --git a/Dalamud/Configuration/PluginConfigurations.cs b/Dalamud/Configuration/PluginConfigurations.cs
index 957a7c99e..de5e071c1 100644
--- a/Dalamud/Configuration/PluginConfigurations.cs
+++ b/Dalamud/Configuration/PluginConfigurations.cs
@@ -1,6 +1,6 @@
using System.IO;
-using Dalamud.Utility;
+using Dalamud.Storage;
using Newtonsoft.Json;
namespace Dalamud.Configuration;
@@ -31,24 +31,39 @@ public sealed class PluginConfigurations
///
/// Plugin configuration.
/// Plugin name.
- public void Save(IPluginConfiguration config, string pluginName)
+ /// WorkingPluginId of the plugin.
+ public void Save(IPluginConfiguration config, string pluginName, Guid workingPluginId)
{
- Util.WriteAllTextSafe(this.GetConfigFile(pluginName).FullName, SerializeConfig(config));
+ Service.Get()
+ .WriteAllText(this.GetConfigFile(pluginName).FullName, SerializeConfig(config), workingPluginId);
}
///
/// Load plugin configuration.
///
/// Plugin name.
+ /// WorkingPluginID of the plugin.
/// Plugin configuration.
- public IPluginConfiguration? Load(string pluginName)
+ public IPluginConfiguration? Load(string pluginName, Guid workingPluginId)
{
var path = this.GetConfigFile(pluginName);
- if (!path.Exists)
- return null;
+ IPluginConfiguration? config = null;
+ try
+ {
+ Service.Get().ReadAllText(path.FullName, text =>
+ {
+ config = DeserializeConfig(text);
+ if (config == null)
+ throw new Exception("Read config was null.");
+ }, workingPluginId);
+ }
+ catch (FileNotFoundException)
+ {
+ // ignored
+ }
- return DeserializeConfig(File.ReadAllText(path.FullName));
+ return config;
}
///
diff --git a/Dalamud/Dalamud.cs b/Dalamud/Dalamud.cs
index c38594771..f9d2aff3c 100644
--- a/Dalamud/Dalamud.cs
+++ b/Dalamud/Dalamud.cs
@@ -1,4 +1,3 @@
-using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
@@ -7,12 +6,13 @@ using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
+using Dalamud.Common;
using Dalamud.Configuration.Internal;
using Dalamud.Game;
-using Dalamud.Game.Gui.Internal;
-using Dalamud.Interface.Internal;
using Dalamud.Plugin.Internal;
+using Dalamud.Storage;
using Dalamud.Utility;
+using Dalamud.Utility.Timing;
using PInvoke;
using Serilog;
@@ -28,6 +28,7 @@ namespace Dalamud;
///
/// The main Dalamud class containing all subsystems.
///
+[ServiceManager.ProvidedService]
internal sealed class Dalamud : IServiceType
{
#region Internals
@@ -40,26 +41,48 @@ internal sealed class Dalamud : IServiceType
/// Initializes a new instance of the class.
///
/// DalamudStartInfo instance.
+ /// ReliableFileStorage instance.
/// The Dalamud configuration.
/// Event used to signal the main thread to continue.
- public Dalamud(DalamudStartInfo info, DalamudConfiguration configuration, IntPtr mainThreadContinueEvent)
+ public Dalamud(DalamudStartInfo info, ReliableFileStorage fs, DalamudConfiguration configuration, IntPtr mainThreadContinueEvent)
{
+ this.StartInfo = info;
+
this.unloadSignal = new ManualResetEvent(false);
this.unloadSignal.Reset();
+
+ // Directory resolved signatures(CS, our own) will be cached in
+ var cacheDir = new DirectoryInfo(Path.Combine(this.StartInfo.WorkingDirectory!, "cachedSigs"));
+ if (!cacheDir.Exists)
+ cacheDir.Create();
+
+ // Set up the SigScanner for our target module
+ TargetSigScanner scanner;
+ using (Timings.Start("SigScanner Init"))
+ {
+ scanner = new TargetSigScanner(
+ true, new FileInfo(Path.Combine(cacheDir.FullName, $"{this.StartInfo.GameVersion}.json")));
+ }
- ServiceManager.InitializeProvidedServicesAndClientStructs(this, info, configuration);
+ ServiceManager.InitializeProvidedServices(this, fs, configuration, scanner);
+
+ // Set up FFXIVClientStructs
+ this.SetupClientStructsResolver(cacheDir);
if (!configuration.IsResumeGameAfterPluginLoad)
{
NativeFunctions.SetEvent(mainThreadContinueEvent);
- try
- {
- _ = ServiceManager.InitializeEarlyLoadableServices();
- }
- catch (Exception e)
- {
- Log.Error(e, "Service initialization failure");
- }
+ ServiceManager.InitializeEarlyLoadableServices()
+ .ContinueWith(t =>
+ {
+ if (t.IsCompletedSuccessfully)
+ return;
+
+ Log.Error(t.Exception!, "Service initialization failure");
+ Util.Fatal(
+ "Dalamud failed to load all necessary services.\n\nThe game will continue, but you may not be able to use plugins.",
+ "Dalamud", false);
+ });
}
else
{
@@ -93,13 +116,36 @@ internal sealed class Dalamud : IServiceType
}
});
}
+
+ this.DefaultExceptionFilter = NativeFunctions.SetUnhandledExceptionFilter(nint.Zero);
+ NativeFunctions.SetUnhandledExceptionFilter(this.DefaultExceptionFilter);
+ Log.Debug($"SE default exception filter at {this.DefaultExceptionFilter.ToInt64():X}");
+
+ var debugSig = "40 55 53 56 48 8D AC 24 ?? ?? ?? ?? B8 ?? ?? ?? ?? E8 ?? ?? ?? ?? 48 2B E0 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 85 ?? ?? ?? ?? 48 83 3D ?? ?? ?? ?? ??";
+ this.DebugExceptionFilter = Service.Get().ScanText(debugSig);
+ Log.Debug($"SE debug exception filter at {this.DebugExceptionFilter.ToInt64():X}");
}
+
+ ///
+ /// Gets the start information for this Dalamud instance.
+ ///
+ internal DalamudStartInfo StartInfo { get; private set; }
///
/// Gets location of stored assets.
///
- internal DirectoryInfo AssetDirectory => new(Service.Get().AssetDirectory!);
-
+ internal DirectoryInfo AssetDirectory => new(this.StartInfo.AssetDirectory!);
+
+ ///
+ /// Gets the in-game default exception filter.
+ ///
+ private nint DefaultExceptionFilter { get; }
+
+ ///
+ /// Gets the in-game debug exception filter.
+ ///
+ private nint DebugExceptionFilter { get; }
+
///
/// Signal to the crash handler process that we should restart the game.
///
@@ -141,36 +187,38 @@ internal sealed class Dalamud : IServiceType
}
///
- /// Dispose subsystems related to plugin handling.
+ /// Replace the current exception handler with the default one.
///
- 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.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.GetNullable()?.Dispose();
-
- Service.GetNullable()?.Dispose();
-
- Service.GetNullable()?.Dispose();
- }
+ internal void UseDefaultExceptionHandler() =>
+ this.SetExceptionHandler(this.DefaultExceptionFilter);
///
- /// Replace the built-in exception handler with a debug one.
+ /// Replace the current exception handler with a debug one.
///
- internal void ReplaceExceptionHandler()
- {
- var releaseSig = "40 55 53 56 48 8D AC 24 ?? ?? ?? ?? B8 ?? ?? ?? ?? E8 ?? ?? ?? ?? 48 2B E0 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 85 ?? ?? ?? ?? 48 83 3D ?? ?? ?? ?? ??";
- var releaseFilter = Service.Get().ScanText(releaseSig);
- Log.Debug($"SE debug filter at {releaseFilter.ToInt64():X}");
+ internal void UseDebugExceptionHandler() =>
+ this.SetExceptionHandler(this.DebugExceptionFilter);
- var oldFilter = NativeFunctions.SetUnhandledExceptionFilter(releaseFilter);
- Log.Debug("Reset ExceptionFilter, old: {0}", oldFilter);
+ ///
+ /// Disable the current exception handler.
+ ///
+ internal void UseNoExceptionHandler() =>
+ this.SetExceptionHandler(nint.Zero);
+
+ ///
+ /// Helper function to set the exception handler.
+ ///
+ private void SetExceptionHandler(nint newFilter)
+ {
+ var oldFilter = NativeFunctions.SetUnhandledExceptionFilter(newFilter);
+ Log.Debug("Set ExceptionFilter to {0}, old: {1}", newFilter, oldFilter);
+ }
+
+ private void SetupClientStructsResolver(DirectoryInfo cacheDir)
+ {
+ using (Timings.Start("CS Resolver Init"))
+ {
+ FFXIVClientStructs.Interop.Resolver.GetInstance.SetupSearchSpace(Service.Get().SearchBase, new FileInfo(Path.Combine(cacheDir.FullName, $"{this.StartInfo.GameVersion}_cs.json")));
+ FFXIVClientStructs.Interop.Resolver.GetInstance.Resolve();
+ }
}
}
diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj
index e2da1a057..7e166d8b3 100644
--- a/Dalamud/Dalamud.csproj
+++ b/Dalamud/Dalamud.csproj
@@ -8,11 +8,12 @@
- 7.10.1.0
+ 9.0.0.21
XIV Launcher addon framework
$(DalamudVersion)
$(DalamudVersion)
$(DalamudVersion)
+ AGPL-3.0-or-later
@@ -67,8 +68,12 @@
-
-
+
+
+
+
+ all
+
@@ -76,6 +81,7 @@
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
@@ -85,9 +91,10 @@
+
-
+
diff --git a/Dalamud/DalamudAsset.cs b/Dalamud/DalamudAsset.cs
new file mode 100644
index 000000000..a7b35b196
--- /dev/null
+++ b/Dalamud/DalamudAsset.cs
@@ -0,0 +1,153 @@
+using Dalamud.Storage.Assets;
+
+namespace Dalamud;
+
+///
+/// Specifies an asset that has been shipped as Dalamud Asset.
+/// Any asset can cease to exist at any point, even if the enum value exists.
+/// Either ship your own assets, or be prepared for errors.
+///
+public enum DalamudAsset
+{
+ ///
+ /// Nothing.
+ ///
+ [DalamudAsset(DalamudAssetPurpose.Empty, data: new byte[0])]
+ Unspecified = 0,
+
+ ///
+ /// : The fallback empty texture.
+ ///
+ [DalamudAsset(DalamudAssetPurpose.TextureFromRaw, data: new byte[] { 0, 0, 0, 0, 0, 0, 0, 0 })]
+ [DalamudAssetRawTexture(4, 8, 4, SharpDX.DXGI.Format.BC1_UNorm)]
+ Empty4X4 = 1000,
+
+ ///
+ /// : The Dalamud logo.
+ ///
+ [DalamudAsset(DalamudAssetPurpose.TextureFromPng)]
+ [DalamudAssetPath("UIRes", "logo.png")]
+ Logo = 1001,
+
+ ///
+ /// : The Dalamud logo, but smaller.
+ ///
+ [DalamudAsset(DalamudAssetPurpose.TextureFromPng)]
+ [DalamudAssetPath("UIRes", "tsmLogo.png")]
+ LogoSmall = 1002,
+
+ ///
+ /// : The default plugin icon.
+ ///
+ [DalamudAsset(DalamudAssetPurpose.TextureFromPng)]
+ [DalamudAssetPath("UIRes", "defaultIcon.png")]
+ DefaultIcon = 1003,
+
+ ///
+ /// : The disabled plugin icon.
+ ///
+ [DalamudAsset(DalamudAssetPurpose.TextureFromPng)]
+ [DalamudAssetPath("UIRes", "disabledIcon.png")]
+ DisabledIcon = 1004,
+
+ ///
+ /// : The outdated installable plugin icon.
+ ///
+ [DalamudAsset(DalamudAssetPurpose.TextureFromPng)]
+ [DalamudAssetPath("UIRes", "outdatedInstallableIcon.png")]
+ OutdatedInstallableIcon = 1005,
+
+ ///
+ /// : The plugin trouble icon overlay.
+ ///
+ [DalamudAsset(DalamudAssetPurpose.TextureFromPng)]
+ [DalamudAssetPath("UIRes", "troubleIcon.png")]
+ TroubleIcon = 1006,
+
+ ///
+ /// : The plugin trouble icon overlay.
+ ///
+ [DalamudAsset(DalamudAssetPurpose.TextureFromPng)]
+ [DalamudAssetPath("UIRes", "devPluginIcon.png")]
+ DevPluginIcon = 1007,
+
+ ///
+ /// : The plugin update icon overlay.
+ ///
+ [DalamudAsset(DalamudAssetPurpose.TextureFromPng)]
+ [DalamudAssetPath("UIRes", "updateIcon.png")]
+ UpdateIcon = 1008,
+
+ ///
+ /// : The plugin installed icon overlay.
+ ///
+ [DalamudAsset(DalamudAssetPurpose.TextureFromPng)]
+ [DalamudAssetPath("UIRes", "installedIcon.png")]
+ InstalledIcon = 1009,
+
+ ///
+ /// : The third party plugin icon overlay.
+ ///
+ [DalamudAsset(DalamudAssetPurpose.TextureFromPng)]
+ [DalamudAssetPath("UIRes", "thirdIcon.png")]
+ ThirdIcon = 1010,
+
+ ///
+ /// : The installed third party plugin icon overlay.
+ ///
+ [DalamudAsset(DalamudAssetPurpose.TextureFromPng)]
+ [DalamudAssetPath("UIRes", "thirdInstalledIcon.png")]
+ ThirdInstalledIcon = 1011,
+
+ ///
+ /// : The API bump explainer icon.
+ ///
+ [DalamudAsset(DalamudAssetPurpose.TextureFromPng)]
+ [DalamudAssetPath("UIRes", "changelogApiBump.png")]
+ ChangelogApiBumpIcon = 1012,
+
+ ///
+ /// : The background shade for
+ /// .
+ ///
+ [DalamudAsset(DalamudAssetPurpose.TextureFromPng)]
+ [DalamudAssetPath("UIRes", "tsmShade.png")]
+ TitleScreenMenuShade = 1013,
+
+ ///
+ /// : Noto Sans CJK JP Medium.
+ ///
+ [DalamudAsset(DalamudAssetPurpose.Font)]
+ [DalamudAssetPath("UIRes", "NotoSansCJKjp-Regular.otf")]
+ [DalamudAssetPath("UIRes", "NotoSansCJKjp-Medium.otf")]
+ NotoSansJpMedium = 2000,
+
+ ///
+ /// : Noto Sans CJK KR Regular.
+ ///
+ [DalamudAsset(DalamudAssetPurpose.Font)]
+ [DalamudAssetPath("UIRes", "NotoSansCJKkr-Regular.otf")]
+ [DalamudAssetPath("UIRes", "NotoSansKR-Regular.otf")]
+ NotoSansKrRegular = 2001,
+
+ ///
+ /// : Inconsolata Regular.
+ ///
+ [DalamudAsset(DalamudAssetPurpose.Font)]
+ [DalamudAssetPath("UIRes", "Inconsolata-Regular.ttf")]
+ InconsolataRegular = 2002,
+
+ ///
+ /// : FontAwesome Free Solid.
+ ///
+ [DalamudAsset(DalamudAssetPurpose.Font)]
+ [DalamudAssetPath("UIRes", "FontAwesomeFreeSolid.otf")]
+ FontAwesomeFreeSolid = 2003,
+
+ ///
+ /// : Game symbol fonts being used as webfonts at Lodestone.
+ ///
+ [DalamudAsset(DalamudAssetPurpose.Font, required: false)]
+ // [DalamudAssetOnlineSource("https://img.finalfantasyxiv.com/lds/pc/global/fonts/FFXIV_Lodestone_SSF.ttf")]
+ LodestoneGameSymbol = 2004,
+}
diff --git a/Dalamud/Data/DataManager.cs b/Dalamud/Data/DataManager.cs
index a4c81c7a7..da93f57c4 100644
--- a/Dalamud/Data/DataManager.cs
+++ b/Dalamud/Data/DataManager.cs
@@ -1,27 +1,20 @@
-using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
-using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Threading;
-using Dalamud.Interface.Internal;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using Dalamud.Utility.Timing;
-using ImGuiScene;
using JetBrains.Annotations;
using Lumina;
using Lumina.Data;
-using Lumina.Data.Files;
-using Lumina.Data.Parsing.Tex.Buffers;
using Lumina.Excel;
using Newtonsoft.Json;
using Serilog;
-using SharpDX.DXGI;
namespace Dalamud.Data;
@@ -34,39 +27,20 @@ namespace Dalamud.Data;
#pragma warning disable SA1015
[ResolveVia]
#pragma warning restore SA1015
-public sealed class DataManager : IDisposable, IServiceType, IDataManager
+internal sealed class DataManager : IInternalDisposableService, IDataManager
{
- private const string IconFileFormat = "ui/icon/{0:D3}000/{1}{2:D6}.tex";
- private const string HighResolutionIconFileFormat = "ui/icon/{0:D3}000/{1}{2:D6}_hr1.tex";
-
private readonly Thread luminaResourceThread;
private readonly CancellationTokenSource luminaCancellationTokenSource;
[ServiceManager.ServiceConstructor]
- private DataManager(DalamudStartInfo dalamudStartInfo, Dalamud dalamud)
+ private DataManager(Dalamud dalamud)
{
- this.Language = dalamudStartInfo.Language;
+ this.Language = (ClientLanguage)dalamud.StartInfo.Language;
- // Set up default values so plugins do not null-reference when data is being loaded.
- this.ClientOpCodes = this.ServerOpCodes = new ReadOnlyDictionary(new Dictionary());
-
- var baseDir = dalamud.AssetDirectory.FullName;
try
{
Log.Verbose("Starting data load...");
-
- var zoneOpCodeDict = JsonConvert.DeserializeObject>(
- File.ReadAllText(Path.Combine(baseDir, "UIRes", "serveropcode.json")))!;
- this.ServerOpCodes = new ReadOnlyDictionary(zoneOpCodeDict);
-
- Log.Verbose("Loaded {0} ServerOpCodes.", zoneOpCodeDict.Count);
-
- var clientOpCodeDict = JsonConvert.DeserializeObject>(
- File.ReadAllText(Path.Combine(baseDir, "UIRes", "clientopcode.json")))!;
- this.ClientOpCodes = new ReadOnlyDictionary(clientOpCodeDict);
-
- Log.Verbose("Loaded {0} ClientOpCodes.", clientOpCodeDict.Count);
-
+
using (Timings.Start("Lumina Init"))
{
var luminaOptions = new LuminaOptions
@@ -93,17 +67,20 @@ public sealed class DataManager : IDisposable, IServiceType, IDataManager
Log.Information("Lumina is ready: {0}", this.GameData.DataPath);
- try
+ if (!dalamud.StartInfo.TroubleshootingPackData.IsNullOrEmpty())
{
- var tsInfo =
- JsonConvert.DeserializeObject(
- dalamudStartInfo.TroubleshootingPackData);
- this.HasModifiedGameDataFiles =
- tsInfo?.IndexIntegrity is LauncherTroubleshootingInfo.IndexIntegrityResult.Failed or LauncherTroubleshootingInfo.IndexIntegrityResult.Exception;
- }
- catch
- {
- // ignored
+ try
+ {
+ var tsInfo =
+ JsonConvert.DeserializeObject(
+ dalamud.StartInfo.TroubleshootingPackData);
+ this.HasModifiedGameDataFiles =
+ tsInfo?.IndexIntegrity is LauncherTroubleshootingInfo.IndexIntegrityResult.Failed or LauncherTroubleshootingInfo.IndexIntegrityResult.Exception;
+ }
+ catch
+ {
+ // ignored
+ }
}
}
@@ -137,25 +114,20 @@ public sealed class DataManager : IDisposable, IServiceType, IDataManager
///
public ClientLanguage Language { get; private set; }
- ///
- public ReadOnlyDictionary ServerOpCodes { get; private set; }
-
- ///
- [UsedImplicitly]
- public ReadOnlyDictionary ClientOpCodes { get; private set; }
-
///
public GameData GameData { get; private set; }
///
public ExcelModule Excel => this.GameData.Excel;
- ///
- public bool IsDataReady { get; private set; }
-
///
public bool HasModifiedGameDataFiles { get; private set; }
+ ///
+ /// Gets a value indicating whether Game Data is ready to be read.
+ ///
+ internal bool IsDataReady { get; private set; }
+
#region Lumina Wrappers
///
@@ -183,162 +155,10 @@ public sealed class DataManager : IDisposable, IServiceType, IDataManager
public bool FileExists(string path)
=> this.GameData.FileExists(path);
- ///
- /// Get a containing the icon with the given ID.
- ///
- /// The icon ID.
- /// The containing the icon.
- [Obsolete("Use ITextureProvider instead")]
- public TexFile? GetIcon(uint iconId)
- => this.GetIcon(this.Language, iconId, false);
-
- ///
- [Obsolete("Use ITextureProvider instead")]
- public TexFile? GetIcon(uint iconId, bool highResolution)
- => this.GetIcon(this.Language, iconId, highResolution);
-
- ///
- [Obsolete("Use ITextureProvider instead")]
- public TexFile? GetIcon(bool isHq, uint iconId)
- {
- var type = isHq ? "hq/" : string.Empty;
- return this.GetIcon(type, iconId);
- }
-
- ///
- /// Get a containing the icon with the given ID, of the given language.
- ///
- /// The requested language.
- /// The icon ID.
- /// The containing the icon.
- [Obsolete("Use ITextureProvider instead")]
- public TexFile? GetIcon(ClientLanguage iconLanguage, uint iconId)
- => this.GetIcon(iconLanguage, iconId, false);
-
- ///
- [Obsolete("Use ITextureProvider instead")]
- public TexFile? GetIcon(ClientLanguage iconLanguage, uint iconId, bool highResolution)
- {
- var type = iconLanguage switch
- {
- ClientLanguage.Japanese => "ja/",
- ClientLanguage.English => "en/",
- ClientLanguage.German => "de/",
- ClientLanguage.French => "fr/",
- _ => throw new ArgumentOutOfRangeException(nameof(iconLanguage), $"Unknown Language: {iconLanguage}"),
- };
-
- return this.GetIcon(type, iconId, highResolution);
- }
-
- ///
- /// Get a containing the icon with the given ID, of the given type.
- ///
- /// The type of the icon (e.g. 'hq' to get the HQ variant of an item icon).
- /// The icon ID.
- /// The containing the icon.
- [Obsolete("Use ITextureProvider instead")]
- public TexFile? GetIcon(string? type, uint iconId)
- => this.GetIcon(type, iconId, false);
-
- ///
- [Obsolete("Use ITextureProvider instead")]
- public TexFile? GetIcon(string? type, uint iconId, bool highResolution)
- {
- var format = highResolution ? HighResolutionIconFileFormat : IconFileFormat;
-
- type ??= string.Empty;
- if (type.Length > 0 && !type.EndsWith("/"))
- type += "/";
-
- var filePath = string.Format(format, iconId / 1000, type, iconId);
- var file = this.GetFile(filePath);
-
- if (type == string.Empty || file != default)
- return file;
-
- // Couldn't get specific type, try for generic version.
- filePath = string.Format(format, iconId / 1000, string.Empty, iconId);
- file = this.GetFile(filePath);
- return file;
- }
-
- ///
- [Obsolete("Use ITextureProvider instead")]
- public TexFile? GetHqIcon(uint iconId)
- => this.GetIcon(true, iconId);
-
- ///
- [Obsolete("Use ITextureProvider instead")]
- [return: NotNullIfNotNull(nameof(tex))]
- public TextureWrap? GetImGuiTexture(TexFile? tex)
- {
- if (tex is null)
- return null;
-
- var im = Service.Get();
- var buffer = tex.TextureBuffer;
- var bpp = 1 << (((int)tex.Header.Format & (int)TexFile.TextureFormat.BppMask) >>
- (int)TexFile.TextureFormat.BppShift);
-
- var (dxgiFormat, conversion) = TexFile.GetDxgiFormatFromTextureFormat(tex.Header.Format, false);
- if (conversion != TexFile.DxgiFormatConversion.NoConversion || !im.SupportsDxgiFormat((Format)dxgiFormat))
- {
- dxgiFormat = (int)Format.B8G8R8A8_UNorm;
- buffer = buffer.Filter(0, 0, TexFile.TextureFormat.B8G8R8A8);
- bpp = 32;
- }
-
- var pitch = buffer is BlockCompressionTextureBuffer
- ? Math.Max(1, (buffer.Width + 3) / 4) * 2 * bpp
- : ((buffer.Width * bpp) + 7) / 8;
- return im.LoadImageFromDxgiFormat(buffer.RawData, pitch, buffer.Width, buffer.Height, (Format)dxgiFormat);
- }
-
- ///
- [Obsolete("Use ITextureProvider instead")]
- public TextureWrap? GetImGuiTexture(string path)
- => this.GetImGuiTexture(this.GetFile(path));
-
- ///
- /// Get a containing the icon with the given ID.
- ///
- /// The icon ID.
- /// The containing the icon.
- /// TODO(v9): remove in api9 in favor of GetImGuiTextureIcon(uint iconId, bool highResolution)
- [Obsolete("Use ITextureProvider instead")]
- public TextureWrap? GetImGuiTextureIcon(uint iconId)
- => this.GetImGuiTexture(this.GetIcon(iconId, false));
-
- ///
- [Obsolete("Use ITextureProvider instead")]
- public TextureWrap? GetImGuiTextureIcon(uint iconId, bool highResolution)
- => this.GetImGuiTexture(this.GetIcon(iconId, highResolution));
-
- ///
- [Obsolete("Use ITextureProvider instead")]
- public TextureWrap? GetImGuiTextureIcon(bool isHq, uint iconId)
- => this.GetImGuiTexture(this.GetIcon(isHq, iconId));
-
- ///
- [Obsolete("Use ITextureProvider instead")]
- public TextureWrap? GetImGuiTextureIcon(ClientLanguage iconLanguage, uint iconId)
- => this.GetImGuiTexture(this.GetIcon(iconLanguage, iconId));
-
- ///
- [Obsolete("Use ITextureProvider instead")]
- public TextureWrap? GetImGuiTextureIcon(string type, uint iconId)
- => this.GetImGuiTexture(this.GetIcon(type, iconId));
-
- ///
- [Obsolete("Use ITextureProvider instead")]
- public TextureWrap? GetImGuiTextureHqIcon(uint iconId)
- => this.GetImGuiTexture(this.GetHqIcon(iconId));
-
#endregion
///
- void IDisposable.Dispose()
+ void IInternalDisposableService.DisposeService()
{
this.luminaCancellationTokenSource.Cancel();
}
diff --git a/Dalamud/EntryPoint.cs b/Dalamud/EntryPoint.cs
index 6b4f7a8d1..0763b9d91 100644
--- a/Dalamud/EntryPoint.cs
+++ b/Dalamud/EntryPoint.cs
@@ -1,4 +1,3 @@
-using System;
using System.Diagnostics;
using System.IO;
using System.Net;
@@ -6,10 +5,12 @@ using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
+using Dalamud.Common;
using Dalamud.Configuration.Internal;
using Dalamud.Logging.Internal;
using Dalamud.Logging.Retention;
using Dalamud.Plugin.Internal;
+using Dalamud.Storage;
using Dalamud.Support;
using Dalamud.Utility;
using Newtonsoft.Json;
@@ -162,7 +163,10 @@ public static class EntryPoint
SerilogEventSink.Instance.LogLine += SerilogOnLogLine;
// Load configuration first to get some early persistent state, like log level
- var configuration = DalamudConfiguration.Load(info.ConfigurationPath!);
+#pragma warning disable CS0618 // Type or member is obsolete
+ var fs = new ReliableFileStorage(Path.GetDirectoryName(info.ConfigurationPath)!);
+#pragma warning restore CS0618 // Type or member is obsolete
+ var configuration = DalamudConfiguration.Load(info.ConfigurationPath!, fs);
// Set the appropriate logging level from the configuration
if (!configuration.LogSynchronously)
@@ -170,7 +174,8 @@ public static class EntryPoint
LogLevelSwitch.MinimumLevel = configuration.LogLevel;
// Log any unhandled exception.
- AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
+ if (!info.NoExceptionHandlers)
+ AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
TaskScheduler.UnobservedTaskException += OnUnobservedTaskException;
var unloadFailed = false;
@@ -186,15 +191,18 @@ public static class EntryPoint
Log.Information(new string('-', 80));
Log.Information("Initializing a session..");
+ if (string.IsNullOrEmpty(info.WorkingDirectory))
+ throw new Exception("Working directory was invalid");
+
Reloaded.Hooks.Tools.Utilities.FasmBasePath = new DirectoryInfo(info.WorkingDirectory);
// This is due to GitHub not supporting TLS 1.0, so we enable all TLS versions globally
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12 | SecurityProtocolType.Tls;
- if (!Util.IsLinux())
+ if (!Util.IsWine())
InitSymbolHandler(info);
- var dalamud = new Dalamud(info, configuration, mainThreadContinueEvent);
+ var dalamud = new Dalamud(info, fs, configuration, mainThreadContinueEvent);
Log.Information("This is Dalamud - Core: {GitHash}, CS: {CsGitHash} [{CsVersion}]", Util.GetGitHash(), Util.GetGitHashClientStructs(), FFXIVClientStructs.Interop.Resolver.Version);
dalamud.WaitForUnload();
@@ -216,7 +224,8 @@ public static class EntryPoint
finally
{
TaskScheduler.UnobservedTaskException -= OnUnobservedTaskException;
- AppDomain.CurrentDomain.UnhandledException -= OnUnhandledException;
+ if (!info.NoExceptionHandlers)
+ AppDomain.CurrentDomain.UnhandledException -= OnUnhandledException;
Log.Information("Session has ended.");
Log.CloseAndFlush();
diff --git a/Dalamud/Game/Addon/AddonLifecyclePooledArgs.cs b/Dalamud/Game/Addon/AddonLifecyclePooledArgs.cs
new file mode 100644
index 000000000..14def2036
--- /dev/null
+++ b/Dalamud/Game/Addon/AddonLifecyclePooledArgs.cs
@@ -0,0 +1,107 @@
+using System.Runtime.CompilerServices;
+using System.Threading;
+
+using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
+
+namespace Dalamud.Game.Addon;
+
+/// Argument pool for Addon Lifecycle services.
+[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()
+ {
+ }
+
+ /// Rents an instance of an argument.
+ /// The rented instance.
+ /// The returner.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public PooledEntry Rent(out AddonSetupArgs arg) => new(out arg, this.addonSetupArgPool);
+
+ /// Rents an instance of an argument.
+ /// The rented instance.
+ /// The returner.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public PooledEntry Rent(out AddonFinalizeArgs arg) => new(out arg, this.addonFinalizeArgPool);
+
+ /// Rents an instance of an argument.
+ /// The rented instance.
+ /// The returner.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public PooledEntry Rent(out AddonDrawArgs arg) => new(out arg, this.addonDrawArgPool);
+
+ /// Rents an instance of an argument.
+ /// The rented instance.
+ /// The returner.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public PooledEntry Rent(out AddonUpdateArgs arg) => new(out arg, this.addonUpdateArgPool);
+
+ /// Rents an instance of an argument.
+ /// The rented instance.
+ /// The returner.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public PooledEntry Rent(out AddonRefreshArgs arg) => new(out arg, this.addonRefreshArgPool);
+
+ /// Rents an instance of an argument.
+ /// The rented instance.
+ /// The returner.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public PooledEntry Rent(out AddonRequestedUpdateArgs arg) =>
+ new(out arg, this.addonRequestedUpdateArgPool);
+
+ /// Rents an instance of an argument.
+ /// The rented instance.
+ /// The returner.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public PooledEntry Rent(out AddonReceiveEventArgs arg) =>
+ new(out arg, this.addonReceiveEventArgPool);
+
+ /// Returns the object to the pool on dispose.
+ /// The type.
+ public readonly ref struct PooledEntry
+ where T : AddonArgs, new()
+ {
+ private readonly Span pool;
+ private readonly T obj;
+
+ /// Initializes a new instance of the struct.
+ /// An instance of the argument.
+ /// The pool to rent from and return to.
+ public PooledEntry(out T arg, Span 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();
+ }
+
+ /// Returns the item to the pool.
+ 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;
+ }
+ }
+ }
+}
diff --git a/Dalamud/Game/Addon/Events/AddonCursorType.cs b/Dalamud/Game/Addon/Events/AddonCursorType.cs
new file mode 100644
index 000000000..83a81582c
--- /dev/null
+++ b/Dalamud/Game/Addon/Events/AddonCursorType.cs
@@ -0,0 +1,97 @@
+namespace Dalamud.Game.Addon.Events;
+
+///
+/// Reimplementation of CursorType.
+///
+public enum AddonCursorType
+{
+ ///
+ /// Arrow.
+ ///
+ Arrow,
+
+ ///
+ /// Boot.
+ ///
+ Boot,
+
+ ///
+ /// Search.
+ ///
+ Search,
+
+ ///
+ /// Chat Pointer.
+ ///
+ ChatPointer,
+
+ ///
+ /// Interact.
+ ///
+ Interact,
+
+ ///
+ /// Attack.
+ ///
+ Attack,
+
+ ///
+ /// Hand.
+ ///
+ Hand,
+
+ ///
+ /// Resizeable Left-Right.
+ ///
+ ResizeWE,
+
+ ///
+ /// Resizeable Up-Down.
+ ///
+ ResizeNS,
+
+ ///
+ /// Resizeable.
+ ///
+ ResizeNWSR,
+
+ ///
+ /// Resizeable 4-way.
+ ///
+ ResizeNESW,
+
+ ///
+ /// Clickable.
+ ///
+ Clickable,
+
+ ///
+ /// Text Input.
+ ///
+ TextInput,
+
+ ///
+ /// Text Click.
+ ///
+ TextClick,
+
+ ///
+ /// Grab.
+ ///
+ Grab,
+
+ ///
+ /// Chat Bubble.
+ ///
+ ChatBubble,
+
+ ///
+ /// No Access.
+ ///
+ NoAccess,
+
+ ///
+ /// Hidden.
+ ///
+ Hidden,
+}
diff --git a/Dalamud/Game/Addon/Events/AddonEventEntry.cs b/Dalamud/Game/Addon/Events/AddonEventEntry.cs
new file mode 100644
index 000000000..a7430acf0
--- /dev/null
+++ b/Dalamud/Game/Addon/Events/AddonEventEntry.cs
@@ -0,0 +1,59 @@
+using Dalamud.Memory;
+using Dalamud.Plugin.Services;
+using FFXIVClientStructs.FFXIV.Component.GUI;
+
+namespace Dalamud.Game.Addon.Events;
+
+///
+/// This class represents a registered event that a plugin registers with a native ui node.
+/// Contains all necessary information to track and clean up events automatically.
+///
+internal unsafe class AddonEventEntry
+{
+ ///
+ /// Name of an invalid addon.
+ ///
+ public const string InvalidAddonName = "NullAddon";
+
+ private string? addonName;
+
+ ///
+ /// Gets the pointer to the addons AtkUnitBase.
+ ///
+ required public nint Addon { get; init; }
+
+ ///
+ /// Gets the name of the addon this args referrers to.
+ ///
+ public string AddonName => this.Addon == nint.Zero ? InvalidAddonName : this.addonName ??= MemoryHelper.ReadString((nint)((AtkUnitBase*)this.Addon)->Name, 0x20);
+
+ ///
+ /// Gets the pointer to the event source.
+ ///
+ required public nint Node { get; init; }
+
+ ///
+ /// Gets the handler that gets called when this event is triggered.
+ ///
+ required public IAddonEventManager.AddonEventHandler Handler { get; init; }
+
+ ///
+ /// Gets the unique id for this event.
+ ///
+ required public uint ParamKey { get; init; }
+
+ ///
+ /// Gets the event type for this event.
+ ///
+ required public AddonEventType EventType { get; init; }
+
+ ///
+ /// Gets the event handle for this event.
+ ///
+ required internal IAddonEventHandle Handle { get; init; }
+
+ ///
+ /// Gets the formatted log string for this AddonEventEntry.
+ ///
+ internal string LogString => $"ParamKey: {this.ParamKey}, Addon: {this.AddonName}, Event: {this.EventType}, GUID: {this.Handle.EventGuid}";
+}
diff --git a/Dalamud/Game/Addon/Events/AddonEventHandle.cs b/Dalamud/Game/Addon/Events/AddonEventHandle.cs
new file mode 100644
index 000000000..fb0e2886c
--- /dev/null
+++ b/Dalamud/Game/Addon/Events/AddonEventHandle.cs
@@ -0,0 +1,19 @@
+namespace Dalamud.Game.Addon.Events;
+
+///
+/// Class that represents a addon event handle.
+///
+public class AddonEventHandle : IAddonEventHandle
+{
+ ///
+ public uint ParamKey { get; init; }
+
+ ///
+ public string AddonName { get; init; } = "NullAddon";
+
+ ///
+ public AddonEventType EventType { get; init; }
+
+ ///
+ public Guid EventGuid { get; init; }
+}
diff --git a/Dalamud/Game/Addon/Events/AddonEventListener.cs b/Dalamud/Game/Addon/Events/AddonEventListener.cs
new file mode 100644
index 000000000..a2498d5a7
--- /dev/null
+++ b/Dalamud/Game/Addon/Events/AddonEventListener.cs
@@ -0,0 +1,97 @@
+using System.Runtime.InteropServices;
+
+using FFXIVClientStructs.FFXIV.Component.GUI;
+
+namespace Dalamud.Game.Addon.Events;
+
+///
+/// Event listener class for managing custom events.
+///
+// Custom event handler tech provided by Pohky, implemented by MidoriKami
+internal unsafe class AddonEventListener : IDisposable
+{
+ private ReceiveEventDelegate? receiveEventDelegate;
+
+ private AtkEventListener* eventListener;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The managed handler to send events to.
+ public AddonEventListener(ReceiveEventDelegate eventHandler)
+ {
+ this.receiveEventDelegate = eventHandler;
+
+ this.eventListener = (AtkEventListener*)Marshal.AllocHGlobal(sizeof(AtkEventListener));
+ this.eventListener->vtbl = (void*)Marshal.AllocHGlobal(sizeof(void*) * 3);
+ this.eventListener->vfunc[0] = (delegate* unmanaged)&NullSub;
+ this.eventListener->vfunc[1] = (delegate* unmanaged)&NullSub;
+ this.eventListener->vfunc[2] = (void*)Marshal.GetFunctionPointerForDelegate(this.receiveEventDelegate);
+ }
+
+ ///
+ /// Delegate for receiving custom events.
+ ///
+ /// Pointer to the event listener.
+ /// Event type.
+ /// Unique Id for this event.
+ /// Event Data.
+ /// Unknown Parameter.
+ public delegate void ReceiveEventDelegate(AtkEventListener* self, AtkEventType eventType, uint eventParam, AtkEvent* eventData, nint unknown);
+
+ ///
+ /// Gets the address of this listener.
+ ///
+ public nint Address => (nint)this.eventListener;
+
+ ///
+ public void Dispose()
+ {
+ if (this.eventListener is null) return;
+
+ Marshal.FreeHGlobal((nint)this.eventListener->vtbl);
+ Marshal.FreeHGlobal((nint)this.eventListener);
+
+ this.eventListener = null;
+ this.receiveEventDelegate = null;
+ }
+
+ ///
+ /// Register an event to this event handler.
+ ///
+ /// Addon that triggers this event.
+ /// Node to attach event to.
+ /// Event type to trigger this event.
+ /// Unique id for this event.
+ public void RegisterEvent(AtkUnitBase* addon, AtkResNode* node, AtkEventType eventType, uint param)
+ {
+ if (node is null) return;
+
+ Service.Get().RunOnFrameworkThread(() =>
+ {
+ node->AddEvent(eventType, param, this.eventListener, (AtkResNode*)addon, false);
+ });
+ }
+
+ ///
+ /// Unregister an event from this event handler.
+ ///
+ /// Node to remove the event from.
+ /// Event type that this event is for.
+ /// Unique id for this event.
+ public void UnregisterEvent(AtkResNode* node, AtkEventType eventType, uint param)
+ {
+ if (node is null) return;
+
+ Service.Get().RunOnFrameworkThread(() =>
+ {
+ node->RemoveEvent(eventType, param, this.eventListener, false);
+ });
+ }
+
+ [UnmanagedCallersOnly]
+ private static void NullSub()
+ {
+ /* do nothing */
+ }
+}
diff --git a/Dalamud/Game/Addon/Events/AddonEventManager.cs b/Dalamud/Game/Addon/Events/AddonEventManager.cs
new file mode 100644
index 000000000..a9b9ef5fa
--- /dev/null
+++ b/Dalamud/Game/Addon/Events/AddonEventManager.cs
@@ -0,0 +1,262 @@
+using System.Collections.Concurrent;
+
+using Dalamud.Game.Addon.Lifecycle;
+using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
+using Dalamud.Hooking;
+using Dalamud.IoC;
+using Dalamud.IoC.Internal;
+using Dalamud.Logging.Internal;
+using Dalamud.Plugin.Internal.Types;
+using Dalamud.Plugin.Services;
+
+using FFXIVClientStructs.FFXIV.Client.UI;
+using FFXIVClientStructs.FFXIV.Component.GUI;
+
+namespace Dalamud.Game.Addon.Events;
+
+///
+/// Service provider for addon event management.
+///
+[InterfaceVersion("1.0")]
+[ServiceManager.EarlyLoadedService]
+internal unsafe class AddonEventManager : IInternalDisposableService
+{
+ ///
+ /// PluginName for Dalamud Internal use.
+ ///
+ public static readonly Guid DalamudInternalKey = Guid.NewGuid();
+
+ private static readonly ModuleLog Log = new("AddonEventManager");
+
+ [ServiceManager.ServiceDependency]
+ private readonly AddonLifecycle addonLifecycle = Service.Get();
+
+ private readonly AddonLifecycleEventListener finalizeEventListener;
+
+ private readonly AddonEventManagerAddressResolver address;
+ private readonly Hook onUpdateCursor;
+
+ private readonly ConcurrentDictionary pluginEventControllers;
+
+ private AddonCursorType? cursorOverride;
+
+ [ServiceManager.ServiceConstructor]
+ private AddonEventManager(TargetSigScanner sigScanner)
+ {
+ this.address = new AddonEventManagerAddressResolver();
+ this.address.Setup(sigScanner);
+
+ this.pluginEventControllers = new ConcurrentDictionary();
+ this.pluginEventControllers.TryAdd(DalamudInternalKey, new PluginEventController());
+
+ this.cursorOverride = null;
+
+ this.onUpdateCursor = Hook.FromAddress(this.address.UpdateCursor, this.UpdateCursorDetour);
+
+ this.finalizeEventListener = new AddonLifecycleEventListener(AddonEvent.PreFinalize, string.Empty, this.OnAddonFinalize);
+ this.addonLifecycle.RegisterListener(this.finalizeEventListener);
+
+ this.onUpdateCursor.Enable();
+ }
+
+ private delegate nint UpdateCursorDelegate(RaptureAtkModule* module);
+
+ ///
+ void IInternalDisposableService.DisposeService()
+ {
+ this.onUpdateCursor.Dispose();
+
+ foreach (var (_, pluginEventController) in this.pluginEventControllers)
+ {
+ pluginEventController.Dispose();
+ }
+
+ this.addonLifecycle.UnregisterListener(this.finalizeEventListener);
+ }
+
+ ///
+ /// Registers an event handler for the specified addon, node, and type.
+ ///
+ /// Unique ID for this plugin.
+ /// The parent addon for this event.
+ /// The node that will trigger this event.
+ /// The event type for this event.
+ /// The handler to call when event is triggered.
+ /// IAddonEventHandle used to remove the event.
+ internal IAddonEventHandle? AddEvent(Guid pluginId, IntPtr atkUnitBase, IntPtr atkResNode, AddonEventType eventType, IAddonEventManager.AddonEventHandler eventHandler)
+ {
+ if (this.pluginEventControllers.TryGetValue(pluginId, out var controller))
+ {
+ return controller.AddEvent(atkUnitBase, atkResNode, eventType, eventHandler);
+ }
+ else
+ {
+ Log.Verbose($"Unable to locate controller for {pluginId}. No event was added.");
+ }
+
+ return null;
+ }
+
+ ///
+ /// Unregisters an event handler with the specified event id and event type.
+ ///
+ /// Unique ID for this plugin.
+ /// The Unique Id for this event.
+ internal void RemoveEvent(Guid pluginId, IAddonEventHandle eventHandle)
+ {
+ if (this.pluginEventControllers.TryGetValue(pluginId, out var controller))
+ {
+ controller.RemoveEvent(eventHandle);
+ }
+ else
+ {
+ Log.Verbose($"Unable to locate controller for {pluginId}. No event was removed.");
+ }
+ }
+
+ ///
+ /// Force the game cursor to be the specified cursor.
+ ///
+ /// Which cursor to use.
+ internal void SetCursor(AddonCursorType cursor) => this.cursorOverride = cursor;
+
+ ///
+ /// Un-forces the game cursor.
+ ///
+ internal void ResetCursor() => this.cursorOverride = null;
+
+ ///
+ /// Adds a new managed event controller if one doesn't already exist for this pluginId.
+ ///
+ /// Unique ID for this plugin.
+ internal void AddPluginEventController(Guid pluginId)
+ {
+ this.pluginEventControllers.GetOrAdd(
+ pluginId,
+ key =>
+ {
+ Log.Verbose($"Creating new PluginEventController for: {key}");
+ return new PluginEventController();
+ });
+ }
+
+ ///
+ /// Removes an existing managed event controller for the specified plugin.
+ ///
+ /// Unique ID for this plugin.
+ internal void RemovePluginEventController(Guid pluginId)
+ {
+ if (this.pluginEventControllers.TryRemove(pluginId, out var controller))
+ {
+ Log.Verbose($"Removing PluginEventController for: {pluginId}");
+ controller.Dispose();
+ }
+ }
+
+ ///
+ /// When an addon finalizes, check it for any registered events, and unregister them.
+ ///
+ /// Event type that triggered this call.
+ /// Addon that triggered this call.
+ private void OnAddonFinalize(AddonEvent eventType, AddonArgs addonInfo)
+ {
+ // It shouldn't be possible for this event to be anything other than PreFinalize.
+ if (eventType != AddonEvent.PreFinalize) return;
+
+ foreach (var pluginList in this.pluginEventControllers)
+ {
+ pluginList.Value.RemoveForAddon(addonInfo.AddonName);
+ }
+ }
+
+ private nint UpdateCursorDetour(RaptureAtkModule* module)
+ {
+ try
+ {
+ var atkStage = AtkStage.GetSingleton();
+
+ if (this.cursorOverride is not null && atkStage is not null)
+ {
+ var cursor = (AddonCursorType)atkStage->AtkCursor.Type;
+ if (cursor != this.cursorOverride)
+ {
+ AtkStage.GetSingleton()->AtkCursor.SetCursorType((AtkCursor.CursorType)this.cursorOverride, 1);
+ }
+
+ return nint.Zero;
+ }
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, "Exception in UpdateCursorDetour.");
+ }
+
+ return this.onUpdateCursor!.Original(module);
+ }
+}
+
+///
+/// Plugin-scoped version of a AddonEventManager service.
+///
+[PluginInterface]
+[InterfaceVersion("1.0")]
+[ServiceManager.ScopedService]
+#pragma warning disable SA1015
+[ResolveVia]
+#pragma warning restore SA1015
+internal class AddonEventManagerPluginScoped : IInternalDisposableService, IAddonEventManager
+{
+ [ServiceManager.ServiceDependency]
+ private readonly AddonEventManager eventManagerService = Service.Get();
+
+ private readonly LocalPlugin plugin;
+
+ private bool isForcingCursor;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Plugin info for the plugin that requested this service.
+ public AddonEventManagerPluginScoped(LocalPlugin plugin)
+ {
+ this.plugin = plugin;
+
+ this.eventManagerService.AddPluginEventController(plugin.Manifest.WorkingPluginId);
+ }
+
+ ///
+ void IInternalDisposableService.DisposeService()
+ {
+ // if multiple plugins force cursors and dispose without un-forcing them then all forces will be cleared.
+ if (this.isForcingCursor)
+ {
+ this.eventManagerService.ResetCursor();
+ }
+
+ this.eventManagerService.RemovePluginEventController(this.plugin.Manifest.WorkingPluginId);
+ }
+
+ ///
+ public IAddonEventHandle? AddEvent(IntPtr atkUnitBase, IntPtr atkResNode, AddonEventType eventType, IAddonEventManager.AddonEventHandler eventHandler)
+ => this.eventManagerService.AddEvent(this.plugin.Manifest.WorkingPluginId, atkUnitBase, atkResNode, eventType, eventHandler);
+
+ ///
+ public void RemoveEvent(IAddonEventHandle eventHandle)
+ => this.eventManagerService.RemoveEvent(this.plugin.Manifest.WorkingPluginId, eventHandle);
+
+ ///
+ public void SetCursor(AddonCursorType cursor)
+ {
+ this.isForcingCursor = true;
+
+ this.eventManagerService.SetCursor(cursor);
+ }
+
+ ///
+ public void ResetCursor()
+ {
+ this.isForcingCursor = false;
+
+ this.eventManagerService.ResetCursor();
+ }
+}
diff --git a/Dalamud/Game/Addon/Events/AddonEventManagerAddressResolver.cs b/Dalamud/Game/Addon/Events/AddonEventManagerAddressResolver.cs
new file mode 100644
index 000000000..927ed87ab
--- /dev/null
+++ b/Dalamud/Game/Addon/Events/AddonEventManagerAddressResolver.cs
@@ -0,0 +1,21 @@
+namespace Dalamud.Game.Addon.Events;
+
+///
+/// AddonEventManager memory address resolver.
+///
+internal class AddonEventManagerAddressResolver : BaseAddressResolver
+{
+ ///
+ /// Gets the address of the AtkModule UpdateCursor method.
+ ///
+ public nint UpdateCursor { get; private set; }
+
+ ///
+ /// Scan for and setup any configured address pointers.
+ ///
+ /// The signature scanner to facilitate setup.
+ protected override void Setup64Bit(ISigScanner scanner)
+ {
+ this.UpdateCursor = scanner.ScanText("48 89 74 24 ?? 48 89 7C 24 ?? 41 56 48 83 EC 20 4C 8B F1 E8 ?? ?? ?? ?? 49 8B CE");
+ }
+}
diff --git a/Dalamud/Game/Addon/Events/AddonEventType.cs b/Dalamud/Game/Addon/Events/AddonEventType.cs
new file mode 100644
index 000000000..100168e22
--- /dev/null
+++ b/Dalamud/Game/Addon/Events/AddonEventType.cs
@@ -0,0 +1,158 @@
+namespace Dalamud.Game.Addon.Events;
+
+///
+/// Reimplementation of AtkEventType.
+///
+public enum AddonEventType : byte
+{
+ ///
+ /// Mouse Down.
+ ///
+ MouseDown = 3,
+
+ ///
+ /// Mouse Up.
+ ///
+ MouseUp = 4,
+
+ ///
+ /// Mouse Move.
+ ///
+ MouseMove = 5,
+
+ ///
+ /// Mouse Over.
+ ///
+ MouseOver = 6,
+
+ ///
+ /// Mouse Out.
+ ///
+ MouseOut = 7,
+
+ ///
+ /// Mouse Click.
+ ///
+ MouseClick = 9,
+
+ ///
+ /// Input Received.
+ ///
+ InputReceived = 12,
+
+ ///
+ /// Focus Start.
+ ///
+ FocusStart = 18,
+
+ ///
+ /// Focus Stop.
+ ///
+ FocusStop = 19,
+
+ ///
+ /// Button Press, sent on MouseDown on Button.
+ ///
+ ButtonPress = 23,
+
+ ///
+ /// Button Release, sent on MouseUp and MouseOut.
+ ///
+ ButtonRelease = 24,
+
+ ///
+ /// Button Click, sent on MouseUp and MouseClick on button.
+ ///
+ ButtonClick = 25,
+
+ ///
+ /// List Item RollOver.
+ ///
+ ListItemRollOver = 33,
+
+ ///
+ /// List Item Roll Out.
+ ///
+ ListItemRollOut = 34,
+
+ ///
+ /// List Item Toggle.
+ ///
+ ListItemToggle = 35,
+
+ ///
+ /// Drag Drop Begin.
+ /// Sent on MouseDown over a draggable icon (will NOT send for a locked icon).
+ ///
+ DragDropBegin = 47,
+
+ ///
+ /// Drag Drop Insert.
+ /// Sent when dropping an icon into a hotbar/inventory slot or similar.
+ ///
+ DragDropInsert = 50,
+
+ ///
+ /// Drag Drop Roll Over.
+ ///
+ DragDropRollOver = 52,
+
+ ///
+ /// Drag Drop Roll Out.
+ ///
+ DragDropRollOut = 53,
+
+ ///
+ /// Drag Drop Discard.
+ /// Sent when dropping an icon into empty screenspace, eg to remove an action from a hotBar.
+ ///
+ DragDropDiscard = 54,
+
+ ///
+ /// Drag Drop Unknown.
+ ///
+ [Obsolete("Use DragDropDiscard")]
+ DragDropUnk54 = 54,
+
+ ///
+ /// Drag Drop Cancel.
+ /// Sent on MouseUp if the cursor has not moved since DragDropBegin, OR on MouseDown over a locked icon.
+ ///
+ DragDropCancel = 55,
+
+ ///
+ /// Drag Drop Unknown.
+ ///
+ [Obsolete("Use DragDropCancel")]
+ DragDropUnk55 = 55,
+
+ ///
+ /// Icon Text Roll Over.
+ ///
+ IconTextRollOver = 56,
+
+ ///
+ /// Icon Text Roll Out.
+ ///
+ IconTextRollOut = 57,
+
+ ///
+ /// Icon Text Click.
+ ///
+ IconTextClick = 58,
+
+ ///
+ /// Window Roll Over.
+ ///
+ WindowRollOver = 67,
+
+ ///
+ /// Window Roll Out.
+ ///
+ WindowRollOut = 68,
+
+ ///
+ /// Window Change Scale.
+ ///
+ WindowChangeScale = 69,
+}
diff --git a/Dalamud/Game/Addon/Events/IAddonEventHandle.cs b/Dalamud/Game/Addon/Events/IAddonEventHandle.cs
new file mode 100644
index 000000000..f9272c92a
--- /dev/null
+++ b/Dalamud/Game/Addon/Events/IAddonEventHandle.cs
@@ -0,0 +1,27 @@
+namespace Dalamud.Game.Addon.Events;
+
+///
+/// Interface representing the data used for managing AddonEvents.
+///
+public interface IAddonEventHandle
+{
+ ///
+ /// Gets the param key associated with this event.
+ ///
+ public uint ParamKey { get; init; }
+
+ ///
+ /// Gets the name of the addon that this event was attached to.
+ ///
+ public string AddonName { get; init; }
+
+ ///
+ /// Gets the event type associated with this handle.
+ ///
+ public AddonEventType EventType { get; init; }
+
+ ///
+ /// Gets the unique ID for this handle.
+ ///
+ public Guid EventGuid { get; init; }
+}
diff --git a/Dalamud/Game/Addon/Events/PluginEventController.cs b/Dalamud/Game/Addon/Events/PluginEventController.cs
new file mode 100644
index 000000000..3ba067a6d
--- /dev/null
+++ b/Dalamud/Game/Addon/Events/PluginEventController.cs
@@ -0,0 +1,201 @@
+using System.Collections.Generic;
+using System.Linq;
+
+using Dalamud.Game.Gui;
+using Dalamud.Logging.Internal;
+using Dalamud.Memory;
+using Dalamud.Plugin.Services;
+using FFXIVClientStructs.FFXIV.Component.GUI;
+
+namespace Dalamud.Game.Addon.Events;
+
+///
+/// Class to manage creating and cleaning up events per-plugin.
+///
+internal unsafe class PluginEventController : IDisposable
+{
+ private static readonly ModuleLog Log = new("AddonEventManager");
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public PluginEventController()
+ {
+ this.EventListener = new AddonEventListener(this.PluginEventListHandler);
+ }
+
+ private AddonEventListener EventListener { get; init; }
+
+ private List Events { get; } = new();
+
+ ///
+ /// Adds a tracked event.
+ ///
+ /// The Parent addon for the event.
+ /// The Node for the event.
+ /// The Event Type.
+ /// The delegate to call when invoking this event.
+ /// IAddonEventHandle used to remove the event.
+ public IAddonEventHandle AddEvent(nint atkUnitBase, nint atkResNode, AddonEventType atkEventType, IAddonEventManager.AddonEventHandler handler)
+ {
+ var node = (AtkResNode*)atkResNode;
+ var addon = (AtkUnitBase*)atkUnitBase;
+ var eventType = (AtkEventType)atkEventType;
+ var eventId = this.GetNextParamKey();
+ var eventGuid = Guid.NewGuid();
+
+ var eventHandle = new AddonEventHandle
+ {
+ AddonName = MemoryHelper.ReadStringNullTerminated((nint)addon->Name),
+ ParamKey = eventId,
+ EventType = atkEventType,
+ EventGuid = eventGuid,
+ };
+
+ var eventEntry = new AddonEventEntry
+ {
+ Addon = atkUnitBase,
+ Handler = handler,
+ Node = atkResNode,
+ EventType = atkEventType,
+ ParamKey = eventId,
+ Handle = eventHandle,
+ };
+
+ Log.Verbose($"Adding Event. {eventEntry.LogString}");
+ this.EventListener.RegisterEvent(addon, node, eventType, eventId);
+ this.Events.Add(eventEntry);
+
+ return eventHandle;
+ }
+
+ ///
+ /// Removes a tracked event, also attempts to un-attach the event from native.
+ ///
+ /// Unique ID of the event to remove.
+ public void RemoveEvent(IAddonEventHandle handle)
+ {
+ if (this.Events.FirstOrDefault(registeredEvent => registeredEvent.Handle == handle) is not { } targetEvent) return;
+
+ Log.Verbose($"Removing Event. {targetEvent.LogString}");
+ this.TryRemoveEventFromNative(targetEvent);
+ this.Events.Remove(targetEvent);
+ }
+
+ ///
+ /// Removes all events attached to the specified addon.
+ ///
+ /// Addon name to remove events from.
+ public void RemoveForAddon(string addonName)
+ {
+ if (this.Events.Where(entry => entry.AddonName == addonName).ToList() is { Count: not 0 } events)
+ {
+ Log.Verbose($"Addon: {addonName} is Finalizing, removing {events.Count} events.");
+
+ foreach (var registeredEvent in events)
+ {
+ this.RemoveEvent(registeredEvent.Handle);
+ }
+ }
+ }
+
+ ///
+ public void Dispose()
+ {
+ foreach (var registeredEvent in this.Events.ToList())
+ {
+ this.RemoveEvent(registeredEvent.Handle);
+ }
+
+ this.EventListener.Dispose();
+ }
+
+ private uint GetNextParamKey()
+ {
+ for (var i = 0u; i < uint.MaxValue; ++i)
+ {
+ if (this.Events.All(registeredEvent => registeredEvent.ParamKey != i)) return i;
+ }
+
+ throw new OverflowException($"uint.MaxValue number of ParamKeys used for this event controller.");
+ }
+
+ ///
+ /// Attempts to remove a tracked event from native UI.
+ /// This method performs several safety checks to only remove events from a still active addon.
+ /// If any of these checks fail, it likely means the native UI already cleaned up the event, and we don't have to worry about them.
+ ///
+ /// Event entry to remove.
+ private void TryRemoveEventFromNative(AddonEventEntry eventEntry)
+ {
+ // Is the eventEntry addon valid?
+ if (eventEntry.AddonName is AddonEventEntry.InvalidAddonName) return;
+
+ // Is an addon with the same name active?
+ var currentAddonPointer = Service.Get().GetAddonByName(eventEntry.AddonName);
+ if (currentAddonPointer == nint.Zero) return;
+
+ // Is our stored addon pointer the same as the active addon pointer?
+ if (currentAddonPointer != eventEntry.Addon) return;
+
+ // Does this addon contain the node this event is for? (by address)
+ var atkUnitBase = (AtkUnitBase*)currentAddonPointer;
+ var nodeFound = false;
+ foreach (var index in Enumerable.Range(0, atkUnitBase->UldManager.NodeListCount))
+ {
+ var node = atkUnitBase->UldManager.NodeList[index];
+
+ // If this node matches our node, then we know our node is still valid.
+ if (node is not null && (nint)node == eventEntry.Node)
+ {
+ nodeFound = true;
+ }
+ }
+
+ // If we didn't find the node, we can't remove the event.
+ if (!nodeFound) return;
+
+ // Does the node have a registered event matching the parameters we have?
+ var atkResNode = (AtkResNode*)eventEntry.Node;
+ var eventType = (AtkEventType)eventEntry.EventType;
+ var currentEvent = atkResNode->AtkEventManager.Event;
+ var eventFound = false;
+ while (currentEvent is not null)
+ {
+ var paramKeyMatches = currentEvent->Param == eventEntry.ParamKey;
+ var eventListenerAddressMatches = (nint)currentEvent->Listener == this.EventListener.Address;
+ var eventTypeMatches = currentEvent->Type == eventType;
+
+ if (paramKeyMatches && eventListenerAddressMatches && eventTypeMatches)
+ {
+ eventFound = true;
+ break;
+ }
+
+ // Move to the next event.
+ currentEvent = currentEvent->NextEvent;
+ }
+
+ // If we didn't find the event, we can't remove the event.
+ if (!eventFound) return;
+
+ // We have a valid addon, valid node, valid event, and valid key.
+ this.EventListener.UnregisterEvent(atkResNode, eventType, eventEntry.ParamKey);
+ }
+
+ private void PluginEventListHandler(AtkEventListener* self, AtkEventType eventType, uint eventParam, AtkEvent* eventData, IntPtr unknown)
+ {
+ try
+ {
+ if (eventData is null) return;
+ if (this.Events.FirstOrDefault(handler => handler.ParamKey == eventParam) is not { } eventInfo) return;
+
+ // We stored the AtkUnitBase* in EventData->Node, and EventData->Target contains the node that triggered the event.
+ eventInfo.Handler.Invoke((AddonEventType)eventType, (nint)eventData->Node, (nint)eventData->Target);
+ }
+ catch (Exception exception)
+ {
+ Log.Error(exception, "Exception in PluginEventList custom event invoke.");
+ }
+ }
+}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs
new file mode 100644
index 000000000..1095202cc
--- /dev/null
+++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs
@@ -0,0 +1,85 @@
+using Dalamud.Memory;
+
+using FFXIVClientStructs.FFXIV.Component.GUI;
+
+namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
+
+///
+/// Base class for AddonLifecycle AddonArgTypes.
+///
+public abstract unsafe class AddonArgs
+{
+ ///
+ /// Constant string representing the name of an addon that is invalid.
+ ///
+ public const string InvalidAddon = "NullAddon";
+
+ private string? addonName;
+ private IntPtr addon;
+
+ ///
+ /// Gets the name of the addon this args referrers to.
+ ///
+ public string AddonName => this.GetAddonName();
+
+ ///
+ /// Gets the pointer to the addons AtkUnitBase.
+ ///
+ public nint Addon
+ {
+ get => this.AddonInternal;
+ init => this.AddonInternal = value;
+ }
+
+ ///
+ /// Gets the type of these args.
+ ///
+ public abstract AddonArgsType Type { get; }
+
+ ///
+ /// Gets or sets the pointer to the addons AtkUnitBase.
+ ///
+ internal nint AddonInternal
+ {
+ get => this.addon;
+ set
+ {
+ this.addon = value;
+
+ // Note: always clear addonName on updating the addon being pointed.
+ // Same address may point to a different addon.
+ this.addonName = null;
+ }
+ }
+
+ ///
+ /// Checks if addon name matches the given span of char.
+ ///
+ /// The name to check.
+ /// Whether it is the case.
+ internal bool IsAddon(ReadOnlySpan name)
+ {
+ if (this.Addon == nint.Zero) return false;
+ if (name.Length is 0 or > 0x20)
+ return false;
+
+ var addonPointer = (AtkUnitBase*)this.Addon;
+ if (addonPointer->Name is null) return false;
+
+ return MemoryHelper.EqualsZeroTerminatedString(name, (nint)addonPointer->Name, null, 0x20);
+ }
+
+ ///
+ /// Helper method for ensuring the name of the addon is valid.
+ ///
+ /// The name of the addon for this object. when invalid.
+ private string GetAddonName()
+ {
+ if (this.Addon == nint.Zero) return InvalidAddon;
+
+ var addonPointer = (AtkUnitBase*)this.Addon;
+ if (addonPointer->Name is null) return InvalidAddon;
+
+ return this.addonName ??= MemoryHelper.ReadString((nint)addonPointer->Name, 0x20);
+ }
+}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs
new file mode 100644
index 000000000..989e11912
--- /dev/null
+++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs
@@ -0,0 +1,24 @@
+namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
+
+///
+/// Addon argument data for Draw events.
+///
+public class AddonDrawArgs : AddonArgs, ICloneable
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ [Obsolete("Not intended for public construction.", false)]
+ public AddonDrawArgs()
+ {
+ }
+
+ ///
+ public override AddonArgsType Type => AddonArgsType.Draw;
+
+ ///
+ public AddonDrawArgs Clone() => (AddonDrawArgs)this.MemberwiseClone();
+
+ ///
+ object ICloneable.Clone() => this.Clone();
+}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs
new file mode 100644
index 000000000..d9401b414
--- /dev/null
+++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs
@@ -0,0 +1,24 @@
+namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
+
+///
+/// Addon argument data for ReceiveEvent events.
+///
+public class AddonFinalizeArgs : AddonArgs, ICloneable
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ [Obsolete("Not intended for public construction.", false)]
+ public AddonFinalizeArgs()
+ {
+ }
+
+ ///
+ public override AddonArgsType Type => AddonArgsType.Finalize;
+
+ ///
+ public AddonFinalizeArgs Clone() => (AddonFinalizeArgs)this.MemberwiseClone();
+
+ ///
+ object ICloneable.Clone() => this.Clone();
+}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs
new file mode 100644
index 000000000..a557b0cb3
--- /dev/null
+++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs
@@ -0,0 +1,44 @@
+namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
+
+///
+/// Addon argument data for ReceiveEvent events.
+///
+public class AddonReceiveEventArgs : AddonArgs, ICloneable
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ [Obsolete("Not intended for public construction.", false)]
+ public AddonReceiveEventArgs()
+ {
+ }
+
+ ///
+ public override AddonArgsType Type => AddonArgsType.ReceiveEvent;
+
+ ///
+ /// Gets or sets the AtkEventType for this event message.
+ ///
+ public byte AtkEventType { get; set; }
+
+ ///
+ /// Gets or sets the event id for this event message.
+ ///
+ public int EventParam { get; set; }
+
+ ///
+ /// Gets or sets the pointer to an AtkEvent for this event message.
+ ///
+ public nint AtkEvent { get; set; }
+
+ ///
+ /// Gets or sets the pointer to a block of data for this event message.
+ ///
+ public nint Data { get; set; }
+
+ ///
+ public AddonReceiveEventArgs Clone() => (AddonReceiveEventArgs)this.MemberwiseClone();
+
+ ///
+ object ICloneable.Clone() => this.Clone();
+}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs
new file mode 100644
index 000000000..6e1b11ead
--- /dev/null
+++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs
@@ -0,0 +1,41 @@
+using FFXIVClientStructs.FFXIV.Component.GUI;
+
+namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
+
+///
+/// Addon argument data for Refresh events.
+///
+public class AddonRefreshArgs : AddonArgs, ICloneable
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ [Obsolete("Not intended for public construction.", false)]
+ public AddonRefreshArgs()
+ {
+ }
+
+ ///
+ public override AddonArgsType Type => AddonArgsType.Refresh;
+
+ ///
+ /// Gets or sets the number of AtkValues.
+ ///
+ public uint AtkValueCount { get; set; }
+
+ ///
+ /// Gets or sets the address of the AtkValue array.
+ ///
+ public nint AtkValues { get; set; }
+
+ ///
+ /// Gets the AtkValues in the form of a span.
+ ///
+ public unsafe Span AtkValueSpan => new(this.AtkValues.ToPointer(), (int)this.AtkValueCount);
+
+ ///
+ public AddonRefreshArgs Clone() => (AddonRefreshArgs)this.MemberwiseClone();
+
+ ///
+ object ICloneable.Clone() => this.Clone();
+}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs
new file mode 100644
index 000000000..26357abb0
--- /dev/null
+++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs
@@ -0,0 +1,34 @@
+namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
+
+///
+/// Addon argument data for OnRequestedUpdate events.
+///
+public class AddonRequestedUpdateArgs : AddonArgs, ICloneable
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ [Obsolete("Not intended for public construction.", false)]
+ public AddonRequestedUpdateArgs()
+ {
+ }
+
+ ///
+ public override AddonArgsType Type => AddonArgsType.RequestedUpdate;
+
+ ///
+ /// Gets or sets the NumberArrayData** for this event.
+ ///
+ public nint NumberArrayData { get; set; }
+
+ ///
+ /// Gets or sets the StringArrayData** for this event.
+ ///
+ public nint StringArrayData { get; set; }
+
+ ///
+ public AddonRequestedUpdateArgs Clone() => (AddonRequestedUpdateArgs)this.MemberwiseClone();
+
+ ///
+ object ICloneable.Clone() => this.Clone();
+}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs
new file mode 100644
index 000000000..19c93ce25
--- /dev/null
+++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs
@@ -0,0 +1,41 @@
+using FFXIVClientStructs.FFXIV.Component.GUI;
+
+namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
+
+///
+/// Addon argument data for Setup events.
+///
+public class AddonSetupArgs : AddonArgs, ICloneable
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ [Obsolete("Not intended for public construction.", false)]
+ public AddonSetupArgs()
+ {
+ }
+
+ ///
+ public override AddonArgsType Type => AddonArgsType.Setup;
+
+ ///
+ /// Gets or sets the number of AtkValues.
+ ///
+ public uint AtkValueCount { get; set; }
+
+ ///
+ /// Gets or sets the address of the AtkValue array.
+ ///
+ public nint AtkValues { get; set; }
+
+ ///
+ /// Gets the AtkValues in the form of a span.
+ ///
+ public unsafe Span AtkValueSpan => new(this.AtkValues.ToPointer(), (int)this.AtkValueCount);
+
+ ///
+ public AddonSetupArgs Clone() => (AddonSetupArgs)this.MemberwiseClone();
+
+ ///
+ object ICloneable.Clone() => this.Clone();
+}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs
new file mode 100644
index 000000000..cc34a7531
--- /dev/null
+++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs
@@ -0,0 +1,38 @@
+namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
+
+///
+/// Addon argument data for Update events.
+///
+public class AddonUpdateArgs : AddonArgs, ICloneable
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ [Obsolete("Not intended for public construction.", false)]
+ public AddonUpdateArgs()
+ {
+ }
+
+ ///
+ public override AddonArgsType Type => AddonArgsType.Update;
+
+ ///
+ /// Gets the time since the last update.
+ ///
+ public float TimeDelta
+ {
+ get => this.TimeDeltaInternal;
+ init => this.TimeDeltaInternal = value;
+ }
+
+ ///
+ /// Gets or sets the time since the last update.
+ ///
+ internal float TimeDeltaInternal { get; set; }
+
+ ///
+ public AddonUpdateArgs Clone() => (AddonUpdateArgs)this.MemberwiseClone();
+
+ ///
+ object ICloneable.Clone() => this.Clone();
+}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs
new file mode 100644
index 000000000..b58b5f4c7
--- /dev/null
+++ b/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs
@@ -0,0 +1,42 @@
+namespace Dalamud.Game.Addon.Lifecycle;
+
+///
+/// Enumeration for available AddonLifecycle arg data.
+///
+public enum AddonArgsType
+{
+ ///
+ /// Contains argument data for Setup.
+ ///
+ Setup,
+
+ ///
+ /// Contains argument data for Update.
+ ///
+ Update,
+
+ ///
+ /// Contains argument data for Draw.
+ ///
+ Draw,
+
+ ///
+ /// Contains argument data for Finalize.
+ ///
+ Finalize,
+
+ ///
+ /// Contains argument data for RequestedUpdate.
+ ///
+ RequestedUpdate,
+
+ ///
+ /// Contains argument data for Refresh.
+ ///
+ Refresh,
+
+ ///
+ /// Contains argument data for ReceiveEvent.
+ ///
+ ReceiveEvent,
+}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs b/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs
new file mode 100644
index 000000000..7cbc93eb2
--- /dev/null
+++ b/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs
@@ -0,0 +1,72 @@
+namespace Dalamud.Game.Addon.Lifecycle;
+
+///
+/// Enumeration for available AddonLifecycle events.
+///
+public enum AddonEvent
+{
+ ///
+ /// Event that is fired before an addon begins it's setup process.
+ ///
+ PreSetup,
+
+ ///
+ /// Event that is fired after an addon has completed it's setup process.
+ ///
+ PostSetup,
+
+ ///
+ /// Event that is fired before an addon begins update.
+ ///
+ PreUpdate,
+
+ ///
+ /// Event that is fired after an addon has completed update.
+ ///
+ PostUpdate,
+
+ ///
+ /// Event that is fired before an addon begins draw.
+ ///
+ PreDraw,
+
+ ///
+ /// Event that is fired after an addon has completed draw.
+ ///
+ PostDraw,
+
+ ///
+ /// Event that is fired before an addon is finalized.
+ ///
+ PreFinalize,
+
+ ///
+ /// Event that is fired before an addon begins a requested update.
+ ///
+ PreRequestedUpdate,
+
+ ///
+ /// Event that is fired after an addon finishes a requested update.
+ ///
+ PostRequestedUpdate,
+
+ ///
+ /// Event that is fired before an addon begins a refresh.
+ ///
+ PreRefresh,
+
+ ///
+ /// Event that is fired after an addon has finished a refresh.
+ ///
+ PostRefresh,
+
+ ///
+ /// Event that is fired before an addon begins processing an event.
+ ///
+ PreReceiveEvent,
+
+ ///
+ /// Event that is fired after an addon has processed an event.
+ ///
+ PostReceiveEvent,
+}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs
new file mode 100644
index 000000000..eefb3b5e9
--- /dev/null
+++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs
@@ -0,0 +1,468 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.CompilerServices;
+
+using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
+using Dalamud.Hooking;
+using Dalamud.Hooking.Internal;
+using Dalamud.IoC;
+using Dalamud.IoC.Internal;
+using Dalamud.Logging.Internal;
+using Dalamud.Memory;
+using Dalamud.Plugin.Services;
+using FFXIVClientStructs.FFXIV.Component.GUI;
+
+namespace Dalamud.Game.Addon.Lifecycle;
+
+///
+/// This class provides events for in-game addon lifecycles.
+///
+[InterfaceVersion("1.0")]
+[ServiceManager.EarlyLoadedService]
+internal unsafe class AddonLifecycle : IInternalDisposableService
+{
+ private static readonly ModuleLog Log = new("AddonLifecycle");
+
+ [ServiceManager.ServiceDependency]
+ private readonly Framework framework = Service.Get();
+
+ [ServiceManager.ServiceDependency]
+ private readonly AddonLifecyclePooledArgs argsPool = Service.Get();
+
+ private readonly nint disallowedReceiveEventAddress;
+
+ private readonly AddonLifecycleAddressResolver address;
+ private readonly CallHook onAddonSetupHook;
+ private readonly CallHook onAddonSetup2Hook;
+ private readonly Hook onAddonFinalizeHook;
+ private readonly CallHook onAddonDrawHook;
+ private readonly CallHook onAddonUpdateHook;
+ private readonly Hook onAddonRefreshHook;
+ private readonly CallHook onAddonRequestedUpdateHook;
+
+ [ServiceManager.ServiceConstructor]
+ private AddonLifecycle(TargetSigScanner sigScanner)
+ {
+ this.address = new AddonLifecycleAddressResolver();
+ this.address.Setup(sigScanner);
+
+ // We want value of the function pointer at vFunc[2]
+ this.disallowedReceiveEventAddress = ((nint*)this.address.AtkEventListener)![2];
+
+ this.onAddonSetupHook = new CallHook(this.address.AddonSetup, this.OnAddonSetup);
+ this.onAddonSetup2Hook = new CallHook(this.address.AddonSetup2, this.OnAddonSetup);
+ this.onAddonFinalizeHook = Hook.FromAddress(this.address.AddonFinalize, this.OnAddonFinalize);
+ this.onAddonDrawHook = new CallHook(this.address.AddonDraw, this.OnAddonDraw);
+ this.onAddonUpdateHook = new CallHook(this.address.AddonUpdate, this.OnAddonUpdate);
+ this.onAddonRefreshHook = Hook.FromAddress(this.address.AddonOnRefresh, this.OnAddonRefresh);
+ this.onAddonRequestedUpdateHook = new CallHook(this.address.AddonOnRequestedUpdate, this.OnRequestedUpdate);
+
+ this.onAddonSetupHook.Enable();
+ this.onAddonSetup2Hook.Enable();
+ this.onAddonFinalizeHook.Enable();
+ this.onAddonDrawHook.Enable();
+ this.onAddonUpdateHook.Enable();
+ this.onAddonRefreshHook.Enable();
+ this.onAddonRequestedUpdateHook.Enable();
+ }
+
+ private delegate void AddonSetupDelegate(AtkUnitBase* addon, uint valueCount, AtkValue* values);
+
+ private delegate void AddonFinalizeDelegate(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase);
+
+ private delegate void AddonDrawDelegate(AtkUnitBase* addon);
+
+ private delegate void AddonUpdateDelegate(AtkUnitBase* addon, float delta);
+
+ private delegate void AddonOnRequestedUpdateDelegate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData);
+
+ private delegate byte AddonOnRefreshDelegate(AtkUnitManager* unitManager, AtkUnitBase* addon, uint valueCount, AtkValue* values);
+
+ ///
+ /// Gets a list of all AddonLifecycle ReceiveEvent Listener Hooks.
+ ///
+ internal List ReceiveEventListeners { get; } = new();
+
+ ///
+ /// Gets a list of all AddonLifecycle Event Listeners.
+ ///
+ internal List EventListeners { get; } = new();
+
+ ///
+ void IInternalDisposableService.DisposeService()
+ {
+ this.onAddonSetupHook.Dispose();
+ this.onAddonSetup2Hook.Dispose();
+ this.onAddonFinalizeHook.Dispose();
+ this.onAddonDrawHook.Dispose();
+ this.onAddonUpdateHook.Dispose();
+ this.onAddonRefreshHook.Dispose();
+ this.onAddonRequestedUpdateHook.Dispose();
+
+ foreach (var receiveEventListener in this.ReceiveEventListeners)
+ {
+ receiveEventListener.Dispose();
+ }
+ }
+
+ ///
+ /// Register a listener for the target event and addon.
+ ///
+ /// The listener to register.
+ internal void RegisterListener(AddonLifecycleEventListener listener)
+ {
+ this.framework.RunOnTick(() =>
+ {
+ this.EventListeners.Add(listener);
+
+ // If we want receive event messages have an already active addon, enable the receive event hook.
+ // If the addon isn't active yet, we'll grab the hook when it sets up.
+ if (listener is { EventType: AddonEvent.PreReceiveEvent or AddonEvent.PostReceiveEvent })
+ {
+ if (this.ReceiveEventListeners.FirstOrDefault(listeners => listeners.AddonNames.Contains(listener.AddonName)) is { } receiveEventListener)
+ {
+ receiveEventListener.Hook?.Enable();
+ }
+ }
+ });
+ }
+
+ ///
+ /// Unregisters the listener from events.
+ ///
+ /// The listener to unregister.
+ internal void UnregisterListener(AddonLifecycleEventListener listener)
+ {
+ this.framework.RunOnTick(() =>
+ {
+ this.EventListeners.Remove(listener);
+
+ // If we are disabling an ReceiveEvent listener, check if we should disable the hook.
+ if (listener is { EventType: AddonEvent.PreReceiveEvent or AddonEvent.PostReceiveEvent })
+ {
+ // Get the ReceiveEvent Listener for this addon
+ if (this.ReceiveEventListeners.FirstOrDefault(listeners => listeners.AddonNames.Contains(listener.AddonName)) is { } receiveEventListener)
+ {
+ // If there are no other listeners listening for this event, disable the hook.
+ if (!this.EventListeners.Any(listeners => listeners.AddonName.Contains(listener.AddonName) && listener.EventType is AddonEvent.PreReceiveEvent or AddonEvent.PostReceiveEvent))
+ {
+ receiveEventListener.Hook?.Disable();
+ }
+ }
+ }
+ });
+ }
+
+ ///
+ /// Invoke listeners for the specified event type.
+ ///
+ /// Event Type.
+ /// AddonArgs.
+ /// What to blame on errors.
+ internal void InvokeListenersSafely(AddonEvent eventType, AddonArgs args, [CallerMemberName] string blame = "")
+ {
+ // Do not use linq; this is a high-traffic function, and more heap allocations avoided, the better.
+ foreach (var listener in this.EventListeners)
+ {
+ if (listener.EventType != eventType)
+ continue;
+
+ // Match on string.empty for listeners that want events for all addons.
+ if (!string.IsNullOrWhiteSpace(listener.AddonName) && !args.IsAddon(listener.AddonName))
+ continue;
+
+ try
+ {
+ listener.FunctionDelegate.Invoke(eventType, args);
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, $"Exception in {blame} during {eventType} invoke.");
+ }
+ }
+ }
+
+ private void RegisterReceiveEventHook(AtkUnitBase* addon)
+ {
+ // Hook the addon's ReceiveEvent function here, but only enable the hook if we have an active listener.
+ // Disallows hooking the core internal event handler.
+ var addonName = MemoryHelper.ReadStringNullTerminated((nint)addon->Name);
+ var receiveEventAddress = (nint)addon->VTable->ReceiveEvent;
+ if (receiveEventAddress != this.disallowedReceiveEventAddress)
+ {
+ // If we have a ReceiveEvent listener already made for this hook address, add this addon's name to that handler.
+ if (this.ReceiveEventListeners.FirstOrDefault(listener => listener.HookAddress == receiveEventAddress) is { } existingListener)
+ {
+ if (!existingListener.AddonNames.Contains(addonName))
+ {
+ existingListener.AddonNames.Add(addonName);
+ }
+ }
+
+ // Else, we have an addon that we don't have the ReceiveEvent for yet, make it.
+ else
+ {
+ this.ReceiveEventListeners.Add(new AddonLifecycleReceiveEventListener(this, addonName, receiveEventAddress));
+ }
+
+ // If we have an active listener for this addon already, we need to activate this hook.
+ if (this.EventListeners.Any(listener => (listener.EventType is AddonEvent.PostReceiveEvent or AddonEvent.PreReceiveEvent) && listener.AddonName == addonName))
+ {
+ if (this.ReceiveEventListeners.FirstOrDefault(listener => listener.AddonNames.Contains(addonName)) is { } receiveEventListener)
+ {
+ receiveEventListener.Hook?.Enable();
+ }
+ }
+ }
+ }
+
+ private void UnregisterReceiveEventHook(string addonName)
+ {
+ // Remove this addons ReceiveEvent Registration
+ if (this.ReceiveEventListeners.FirstOrDefault(listener => listener.AddonNames.Contains(addonName)) is { } eventListener)
+ {
+ eventListener.AddonNames.Remove(addonName);
+
+ // If there are no more listeners let's remove and dispose.
+ if (eventListener.AddonNames.Count is 0)
+ {
+ this.ReceiveEventListeners.Remove(eventListener);
+ eventListener.Dispose();
+ }
+ }
+ }
+
+ private void OnAddonSetup(AtkUnitBase* addon, uint valueCount, AtkValue* values)
+ {
+ try
+ {
+ this.RegisterReceiveEventHook(addon);
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, "Exception in OnAddonSetup ReceiveEvent Registration.");
+ }
+
+ using var returner = this.argsPool.Rent(out AddonSetupArgs arg);
+ arg.AddonInternal = (nint)addon;
+ arg.AtkValueCount = valueCount;
+ arg.AtkValues = (nint)values;
+ this.InvokeListenersSafely(AddonEvent.PreSetup, arg);
+ valueCount = arg.AtkValueCount;
+ values = (AtkValue*)arg.AtkValues;
+
+ try
+ {
+ addon->OnSetup(valueCount, values);
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, "Caught exception when calling original AddonSetup. This may be a bug in the game or another plugin hooking this method.");
+ }
+
+ this.InvokeListenersSafely(AddonEvent.PostSetup, arg);
+ }
+
+ private void OnAddonFinalize(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase)
+ {
+ try
+ {
+ var addonName = MemoryHelper.ReadStringNullTerminated((nint)atkUnitBase[0]->Name);
+ this.UnregisterReceiveEventHook(addonName);
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, "Exception in OnAddonFinalize ReceiveEvent Removal.");
+ }
+
+ using var returner = this.argsPool.Rent(out AddonFinalizeArgs arg);
+ arg.AddonInternal = (nint)atkUnitBase[0];
+ this.InvokeListenersSafely(AddonEvent.PreFinalize, arg);
+
+ try
+ {
+ this.onAddonFinalizeHook.Original(unitManager, atkUnitBase);
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, "Caught exception when calling original AddonFinalize. This may be a bug in the game or another plugin hooking this method.");
+ }
+ }
+
+ private void OnAddonDraw(AtkUnitBase* addon)
+ {
+ using var returner = this.argsPool.Rent(out AddonDrawArgs arg);
+ arg.AddonInternal = (nint)addon;
+ this.InvokeListenersSafely(AddonEvent.PreDraw, arg);
+
+ try
+ {
+ addon->Draw();
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, "Caught exception when calling original AddonDraw. This may be a bug in the game or another plugin hooking this method.");
+ }
+
+ this.InvokeListenersSafely(AddonEvent.PostDraw, arg);
+ }
+
+ private void OnAddonUpdate(AtkUnitBase* addon, float delta)
+ {
+ using var returner = this.argsPool.Rent(out AddonUpdateArgs arg);
+ arg.AddonInternal = (nint)addon;
+ arg.TimeDeltaInternal = delta;
+ this.InvokeListenersSafely(AddonEvent.PreUpdate, arg);
+
+ try
+ {
+ addon->Update(delta);
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, "Caught exception when calling original AddonUpdate. This may be a bug in the game or another plugin hooking this method.");
+ }
+
+ this.InvokeListenersSafely(AddonEvent.PostUpdate, arg);
+ }
+
+ private byte OnAddonRefresh(AtkUnitManager* atkUnitManager, AtkUnitBase* addon, uint valueCount, AtkValue* values)
+ {
+ byte result = 0;
+
+ using var returner = this.argsPool.Rent(out AddonRefreshArgs arg);
+ arg.AddonInternal = (nint)addon;
+ arg.AtkValueCount = valueCount;
+ arg.AtkValues = (nint)values;
+ this.InvokeListenersSafely(AddonEvent.PreRefresh, arg);
+ valueCount = arg.AtkValueCount;
+ values = (AtkValue*)arg.AtkValues;
+
+ try
+ {
+ result = this.onAddonRefreshHook.Original(atkUnitManager, addon, valueCount, values);
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, "Caught exception when calling original AddonRefresh. This may be a bug in the game or another plugin hooking this method.");
+ }
+
+ this.InvokeListenersSafely(AddonEvent.PostRefresh, arg);
+ return result;
+ }
+
+ private void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData)
+ {
+ using var returner = this.argsPool.Rent(out AddonRequestedUpdateArgs arg);
+ arg.AddonInternal = (nint)addon;
+ arg.NumberArrayData = (nint)numberArrayData;
+ arg.StringArrayData = (nint)stringArrayData;
+ this.InvokeListenersSafely(AddonEvent.PreRequestedUpdate, arg);
+ numberArrayData = (NumberArrayData**)arg.NumberArrayData;
+ stringArrayData = (StringArrayData**)arg.StringArrayData;
+
+ try
+ {
+ addon->OnUpdate(numberArrayData, stringArrayData);
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, "Caught exception when calling original AddonRequestedUpdate. This may be a bug in the game or another plugin hooking this method.");
+ }
+
+ this.InvokeListenersSafely(AddonEvent.PostRequestedUpdate, arg);
+ }
+}
+
+///
+/// Plugin-scoped version of a AddonLifecycle service.
+///
+[PluginInterface]
+[InterfaceVersion("1.0")]
+[ServiceManager.ScopedService]
+#pragma warning disable SA1015
+[ResolveVia]
+#pragma warning restore SA1015
+internal class AddonLifecyclePluginScoped : IInternalDisposableService, IAddonLifecycle
+{
+ [ServiceManager.ServiceDependency]
+ private readonly AddonLifecycle addonLifecycleService = Service.Get();
+
+ private readonly List eventListeners = new();
+
+ ///
+ void IInternalDisposableService.DisposeService()
+ {
+ foreach (var listener in this.eventListeners)
+ {
+ this.addonLifecycleService.UnregisterListener(listener);
+ }
+ }
+
+ ///
+ public void RegisterListener(AddonEvent eventType, IEnumerable addonNames, IAddonLifecycle.AddonEventDelegate handler)
+ {
+ foreach (var addonName in addonNames)
+ {
+ this.RegisterListener(eventType, addonName, handler);
+ }
+ }
+
+ ///
+ public void RegisterListener(AddonEvent eventType, string addonName, IAddonLifecycle.AddonEventDelegate handler)
+ {
+ var listener = new AddonLifecycleEventListener(eventType, addonName, handler);
+ this.eventListeners.Add(listener);
+ this.addonLifecycleService.RegisterListener(listener);
+ }
+
+ ///
+ public void RegisterListener(AddonEvent eventType, IAddonLifecycle.AddonEventDelegate handler)
+ {
+ this.RegisterListener(eventType, string.Empty, handler);
+ }
+
+ ///
+ public void UnregisterListener(AddonEvent eventType, IEnumerable addonNames, IAddonLifecycle.AddonEventDelegate? handler = null)
+ {
+ foreach (var addonName in addonNames)
+ {
+ this.UnregisterListener(eventType, addonName, handler);
+ }
+ }
+
+ ///
+ public void UnregisterListener(AddonEvent eventType, string addonName, IAddonLifecycle.AddonEventDelegate? handler = null)
+ {
+ this.eventListeners.RemoveAll(entry =>
+ {
+ if (entry.EventType != eventType) return false;
+ if (entry.AddonName != addonName) return false;
+ if (handler is not null && entry.FunctionDelegate != handler) return false;
+
+ this.addonLifecycleService.UnregisterListener(entry);
+ return true;
+ });
+ }
+
+ ///
+ public void UnregisterListener(AddonEvent eventType, IAddonLifecycle.AddonEventDelegate? handler = null)
+ {
+ this.UnregisterListener(eventType, string.Empty, handler);
+ }
+
+ ///
+ public void UnregisterListener(params IAddonLifecycle.AddonEventDelegate[] handlers)
+ {
+ foreach (var handler in handlers)
+ {
+ this.eventListeners.RemoveAll(entry =>
+ {
+ if (entry.FunctionDelegate != handler) return false;
+
+ this.addonLifecycleService.UnregisterListener(entry);
+ return true;
+ });
+ }
+ }
+}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs
new file mode 100644
index 000000000..df25d0a46
--- /dev/null
+++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs
@@ -0,0 +1,68 @@
+namespace Dalamud.Game.Addon.Lifecycle;
+
+///
+/// AddonLifecycleService memory address resolver.
+///
+internal class AddonLifecycleAddressResolver : BaseAddressResolver
+{
+ ///
+ /// Gets the address of the addon setup hook invoked by the AtkUnitManager.
+ /// There are two callsites for this vFunc, we need to hook both of them to catch both normal UI and special UI cases like dialogue.
+ /// This is called for a majority of all addon OnSetup's.
+ ///
+ public nint AddonSetup { get; private set; }
+
+ ///
+ /// Gets the address of the other addon setup hook invoked by the AtkUnitManager.
+ /// There are two callsites for this vFunc, we need to hook both of them to catch both normal UI and special UI cases like dialogue.
+ /// This seems to be called rarely for specific addons.
+ ///
+ public nint AddonSetup2 { get; private set; }
+
+ ///
+ /// Gets the address of the addon finalize hook invoked by the AtkUnitManager.
+ ///
+ public nint AddonFinalize { get; private set; }
+
+ ///
+ /// Gets the address of the addon draw hook invoked by virtual function call.
+ ///
+ public nint AddonDraw { get; private set; }
+
+ ///
+ /// Gets the address of the addon update hook invoked by virtual function call.
+ ///
+ public nint AddonUpdate { get; private set; }
+
+ ///
+ /// Gets the address of the addon onRequestedUpdate hook invoked by virtual function call.
+ ///
+ public nint AddonOnRequestedUpdate { get; private set; }
+
+ ///
+ /// Gets the address of AtkUnitManager_vf10 which triggers addon onRefresh.
+ ///
+ public nint AddonOnRefresh { get; private set; }
+
+ ///
+ /// Gets the address of AtkEventListener base vTable.
+ /// This is used to ensure that we do not hook ReceiveEvents that resolve back to the internal handler.
+ ///
+ public nint AtkEventListener { get; private set; }
+
+ ///
+ /// Scan for and setup any configured address pointers.
+ ///
+ /// The signature scanner to facilitate setup.
+ protected override void Setup64Bit(ISigScanner sig)
+ {
+ this.AddonSetup = sig.ScanText("FF 90 ?? ?? ?? ?? 48 8B 93 ?? ?? ?? ?? 80 8B");
+ this.AddonSetup2 = sig.ScanText("FF 90 ?? ?? ?? ?? 48 8B 03 48 8B CB 80 8B");
+ this.AddonFinalize = sig.ScanText("E8 ?? ?? ?? ?? 48 8B 7C 24 ?? 41 8B C6");
+ this.AddonDraw = sig.ScanText("FF 90 ?? ?? ?? ?? 83 EB 01 79 C1");
+ this.AddonUpdate = sig.ScanText("FF 90 ?? ?? ?? ?? 40 88 AF");
+ this.AddonOnRequestedUpdate = sig.ScanText("FF 90 98 01 00 00 48 8B 5C 24 30 48 83 C4 20");
+ this.AddonOnRefresh = sig.ScanText("48 89 5C 24 08 57 48 83 EC 20 41 8B F8 48 8B DA");
+ this.AtkEventListener = sig.GetStaticAddressFromSig("4C 8D 3D ?? ?? ?? ?? 49 8D 8E");
+ }
+}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleEventListener.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleEventListener.cs
new file mode 100644
index 000000000..6464a1edd
--- /dev/null
+++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleEventListener.cs
@@ -0,0 +1,38 @@
+using Dalamud.Plugin.Services;
+
+namespace Dalamud.Game.Addon.Lifecycle;
+
+///
+/// This class is a helper for tracking and invoking listener delegates.
+///
+internal class AddonLifecycleEventListener
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Event type to listen for.
+ /// Addon name to listen for.
+ /// Delegate to invoke.
+ internal AddonLifecycleEventListener(AddonEvent eventType, string addonName, IAddonLifecycle.AddonEventDelegate functionDelegate)
+ {
+ this.EventType = eventType;
+ this.AddonName = addonName;
+ this.FunctionDelegate = functionDelegate;
+ }
+
+ ///
+ /// Gets the name of the addon this listener is looking for.
+ /// string.Empty if it wants to be called for any addon.
+ ///
+ public string AddonName { get; init; }
+
+ ///
+ /// Gets the event type this listener is looking for.
+ ///
+ public AddonEvent EventType { get; init; }
+
+ ///
+ /// Gets the delegate this listener invokes.
+ ///
+ public IAddonLifecycle.AddonEventDelegate FunctionDelegate { get; init; }
+}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleReceiveEventListener.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleReceiveEventListener.cs
new file mode 100644
index 000000000..fd3b5d79d
--- /dev/null
+++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleReceiveEventListener.cs
@@ -0,0 +1,104 @@
+using System.Collections.Generic;
+
+using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
+using Dalamud.Hooking;
+using Dalamud.Logging.Internal;
+using Dalamud.Memory;
+using FFXIVClientStructs.FFXIV.Component.GUI;
+
+namespace Dalamud.Game.Addon.Lifecycle;
+
+///
+/// This class is a helper for tracking and invoking listener delegates for Addon_OnReceiveEvent.
+/// Multiple addons may use the same ReceiveEvent function, this helper makes sure that those addon events are handled properly.
+///
+internal unsafe class AddonLifecycleReceiveEventListener : IDisposable
+{
+ private static readonly ModuleLog Log = new("AddonLifecycle");
+
+ [ServiceManager.ServiceDependency]
+ private readonly AddonLifecyclePooledArgs argsPool = Service.Get();
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// AddonLifecycle service instance.
+ /// Initial Addon Requesting this listener.
+ /// Address of Addon's ReceiveEvent function.
+ internal AddonLifecycleReceiveEventListener(AddonLifecycle service, string addonName, nint receiveEventAddress)
+ {
+ this.AddonLifecycle = service;
+ this.AddonNames = new List { addonName };
+ this.Hook = Hook.FromAddress(receiveEventAddress, this.OnReceiveEvent);
+ }
+
+ ///
+ /// Addon Receive Event Function delegate.
+ ///
+ /// Addon Pointer.
+ /// Event Type.
+ /// Unique Event ID.
+ /// Event Data.
+ /// Unknown.
+ public delegate void AddonReceiveEventDelegate(AtkUnitBase* addon, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, nint a5);
+
+ ///
+ /// Gets the list of addons that use this receive event hook.
+ ///
+ public List AddonNames { get; init; }
+
+ ///
+ /// Gets the address of the registered hook.
+ ///
+ public nint HookAddress => this.Hook?.Address ?? nint.Zero;
+
+ ///
+ /// Gets the contained hook for these addons.
+ ///
+ public Hook? Hook { get; init; }
+
+ ///
+ /// Gets or sets the Reference to AddonLifecycle service instance.
+ ///
+ private AddonLifecycle AddonLifecycle { get; set; }
+
+ ///
+ public void Dispose()
+ {
+ this.Hook?.Dispose();
+ }
+
+ private void OnReceiveEvent(AtkUnitBase* addon, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, nint data)
+ {
+ // Check that we didn't get here through a call to another addons handler.
+ var addonName = MemoryHelper.ReadString((nint)addon->Name, 0x20);
+ if (!this.AddonNames.Contains(addonName))
+ {
+ this.Hook!.Original(addon, eventType, eventParam, atkEvent, data);
+ return;
+ }
+
+ using var returner = this.argsPool.Rent(out AddonReceiveEventArgs arg);
+ arg.AddonInternal = (nint)addon;
+ arg.AtkEventType = (byte)eventType;
+ arg.EventParam = eventParam;
+ arg.AtkEvent = (IntPtr)atkEvent;
+ arg.Data = data;
+ this.AddonLifecycle.InvokeListenersSafely(AddonEvent.PreReceiveEvent, arg);
+ eventType = (AtkEventType)arg.AtkEventType;
+ eventParam = arg.EventParam;
+ atkEvent = (AtkEvent*)arg.AtkEvent;
+ data = arg.Data;
+
+ try
+ {
+ this.Hook!.Original(addon, eventType, eventParam, atkEvent, data);
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, "Caught exception when calling original AddonReceiveEvent. This may be a bug in the game or another plugin hooking this method.");
+ }
+
+ this.AddonLifecycle.InvokeListenersSafely(AddonEvent.PostReceiveEvent, arg);
+ }
+}
diff --git a/Dalamud/Game/BaseAddressResolver.cs b/Dalamud/Game/BaseAddressResolver.cs
index 24e7dffe8..7a455aea0 100644
--- a/Dalamud/Game/BaseAddressResolver.cs
+++ b/Dalamud/Game/BaseAddressResolver.cs
@@ -18,24 +18,15 @@ public abstract class BaseAddressResolver
public static Dictionary> DebugScannedValues { get; } = new();
///
- /// Gets or sets a value indicating whether the resolver has successfully run or .
+ /// Gets or sets a value indicating whether the resolver has successfully run or .
///
protected bool IsResolved { get; set; }
- ///
- /// Setup the resolver, calling the appropriate method based on the process architecture,
- /// using the default SigScanner.
- ///
- /// For plugins. Not intended to be called from Dalamud Service{T} constructors.
- ///
- [UsedImplicitly]
- public void Setup() => this.Setup(Service.Get());
-
///
/// Setup the resolver, calling the appropriate method based on the process architecture.
///
/// The SigScanner instance.
- public void Setup(SigScanner scanner)
+ public void Setup(ISigScanner scanner)
{
// Because C# don't allow to call virtual function while in ctor
// we have to do this shit :\
@@ -92,7 +83,7 @@ public abstract class BaseAddressResolver
/// Setup the resolver by finding any necessary memory addresses.
///
/// The SigScanner instance.
- protected virtual void Setup32Bit(SigScanner scanner)
+ protected virtual void Setup32Bit(ISigScanner scanner)
{
throw new NotSupportedException("32 bit version is not supported.");
}
@@ -101,7 +92,7 @@ public abstract class BaseAddressResolver
/// Setup the resolver by finding any necessary memory addresses.
///
/// The SigScanner instance.
- protected virtual void Setup64Bit(SigScanner scanner)
+ protected virtual void Setup64Bit(ISigScanner scanner)
{
throw new NotSupportedException("64 bit version is not supported.");
}
@@ -110,7 +101,7 @@ public abstract class BaseAddressResolver
/// Setup the resolver by finding any necessary memory addresses.
///
/// The SigScanner instance.
- protected virtual void SetupInternal(SigScanner scanner)
+ protected virtual void SetupInternal(ISigScanner scanner)
{
// Do nothing
}
diff --git a/Dalamud/Game/ChatHandlers.cs b/Dalamud/Game/ChatHandlers.cs
index ed69b7bbe..5dd6ed3ba 100644
--- a/Dalamud/Game/ChatHandlers.cs
+++ b/Dalamud/Game/ChatHandlers.cs
@@ -1,8 +1,8 @@
-using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
+using System.Threading;
using System.Threading.Tasks;
using CheapLoc;
@@ -11,24 +11,22 @@ using Dalamud.Game.Gui;
using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads;
+using Dalamud.Interface.ImGuiNotification.Internal;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Internal.Notifications;
using Dalamud.Interface.Internal.Windows;
-using Dalamud.IoC;
-using Dalamud.IoC.Internal;
+using Dalamud.Interface.Internal.Windows.PluginInstaller;
+using Dalamud.Logging.Internal;
using Dalamud.Plugin.Internal;
using Dalamud.Utility;
-using Serilog;
namespace Dalamud.Game;
///
/// Chat events and public helper functions.
///
-[PluginInterface]
-[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
-public class ChatHandlers : IServiceType
+internal class ChatHandlers : IServiceType
{
// private static readonly Dictionary UnicodeToDiscordEmojiDict = new()
// {
@@ -64,6 +62,8 @@ public class ChatHandlers : IServiceType
// { XivChatType.Echo, Color.Gray },
// };
+ private static readonly ModuleLog Log = new("CHATHANDLER");
+
private readonly Regex rmtRegex = new(
@"4KGOLD|We have sufficient stock|VPK\.OM|[Gg]il for free|[Gg]il [Cc]heap|5GOLD|www\.so9\.com|Fast & Convenient|Cheap & Safety Guarantee|【Code|A O A U E|igfans|4KGOLD\.COM|Cheapest Gil with|pvp and bank on google|Selling Cheap GIL|ff14mogstation\.com|Cheap Gil 1000k|gilsforyou|server 1000K =|gils_selling|E A S Y\.C O M|bonus code|mins delivery guarantee|Sell cheap|Salegm\.com|cheap Mog|Off Code:|FF14Mog.com|使用する5%オ|[Oo][Ff][Ff] [Cc]ode( *)[:;]|offers Fantasia",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
@@ -106,11 +106,15 @@ public class ChatHandlers : IServiceType
private readonly DalamudLinkPayload openInstallerWindowLink;
+ [ServiceManager.ServiceDependency]
+ private readonly Dalamud dalamud = Service.Get();
+
[ServiceManager.ServiceDependency]
private readonly DalamudConfiguration configuration = Service.Get();
private bool hasSeenLoadingMsg;
private bool startedAutoUpdatingPlugins;
+ private CancellationTokenSource deferredAutoUpdateCts = new();
[ServiceManager.ServiceConstructor]
private ChatHandlers(ChatGui chatGui)
@@ -120,7 +124,7 @@ public class ChatHandlers : IServiceType
this.openInstallerWindowLink = chatGui.AddChatLinkHandler("Dalamud", 1001, (i, m) =>
{
- Service.GetNullable()?.OpenPluginInstaller();
+ Service.GetNullable()?.OpenPluginInstallerTo(PluginInstallerWindow.PluginInstallerOpenKind.InstalledPlugins);
});
}
@@ -134,22 +138,6 @@ public class ChatHandlers : IServiceType
///
public bool IsAutoUpdateComplete { get; private set; }
- ///
- /// Convert a TextPayload to SeString and wrap in italics payloads.
- ///
- /// Text to convert.
- /// SeString payload of italicized text.
- public static SeString MakeItalics(string text)
- => MakeItalics(new TextPayload(text));
-
- ///
- /// Convert a TextPayload to SeString and wrap in italics payloads.
- ///
- /// Text to convert.
- /// SeString payload of italicized text.
- public static SeString MakeItalics(TextPayload text)
- => new(EmphasisItalicPayload.ItalicsOn, text, EmphasisItalicPayload.ItalicsOff);
-
private void OnCheckMessageHandled(XivChatType type, uint senderid, ref SeString sender, ref SeString message, ref bool isHandled)
{
var textVal = message.TextValue;
@@ -178,21 +166,23 @@ public class ChatHandlers : IServiceType
private void OnChatMessage(XivChatType type, uint senderId, ref SeString sender, ref SeString message, ref bool isHandled)
{
- var startInfo = Service.Get();
var clientState = Service.GetNullable();
if (clientState == null)
return;
- if (type == XivChatType.Notice && !this.hasSeenLoadingMsg)
- this.PrintWelcomeMessage();
+ if (type == XivChatType.Notice)
+ {
+ if (!this.hasSeenLoadingMsg)
+ this.PrintWelcomeMessage();
+
+ if (!this.startedAutoUpdatingPlugins)
+ this.AutoUpdatePluginsWithRetry();
+ }
// For injections while logged in
if (clientState.LocalPlayer != null && clientState.TerritoryType == 0 && !this.hasSeenLoadingMsg)
this.PrintWelcomeMessage();
- if (!this.startedAutoUpdatingPlugins)
- this.AutoUpdatePlugins();
-
#if !DEBUG && false
if (!this.hasSeenLoadingMsg)
return;
@@ -200,7 +190,7 @@ public class ChatHandlers : IServiceType
if (type == XivChatType.RetainerSale)
{
- foreach (var regex in this.retainerSaleRegexes[startInfo.Language])
+ foreach (var regex in this.retainerSaleRegexes[(ClientLanguage)this.dalamud.StartInfo.Language])
{
var matchInfo = regex.Match(message.TextValue);
@@ -258,22 +248,21 @@ public class ChatHandlers : IServiceType
{
foreach (var plugin in pluginManager.InstalledPlugins.OrderBy(plugin => plugin.Name).Where(x => x.IsLoaded))
{
- chatGui.Print(string.Format(Loc.Localize("DalamudPluginLoaded", " 》 {0} v{1} loaded."), plugin.Name, plugin.Manifest.AssemblyVersion));
+ chatGui.Print(string.Format(Loc.Localize("DalamudPluginLoaded", " 》 {0} v{1} loaded."), plugin.Name, plugin.EffectiveVersion));
}
}
if (string.IsNullOrEmpty(this.configuration.LastVersion) || !assemblyVersion.StartsWith(this.configuration.LastVersion))
{
- chatGui.PrintChat(new XivChatEntry
+ chatGui.Print(new XivChatEntry
{
Message = Loc.Localize("DalamudUpdated", "Dalamud has been updated successfully! Please check the discord for a full changelog."),
Type = XivChatType.Notice,
});
- if (string.IsNullOrEmpty(this.configuration.LastChangelogMajorMinor) || (!ChangelogWindow.WarrantsChangelogForMajorMinor.StartsWith(this.configuration.LastChangelogMajorMinor) && assemblyVersion.StartsWith(ChangelogWindow.WarrantsChangelogForMajorMinor)))
+ if (ChangelogWindow.WarrantsChangelog())
{
dalamudInterface.OpenChangelogWindow();
- this.configuration.LastChangelogMajorMinor = ChangelogWindow.WarrantsChangelogForMajorMinor;
}
this.configuration.LastVersion = assemblyVersion;
@@ -283,24 +272,42 @@ public class ChatHandlers : IServiceType
this.hasSeenLoadingMsg = true;
}
- private void AutoUpdatePlugins()
+ private void AutoUpdatePluginsWithRetry()
+ {
+ var firstAttempt = this.AutoUpdatePlugins();
+ if (!firstAttempt)
+ {
+ Task.Run(() =>
+ {
+ Task.Delay(30_000, this.deferredAutoUpdateCts.Token);
+ this.AutoUpdatePlugins();
+ });
+ }
+ }
+
+ private bool AutoUpdatePlugins()
{
var chatGui = Service.GetNullable();
var pluginManager = Service.GetNullable();
var notifications = Service.GetNullable();
if (chatGui == null || pluginManager == null || notifications == null)
- return;
+ {
+ Log.Warning("Aborting auto-update because a required service was not loaded.");
+ return false;
+ }
if (!pluginManager.ReposReady || !pluginManager.InstalledPlugins.Any() || !pluginManager.AvailablePlugins.Any())
{
// Plugins aren't ready yet.
// TODO: We should retry. This sucks, because it means we won't ever get here again until another notice.
- return;
+ Log.Warning("Aborting auto-update because plugins weren't loaded or ready.");
+ return false;
}
this.startedAutoUpdatingPlugins = true;
+ Log.Debug("Beginning plugin auto-update process...");
Task.Run(() => pluginManager.UpdatePluginsAsync(true, !this.configuration.AutoUpdatePlugins, true)).ContinueWith(task =>
{
this.IsAutoUpdateComplete = true;
@@ -321,7 +328,7 @@ public class ChatHandlers : IServiceType
}
else
{
- chatGui.PrintChat(new XivChatEntry
+ chatGui.Print(new XivChatEntry
{
Message = new SeString(new List()
{
@@ -339,5 +346,7 @@ public class ChatHandlers : IServiceType
}
}
});
+
+ return true;
}
}
diff --git a/Dalamud/Game/ClientState/Aetherytes/AetheryteList.cs b/Dalamud/Game/ClientState/Aetherytes/AetheryteList.cs
index 17b468d70..e6af6e1df 100644
--- a/Dalamud/Game/ClientState/Aetherytes/AetheryteList.cs
+++ b/Dalamud/Game/ClientState/Aetherytes/AetheryteList.cs
@@ -18,7 +18,7 @@ namespace Dalamud.Game.ClientState.Aetherytes;
#pragma warning disable SA1015
[ResolveVia]
#pragma warning restore SA1015
-public sealed unsafe partial class AetheryteList : IServiceType, IAetheryteList
+internal sealed unsafe partial class AetheryteList : IServiceType, IAetheryteList
{
[ServiceManager.ServiceDependency]
private readonly ClientState clientState = Service.Get();
@@ -78,7 +78,7 @@ public sealed unsafe partial class AetheryteList : IServiceType, IAetheryteList
///
/// This collection represents the list of available Aetherytes in the Teleport window.
///
-public sealed partial class AetheryteList
+internal sealed partial class AetheryteList
{
///
public int Count => this.Length;
diff --git a/Dalamud/Game/ClientState/Buddy/BuddyList.cs b/Dalamud/Game/ClientState/Buddy/BuddyList.cs
index dc2cb9fae..5d0098187 100644
--- a/Dalamud/Game/ClientState/Buddy/BuddyList.cs
+++ b/Dalamud/Game/ClientState/Buddy/BuddyList.cs
@@ -20,7 +20,7 @@ namespace Dalamud.Game.ClientState.Buddy;
#pragma warning disable SA1015
[ResolveVia]
#pragma warning restore SA1015
-public sealed partial class BuddyList : IServiceType, IBuddyList
+internal sealed partial class BuddyList : IServiceType, IBuddyList
{
private const uint InvalidObjectID = 0xE0000000;
@@ -55,18 +55,6 @@ public sealed partial class BuddyList : IServiceType, IBuddyList
}
}
- ///
- /// Gets a value indicating whether the local player's companion is present.
- ///
- [Obsolete("Use CompanionBuddy != null", false)]
- public bool CompanionBuddyPresent => this.CompanionBuddy != null;
-
- ///
- /// Gets a value indicating whether the local player's pet is present.
- ///
- [Obsolete("Use PetBuddy != null", false)]
- public bool PetBuddyPresent => this.PetBuddy != null;
-
///
public BuddyMember? CompanionBuddy
{
@@ -147,7 +135,7 @@ public sealed partial class BuddyList : IServiceType, IBuddyList
///
/// This collection represents the buddies present in your squadron or trust party.
///
-public sealed partial class BuddyList
+internal sealed partial class BuddyList
{
///
int IReadOnlyCollection.Count => this.Length;
diff --git a/Dalamud/Game/ClientState/ClientState.cs b/Dalamud/Game/ClientState/ClientState.cs
index fed0ec3c4..bd4259f5a 100644
--- a/Dalamud/Game/ClientState/ClientState.cs
+++ b/Dalamud/Game/ClientState/ClientState.cs
@@ -1,4 +1,3 @@
-using System;
using System.Runtime.InteropServices;
using Dalamud.Data;
@@ -9,24 +8,25 @@ using Dalamud.Game.Network.Internal;
using Dalamud.Hooking;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
+using Dalamud.Logging.Internal;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.Game;
-using Serilog;
+using Lumina.Excel.GeneratedSheets;
+
+using Action = System.Action;
namespace Dalamud.Game.ClientState;
///
/// This class represents the state of the game client at the time of access.
///
-[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
-#pragma warning disable SA1015
-[ResolveVia]
-#pragma warning restore SA1015
-public sealed class ClientState : IDisposable, IServiceType, IClientState
+internal sealed class ClientState : IInternalDisposableService, IClientState
{
+ private static readonly ModuleLog Log = new("ClientState");
+
private readonly GameLifecycle lifecycle;
private readonly ClientStateAddressResolver address;
private readonly Hook setupTerritoryTypeHook;
@@ -38,10 +38,10 @@ public sealed class ClientState : IDisposable, IServiceType, IClientState
private readonly NetworkHandlers networkHandlers = Service.Get();
private bool lastConditionNone = true;
- private bool lastFramePvP = false;
+ private bool lastFramePvP;
[ServiceManager.ServiceConstructor]
- private ClientState(SigScanner sigScanner, DalamudStartInfo startInfo, GameLifecycle lifecycle)
+ private ClientState(TargetSigScanner sigScanner, Dalamud dalamud, GameLifecycle lifecycle)
{
this.lifecycle = lifecycle;
this.address = new ClientStateAddressResolver();
@@ -49,7 +49,7 @@ public sealed class ClientState : IDisposable, IServiceType, IClientState
Log.Verbose("===== C L I E N T S T A T E =====");
- this.ClientLanguage = startInfo.Language;
+ this.ClientLanguage = (ClientLanguage)dalamud.StartInfo.Language;
Log.Verbose($"SetupTerritoryType address 0x{this.address.SetupTerritoryType.ToInt64():X}");
@@ -58,28 +58,30 @@ public sealed class ClientState : IDisposable, IServiceType, IClientState
this.framework.Update += this.FrameworkOnOnUpdateEvent;
this.networkHandlers.CfPop += this.NetworkHandlersOnCfPop;
+
+ this.setupTerritoryTypeHook.Enable();
}
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate IntPtr SetupTerritoryTypeDelegate(IntPtr manager, ushort terriType);
///
- public event EventHandler TerritoryChanged;
+ public event Action? TerritoryChanged;
///
- public event EventHandler Login;
+ public event Action? Login;
///
- public event EventHandler Logout;
+ public event Action? Logout;
///
- public event Action EnterPvP;
+ public event Action? EnterPvP;
///
- public event Action LeavePvP;
+ public event Action? LeavePvP;
///
- public event EventHandler CfPop;
+ public event Action? CfPop;
///
public ClientLanguage ClientLanguage { get; }
@@ -102,6 +104,9 @@ public sealed class ClientState : IDisposable, IServiceType, IClientState
///
public bool IsPvPExcludingDen { get; private set; }
+ ///
+ public bool IsGPosing => GameMain.IsInGPose();
+
///
/// Gets client state address resolver.
///
@@ -110,35 +115,29 @@ public sealed class ClientState : IDisposable, IServiceType, IClientState
///
/// Dispose of managed and unmanaged resources.
///
- void IDisposable.Dispose()
+ void IInternalDisposableService.DisposeService()
{
this.setupTerritoryTypeHook.Dispose();
this.framework.Update -= this.FrameworkOnOnUpdateEvent;
this.networkHandlers.CfPop -= this.NetworkHandlersOnCfPop;
}
- [ServiceManager.CallWhenServicesReady]
- private void ContinueConstruction()
- {
- this.setupTerritoryTypeHook.Enable();
- }
-
private IntPtr SetupTerritoryTypeDetour(IntPtr manager, ushort terriType)
{
this.TerritoryType = terriType;
- this.TerritoryChanged?.InvokeSafely(this, terriType);
+ this.TerritoryChanged?.InvokeSafely(terriType);
Log.Debug("TerritoryType changed: {0}", terriType);
return this.setupTerritoryTypeHook.Original(manager, terriType);
}
- private void NetworkHandlersOnCfPop(object sender, Lumina.Excel.GeneratedSheets.ContentFinderCondition e)
+ private void NetworkHandlersOnCfPop(ContentFinderCondition e)
{
- this.CfPop?.InvokeSafely(this, e);
+ this.CfPop?.InvokeSafely(e);
}
- private void FrameworkOnOnUpdateEvent(Framework framework1)
+ private void FrameworkOnOnUpdateEvent(IFramework framework1)
{
var condition = Service.GetNullable();
var gameGui = Service.GetNullable();
@@ -147,12 +146,12 @@ public sealed class ClientState : IDisposable, IServiceType, IClientState
if (condition == null || gameGui == null || data == null)
return;
- if (condition.Any() && this.lastConditionNone == true && this.LocalPlayer != null)
+ if (condition.Any() && this.lastConditionNone && this.LocalPlayer != null)
{
Log.Debug("Is login");
this.lastConditionNone = false;
this.IsLoggedIn = true;
- this.Login?.InvokeSafely(this, null);
+ this.Login?.InvokeSafely();
gameGui.ResetUiHideState();
this.lifecycle.ResetLogout();
@@ -163,7 +162,7 @@ public sealed class ClientState : IDisposable, IServiceType, IClientState
Log.Debug("Is logout");
this.lastConditionNone = true;
this.IsLoggedIn = false;
- this.Logout?.InvokeSafely(this, null);
+ this.Logout?.InvokeSafely();
gameGui.ResetUiHideState();
this.lifecycle.SetLogout();
@@ -187,3 +186,103 @@ public sealed class ClientState : IDisposable, IServiceType, IClientState
}
}
}
+
+///
+/// Plugin-scoped version of a GameConfig service.
+///
+[PluginInterface]
+[InterfaceVersion("1.0")]
+[ServiceManager.ScopedService]
+#pragma warning disable SA1015
+[ResolveVia]
+#pragma warning restore SA1015
+internal class ClientStatePluginScoped : IInternalDisposableService, IClientState
+{
+ [ServiceManager.ServiceDependency]
+ private readonly ClientState clientStateService = Service.Get();
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ internal ClientStatePluginScoped()
+ {
+ this.clientStateService.TerritoryChanged += this.TerritoryChangedForward;
+ this.clientStateService.Login += this.LoginForward;
+ this.clientStateService.Logout += this.LogoutForward;
+ this.clientStateService.EnterPvP += this.EnterPvPForward;
+ this.clientStateService.LeavePvP += this.ExitPvPForward;
+ this.clientStateService.CfPop += this.ContentFinderPopForward;
+ }
+
+ ///
+ public event Action? TerritoryChanged;
+
+ ///
+ public event Action? Login;
+
+ ///
+ public event Action? Logout;
+
+ ///
+ public event Action? EnterPvP;
+
+ ///
+ public event Action? LeavePvP;
+
+ ///
+ public event Action? CfPop;
+
+ ///
+ public ClientLanguage ClientLanguage => this.clientStateService.ClientLanguage;
+
+ ///
+ public ushort TerritoryType => this.clientStateService.TerritoryType;
+
+ ///
+ public PlayerCharacter? LocalPlayer => this.clientStateService.LocalPlayer;
+
+ ///
+ public ulong LocalContentId => this.clientStateService.LocalContentId;
+
+ ///
+ public bool IsLoggedIn => this.clientStateService.IsLoggedIn;
+
+ ///
+ public bool IsPvP => this.clientStateService.IsPvP;
+
+ ///
+ public bool IsPvPExcludingDen => this.clientStateService.IsPvPExcludingDen;
+
+ ///
+ public bool IsGPosing => this.clientStateService.IsGPosing;
+
+ ///
+ void IInternalDisposableService.DisposeService()
+ {
+ this.clientStateService.TerritoryChanged -= this.TerritoryChangedForward;
+ this.clientStateService.Login -= this.LoginForward;
+ this.clientStateService.Logout -= this.LogoutForward;
+ this.clientStateService.EnterPvP -= this.EnterPvPForward;
+ this.clientStateService.LeavePvP -= this.ExitPvPForward;
+ this.clientStateService.CfPop -= this.ContentFinderPopForward;
+
+ this.TerritoryChanged = null;
+ this.Login = null;
+ this.Logout = null;
+ this.EnterPvP = null;
+ this.LeavePvP = null;
+ this.CfPop = null;
+ }
+
+ private void TerritoryChangedForward(ushort territoryId) => this.TerritoryChanged?.Invoke(territoryId);
+
+ private void LoginForward() => this.Login?.Invoke();
+
+ private void LogoutForward() => this.Logout?.Invoke();
+
+ private void EnterPvPForward() => this.EnterPvP?.Invoke();
+
+ private void ExitPvPForward() => this.LeavePvP?.Invoke();
+
+ private void ContentFinderPopForward(ContentFinderCondition cfc) => this.CfPop?.Invoke(cfc);
+}
diff --git a/Dalamud/Game/ClientState/ClientStateAddressResolver.cs b/Dalamud/Game/ClientState/ClientStateAddressResolver.cs
index 369e620be..73ed24e95 100644
--- a/Dalamud/Game/ClientState/ClientStateAddressResolver.cs
+++ b/Dalamud/Game/ClientState/ClientStateAddressResolver.cs
@@ -5,7 +5,7 @@ namespace Dalamud.Game.ClientState;
///
/// Client state memory address resolver.
///
-public sealed class ClientStateAddressResolver : BaseAddressResolver
+internal sealed class ClientStateAddressResolver : BaseAddressResolver
{
// Static offsets
@@ -79,7 +79,7 @@ public sealed class ClientStateAddressResolver : BaseAddressResolver
/// Scan for and setup any configured address pointers.
///
/// The signature scanner to facilitate setup.
- protected override void Setup64Bit(SigScanner sig)
+ protected override void Setup64Bit(ISigScanner sig)
{
this.ObjectTable = sig.GetStaticAddressFromSig("48 8D 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 44 0F B6 83 ?? ?? ?? ?? C6 83 ?? ?? ?? ?? ??");
diff --git a/Dalamud/Game/ClientState/Conditions/Condition.cs b/Dalamud/Game/ClientState/Conditions/Condition.cs
index f611a01c6..dc8b28494 100644
--- a/Dalamud/Game/ClientState/Conditions/Condition.cs
+++ b/Dalamud/Game/ClientState/Conditions/Condition.cs
@@ -1,7 +1,6 @@
-using System;
-
using Dalamud.IoC;
using Dalamud.IoC.Internal;
+using Dalamud.Plugin.Services;
using Serilog;
namespace Dalamud.Game.ClientState.Conditions;
@@ -9,47 +8,48 @@ namespace Dalamud.Game.ClientState.Conditions;
///
/// Provides access to conditions (generally player state). You can check whether a player is in combat, mounted, etc.
///
-[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
-public sealed partial class Condition : IServiceType
+internal sealed partial class Condition : IInternalDisposableService, ICondition
{
///
- /// The current max number of conditions. You can get this just by looking at the condition sheet and how many rows it has.
+ /// Gets the current max number of conditions. You can get this just by looking at the condition sheet and how many rows it has.
///
- public const int MaxConditionEntries = 104;
+ internal const int MaxConditionEntries = 104;
+ [ServiceManager.ServiceDependency]
+ private readonly Framework framework = Service.Get();
+
private readonly bool[] cache = new bool[MaxConditionEntries];
+ private bool isDisposed;
+
[ServiceManager.ServiceConstructor]
private Condition(ClientState clientState)
{
var resolver = clientState.AddressResolver;
this.Address = resolver.ConditionFlags;
+
+ // Initialization
+ for (var i = 0; i < MaxConditionEntries; i++)
+ this.cache[i] = this[i];
+
+ this.framework.Update += this.FrameworkUpdate;
}
+
+ /// Finalizes an instance of the class.
+ ~Condition() => this.Dispose(false);
- ///
- /// A delegate type used with the event.
- ///
- /// The changed condition.
- /// The value the condition is set to.
- public delegate void ConditionChangeDelegate(ConditionFlag flag, bool value);
+ ///
+ public event ICondition.ConditionChangeDelegate? ConditionChange;
- ///
- /// Event that gets fired when a condition is set.
- /// Should only get fired for actual changes, so the previous value will always be !value.
- ///
- public event ConditionChangeDelegate? ConditionChange;
+ ///
+ public int MaxEntries => MaxConditionEntries;
- ///
- /// Gets the condition array base pointer.
- ///
+ ///
public IntPtr Address { get; private set; }
- ///
- /// Check the value of a specific condition/state flag.
- ///
- /// The condition flag to check.
+ ///
public unsafe bool this[int flag]
{
get
@@ -61,14 +61,14 @@ public sealed partial class Condition : IServiceType
}
}
- ///
- public unsafe bool this[ConditionFlag flag]
+ ///
+ public bool this[ConditionFlag flag]
=> this[(int)flag];
- ///
- /// Check if any condition flags are set.
- ///
- /// Whether any single flag is set.
+ ///
+ void IInternalDisposableService.DisposeService() => this.Dispose(true);
+
+ ///
public bool Any()
{
for (var i = 0; i < MaxConditionEntries; i++)
@@ -81,18 +81,36 @@ public sealed partial class Condition : IServiceType
return false;
}
-
- [ServiceManager.CallWhenServicesReady]
- private void ContinueConstruction(Framework framework)
+
+ ///
+ public bool Any(params ConditionFlag[] flags)
{
- // Initialization
- for (var i = 0; i < MaxConditionEntries; i++)
- this.cache[i] = this[i];
+ foreach (var flag in flags)
+ {
+ // this[i] performs range checking, so no need to check here
+ if (this[flag])
+ {
+ return true;
+ }
+ }
- framework.Update += this.FrameworkUpdate;
+ return false;
}
- private void FrameworkUpdate(Framework framework)
+ private void Dispose(bool disposing)
+ {
+ if (this.isDisposed)
+ return;
+
+ if (disposing)
+ {
+ this.framework.Update -= this.FrameworkUpdate;
+ }
+
+ this.isDisposed = true;
+ }
+
+ private void FrameworkUpdate(IFramework unused)
{
for (var i = 0; i < MaxConditionEntries; i++)
{
@@ -116,39 +134,52 @@ public sealed partial class Condition : IServiceType
}
///
-/// Provides access to conditions (generally player state). You can check whether a player is in combat, mounted, etc.
+/// Plugin-scoped version of a Condition service.
///
-public sealed partial class Condition : IDisposable
+[PluginInterface]
+[InterfaceVersion("1.0")]
+[ServiceManager.ScopedService]
+#pragma warning disable SA1015
+[ResolveVia]
+#pragma warning restore SA1015
+internal class ConditionPluginScoped : IInternalDisposableService, ICondition
{
- private bool isDisposed;
+ [ServiceManager.ServiceDependency]
+ private readonly Condition conditionService = Service.Get();
///
- /// Finalizes an instance of the class.
+ /// Initializes a new instance of the class.
///
- ~Condition()
+ internal ConditionPluginScoped()
{
- this.Dispose(false);
+ this.conditionService.ConditionChange += this.ConditionChangedForward;
+ }
+
+ ///
+ public event ICondition.ConditionChangeDelegate? ConditionChange;
+
+ ///
+ public int MaxEntries => this.conditionService.MaxEntries;
+
+ ///
+ public IntPtr Address => this.conditionService.Address;
+
+ ///
+ public bool this[int flag] => this.conditionService[flag];
+
+ ///
+ void IInternalDisposableService.DisposeService()
+ {
+ this.conditionService.ConditionChange -= this.ConditionChangedForward;
+
+ this.ConditionChange = null;
}
- ///
- /// Disposes this instance, alongside its hooks.
- ///
- void IDisposable.Dispose()
- {
- GC.SuppressFinalize(this);
- this.Dispose(true);
- }
+ ///
+ public bool Any() => this.conditionService.Any();
- private void Dispose(bool disposing)
- {
- if (this.isDisposed)
- return;
+ ///
+ public bool Any(params ConditionFlag[] flags) => this.conditionService.Any(flags);
- if (disposing)
- {
- Service.Get().Update -= this.FrameworkUpdate;
- }
-
- this.isDisposed = true;
- }
+ private void ConditionChangedForward(ConditionFlag flag, bool value) => this.ConditionChange?.Invoke(flag, value);
}
diff --git a/Dalamud/Game/ClientState/Fates/FateTable.cs b/Dalamud/Game/ClientState/Fates/FateTable.cs
index 53196d5df..e9400842f 100644
--- a/Dalamud/Game/ClientState/Fates/FateTable.cs
+++ b/Dalamud/Game/ClientState/Fates/FateTable.cs
@@ -18,7 +18,7 @@ namespace Dalamud.Game.ClientState.Fates;
#pragma warning disable SA1015
[ResolveVia]
#pragma warning restore SA1015
-public sealed partial class FateTable : IServiceType, IFateTable
+internal sealed partial class FateTable : IServiceType, IFateTable
{
private readonly ClientStateAddressResolver address;
@@ -110,7 +110,7 @@ public sealed partial class FateTable : IServiceType, IFateTable
///
/// This collection represents the currently available Fate events.
///
-public sealed partial class FateTable
+internal sealed partial class FateTable
{
///
int IReadOnlyCollection.Count => this.Length;
diff --git a/Dalamud/Game/ClientState/GamePad/GamepadState.cs b/Dalamud/Game/ClientState/GamePad/GamepadState.cs
index bc5744047..a0e16f0e2 100644
--- a/Dalamud/Game/ClientState/GamePad/GamepadState.cs
+++ b/Dalamud/Game/ClientState/GamePad/GamepadState.cs
@@ -21,7 +21,7 @@ namespace Dalamud.Game.ClientState.GamePad;
#pragma warning disable SA1015
[ResolveVia]
#pragma warning restore SA1015
-public unsafe class GamepadState : IDisposable, IServiceType, IGamepadState
+internal unsafe class GamepadState : IInternalDisposableService, IGamepadState
{
private readonly Hook? gamepadPoll;
@@ -38,6 +38,7 @@ public unsafe class GamepadState : IDisposable, IServiceType, IGamepadState
var resolver = clientState.AddressResolver;
Log.Verbose($"GamepadPoll address 0x{resolver.GamepadPoll.ToInt64():X}");
this.gamepadPoll = Hook.FromAddress(resolver.GamepadPoll, this.GamepadPollDetour);
+ this.gamepadPoll?.Enable();
}
private delegate int ControllerPoll(IntPtr controllerInput);
@@ -55,54 +56,6 @@ public unsafe class GamepadState : IDisposable, IServiceType, IGamepadState
public Vector2 RightStick =>
new(this.rightStickX, this.rightStickY);
- ///
- /// Gets the state of the left analogue stick in the left direction between 0 (not tilted) and 1 (max tilt).
- ///
- [Obsolete("Use IGamepadState.LeftStick.X", false)]
- public float LeftStickLeft => this.leftStickX < 0 ? -this.leftStickX / 100f : 0;
-
- ///
- /// Gets the state of the left analogue stick in the right direction between 0 (not tilted) and 1 (max tilt).
- ///
- [Obsolete("Use IGamepadState.LeftStick.X", false)]
- public float LeftStickRight => this.leftStickX > 0 ? this.leftStickX / 100f : 0;
-
- ///
- /// Gets the state of the left analogue stick in the up direction between 0 (not tilted) and 1 (max tilt).
- ///
- [Obsolete("Use IGamepadState.LeftStick.Y", false)]
- public float LeftStickUp => this.leftStickY > 0 ? this.leftStickY / 100f : 0;
-
- ///
- /// Gets the state of the left analogue stick in the down direction between 0 (not tilted) and 1 (max tilt).
- ///
- [Obsolete("Use IGamepadState.LeftStick.Y", false)]
- public float LeftStickDown => this.leftStickY < 0 ? -this.leftStickY / 100f : 0;
-
- ///
- /// Gets the state of the right analogue stick in the left direction between 0 (not tilted) and 1 (max tilt).
- ///
- [Obsolete("Use IGamepadState.RightStick.X", false)]
- public float RightStickLeft => this.rightStickX < 0 ? -this.rightStickX / 100f : 0;
-
- ///
- /// Gets the state of the right analogue stick in the right direction between 0 (not tilted) and 1 (max tilt).
- ///
- [Obsolete("Use IGamepadState.RightStick.X", false)]
- public float RightStickRight => this.rightStickX > 0 ? this.rightStickX / 100f : 0;
-
- ///
- /// Gets the state of the right analogue stick in the up direction between 0 (not tilted) and 1 (max tilt).
- ///
- [Obsolete("Use IGamepadState.RightStick.Y", false)]
- public float RightStickUp => this.rightStickY > 0 ? this.rightStickY / 100f : 0;
-
- ///
- /// Gets the state of the right analogue stick in the down direction between 0 (not tilted) and 1 (max tilt).
- ///
- [Obsolete("Use IGamepadState.RightStick.Y", false)]
- public float RightStickDown => this.rightStickY < 0 ? -this.rightStickY / 100f : 0;
-
///
/// Gets buttons pressed bitmask, set once when the button is pressed. See for the mapping.
///
@@ -156,18 +109,12 @@ public unsafe class GamepadState : IDisposable, IServiceType, IGamepadState
///
/// Disposes this instance, alongside its hooks.
///
- void IDisposable.Dispose()
+ void IInternalDisposableService.DisposeService()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}
- [ServiceManager.CallWhenServicesReady]
- private void ContinueConstruction()
- {
- this.gamepadPoll?.Enable();
- }
-
private int GamepadPollDetour(IntPtr gamepadInput)
{
var original = this.gamepadPoll!.Original(gamepadInput);
diff --git a/Dalamud/Game/ClientState/JobGauge/JobGauges.cs b/Dalamud/Game/ClientState/JobGauge/JobGauges.cs
index 683f5c61f..74e22ddbe 100644
--- a/Dalamud/Game/ClientState/JobGauge/JobGauges.cs
+++ b/Dalamud/Game/ClientState/JobGauge/JobGauges.cs
@@ -19,7 +19,7 @@ namespace Dalamud.Game.ClientState.JobGauge;
#pragma warning disable SA1015
[ResolveVia]
#pragma warning restore SA1015
-public class JobGauges : IServiceType, IJobGauges
+internal class JobGauges : IServiceType, IJobGauges
{
private Dictionary cache = new();
diff --git a/Dalamud/Game/ClientState/Keys/KeyState.cs b/Dalamud/Game/ClientState/Keys/KeyState.cs
index 685973e17..76bee51bf 100644
--- a/Dalamud/Game/ClientState/Keys/KeyState.cs
+++ b/Dalamud/Game/ClientState/Keys/KeyState.cs
@@ -1,9 +1,11 @@
using System;
+using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
+using Dalamud.Plugin.Services;
using Serilog;
namespace Dalamud.Game.ClientState.Keys;
@@ -23,7 +25,10 @@ namespace Dalamud.Game.ClientState.Keys;
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
-public class KeyState : IServiceType
+#pragma warning disable SA1015
+[ResolveVia]
+#pragma warning restore SA1015
+internal class KeyState : IServiceType, IKeyState
{
// The array is accessed in a way that this limit doesn't appear to exist
// but there is other state data past this point, and keys beyond here aren't
@@ -31,10 +36,10 @@ public class KeyState : IServiceType
private const int MaxKeyCode = 0xF0;
private readonly IntPtr bufferBase;
private readonly IntPtr indexBase;
- private VirtualKey[] validVirtualKeyCache = null;
+ private VirtualKey[]? validVirtualKeyCache;
[ServiceManager.ServiceConstructor]
- private KeyState(SigScanner sigScanner, ClientState clientState)
+ private KeyState(TargetSigScanner sigScanner, ClientState clientState)
{
var moduleBaseAddress = sigScanner.Module.BaseAddress;
var addressResolver = clientState.AddressResolver;
@@ -44,46 +49,29 @@ public class KeyState : IServiceType
Log.Verbose($"Keyboard state buffer address 0x{this.bufferBase.ToInt64():X}");
}
- ///
- /// Get or set the key-pressed state for a given vkCode.
- ///
- /// The virtual key to change.
- /// Whether the specified key is currently pressed.
- /// If the vkCode is not valid. Refer to or .
- /// If the set value is non-zero.
- public unsafe bool this[int vkCode]
+ ///
+ public bool this[int vkCode]
{
get => this.GetRawValue(vkCode) != 0;
set => this.SetRawValue(vkCode, value ? 1 : 0);
}
- ///
+ ///
public bool this[VirtualKey vkCode]
{
get => this[(int)vkCode];
set => this[(int)vkCode] = value;
}
- ///
- /// Gets the value in the index array.
- ///
- /// The virtual key to change.
- /// The raw value stored in the index array.
- /// If the vkCode is not valid. Refer to or .
+ ///
public int GetRawValue(int vkCode)
=> this.GetRefValue(vkCode);
- ///
+ ///
public int GetRawValue(VirtualKey vkCode)
=> this.GetRawValue((int)vkCode);
- ///
- /// Sets the value in the index array.
- ///
- /// The virtual key to change.
- /// The raw value to set in the index array.
- /// If the vkCode is not valid. Refer to or .
- /// If the set value is non-zero.
+ ///
public void SetRawValue(int vkCode, int value)
{
if (value != 0)
@@ -92,32 +80,23 @@ public class KeyState : IServiceType
this.GetRefValue(vkCode) = value;
}
- ///
+ ///
public void SetRawValue(VirtualKey vkCode, int value)
=> this.SetRawValue((int)vkCode, value);
- ///
- /// Gets a value indicating whether the given VirtualKey code is regarded as valid input by the game.
- ///
- /// Virtual key code.
- /// If the code is valid.
+ ///
public bool IsVirtualKeyValid(int vkCode)
=> this.ConvertVirtualKey(vkCode) != 0;
- ///
+ ///
public bool IsVirtualKeyValid(VirtualKey vkCode)
=> this.IsVirtualKeyValid((int)vkCode);
- ///
- /// Gets an array of virtual keys the game considers valid input.
- ///
- /// An array of valid virtual keys.
- public VirtualKey[] GetValidVirtualKeys()
- => this.validVirtualKeyCache ??= Enum.GetValues().Where(vk => this.IsVirtualKeyValid(vk)).ToArray();
+ ///
+ public IEnumerable GetValidVirtualKeys()
+ => this.validVirtualKeyCache ??= Enum.GetValues().Where(this.IsVirtualKeyValid).ToArray();
- ///
- /// Clears the pressed state for all keys.
- ///
+ ///
public void ClearAll()
{
foreach (var vk in this.GetValidVirtualKeys())
diff --git a/Dalamud/Game/ClientState/Objects/ObjectTable.cs b/Dalamud/Game/ClientState/Objects/ObjectTable.cs
index 16cf7c277..278c0772f 100644
--- a/Dalamud/Game/ClientState/Objects/ObjectTable.cs
+++ b/Dalamud/Game/ClientState/Objects/ObjectTable.cs
@@ -21,9 +21,9 @@ namespace Dalamud.Game.ClientState.Objects;
#pragma warning disable SA1015
[ResolveVia]
#pragma warning restore SA1015
-public sealed partial class ObjectTable : IServiceType, IObjectTable
+internal sealed partial class ObjectTable : IServiceType, IObjectTable
{
- private const int ObjectTableLength = 596;
+ private const int ObjectTableLength = 599;
private readonly ClientStateAddressResolver address;
@@ -109,7 +109,7 @@ public sealed partial class ObjectTable : IServiceType, IObjectTable
///
/// This collection represents the currently spawned FFXIV game objects.
///
-public sealed partial class ObjectTable
+internal sealed partial class ObjectTable
{
///
int IReadOnlyCollection.Count => this.Length;
diff --git a/Dalamud/Game/ClientState/Objects/SubKinds/BattleNpc.cs b/Dalamud/Game/ClientState/Objects/SubKinds/BattleNpc.cs
index 59f32e33d..add7a7f9f 100644
--- a/Dalamud/Game/ClientState/Objects/SubKinds/BattleNpc.cs
+++ b/Dalamud/Game/ClientState/Objects/SubKinds/BattleNpc.cs
@@ -1,5 +1,3 @@
-using System;
-
using Dalamud.Game.ClientState.Objects.Enums;
namespace Dalamud.Game.ClientState.Objects.Types;
@@ -25,5 +23,5 @@ public unsafe class BattleNpc : BattleChara
public BattleNpcSubKind BattleNpcKind => (BattleNpcSubKind)this.Struct->Character.GameObject.SubKind;
///
- public override ulong TargetObjectId => this.Struct->Character.TargetObjectID;
+ public override ulong TargetObjectId => this.Struct->Character.TargetId;
}
diff --git a/Dalamud/Game/ClientState/Objects/SubKinds/PlayerCharacter.cs b/Dalamud/Game/ClientState/Objects/SubKinds/PlayerCharacter.cs
index 7fc9c0079..9de11e3ec 100644
--- a/Dalamud/Game/ClientState/Objects/SubKinds/PlayerCharacter.cs
+++ b/Dalamud/Game/ClientState/Objects/SubKinds/PlayerCharacter.cs
@@ -1,5 +1,3 @@
-using System;
-
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Game.ClientState.Resolvers;
@@ -33,5 +31,5 @@ public unsafe class PlayerCharacter : BattleChara
///
/// Gets the target actor ID of the PlayerCharacter.
///
- public override ulong TargetObjectId => this.Struct->Character.PlayerTargetObjectID;
+ public override ulong TargetObjectId => this.Struct->Character.LookTargetId;
}
diff --git a/Dalamud/Game/ClientState/Objects/TargetManager.cs b/Dalamud/Game/ClientState/Objects/TargetManager.cs
index ff1bdc5ba..fcb242c1e 100644
--- a/Dalamud/Game/ClientState/Objects/TargetManager.cs
+++ b/Dalamud/Game/ClientState/Objects/TargetManager.cs
@@ -16,7 +16,7 @@ namespace Dalamud.Game.ClientState.Objects;
#pragma warning disable SA1015
[ResolveVia]
#pragma warning restore SA1015
-public sealed unsafe class TargetManager : IServiceType, ITargetManager
+internal sealed unsafe class TargetManager : IServiceType, ITargetManager
{
[ServiceManager.ServiceDependency]
private readonly ClientState clientState = Service.Get();
@@ -39,136 +39,50 @@ public sealed unsafe class TargetManager : IServiceType, ITargetManager
public GameObject? Target
{
get => this.objectTable.CreateObjectReference((IntPtr)Struct->Target);
- set => this.SetTarget(value);
+ set => Struct->Target = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address;
}
///
public GameObject? MouseOverTarget
{
get => this.objectTable.CreateObjectReference((IntPtr)Struct->MouseOverTarget);
- set => this.SetMouseOverTarget(value);
+ set => Struct->MouseOverTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address;
}
///
public GameObject? FocusTarget
{
get => this.objectTable.CreateObjectReference((IntPtr)Struct->FocusTarget);
- set => this.SetFocusTarget(value);
+ set => Struct->FocusTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address;
}
///
public GameObject? PreviousTarget
{
get => this.objectTable.CreateObjectReference((IntPtr)Struct->PreviousTarget);
- set => this.SetPreviousTarget(value);
+ set => Struct->PreviousTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address;
}
///
public GameObject? SoftTarget
{
get => this.objectTable.CreateObjectReference((IntPtr)Struct->SoftTarget);
- set => this.SetSoftTarget(value);
+ set => Struct->SoftTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address;
+ }
+
+ ///
+ public GameObject? GPoseTarget
+ {
+ get => this.objectTable.CreateObjectReference((IntPtr)Struct->GPoseTarget);
+ set => Struct->GPoseTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address;
+ }
+
+ ///
+ public GameObject? MouseOverNameplateTarget
+ {
+ get => this.objectTable.CreateObjectReference((IntPtr)Struct->MouseOverNameplateTarget);
+ set => Struct->MouseOverNameplateTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address;
}
private FFXIVClientStructs.FFXIV.Client.Game.Control.TargetSystem* Struct => (FFXIVClientStructs.FFXIV.Client.Game.Control.TargetSystem*)this.Address;
-
- ///
- /// Sets the current target.
- ///
- /// Actor to target.
- [Obsolete("Use Target Property", false)]
- public void SetTarget(GameObject? actor) => this.SetTarget(actor?.Address ?? IntPtr.Zero);
-
- ///
- /// Sets the mouseover target.
- ///
- /// Actor to target.
- [Obsolete("Use MouseOverTarget Property", false)]
- public void SetMouseOverTarget(GameObject? actor) => this.SetMouseOverTarget(actor?.Address ?? IntPtr.Zero);
-
- ///
- /// Sets the focus target.
- ///
- /// Actor to target.
- [Obsolete("Use FocusTarget Property", false)]
- public void SetFocusTarget(GameObject? actor) => this.SetFocusTarget(actor?.Address ?? IntPtr.Zero);
-
- ///
- /// Sets the previous target.
- ///
- /// Actor to target.
- [Obsolete("Use PreviousTarget Property", false)]
- public void SetPreviousTarget(GameObject? actor) => this.SetTarget(actor?.Address ?? IntPtr.Zero);
-
- ///
- /// Sets the soft target.
- ///
- /// Actor to target.
- [Obsolete("Use SoftTarget Property", false)]
- public void SetSoftTarget(GameObject? actor) => this.SetTarget(actor?.Address ?? IntPtr.Zero);
-
- ///
- /// Sets the current target.
- ///
- /// Actor (address) to target.
- [Obsolete("Use Target Property", false)]
- public void SetTarget(IntPtr actorAddress) => Struct->Target = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)actorAddress;
-
- ///
- /// Sets the mouseover target.
- ///
- /// Actor (address) to target.
- [Obsolete("Use MouseOverTarget Property", false)]
- public void SetMouseOverTarget(IntPtr actorAddress) => Struct->MouseOverTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)actorAddress;
-
- ///
- /// Sets the focus target.
- ///
- /// Actor (address) to target.
- [Obsolete("Use FocusTarget Property", false)]
- public void SetFocusTarget(IntPtr actorAddress) => Struct->FocusTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)actorAddress;
-
- ///
- /// Sets the previous target.
- ///
- /// Actor (address) to target.
- [Obsolete("Use PreviousTarget Property", false)]
- public void SetPreviousTarget(IntPtr actorAddress) => Struct->PreviousTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)actorAddress;
-
- ///
- /// Sets the soft target.
- ///
- /// Actor (address) to target.
- [Obsolete("Use SoftTarget Property", false)]
- public void SetSoftTarget(IntPtr actorAddress) => Struct->SoftTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)actorAddress;
-
- ///
- /// Clears the current target.
- ///
- [Obsolete("Use Target Property", false)]
- public void ClearTarget() => this.SetTarget(IntPtr.Zero);
-
- ///
- /// Clears the mouseover target.
- ///
- [Obsolete("Use MouseOverTarget Property", false)]
- public void ClearMouseOverTarget() => this.SetMouseOverTarget(IntPtr.Zero);
-
- ///
- /// Clears the focus target.
- ///
- [Obsolete("Use FocusTarget Property", false)]
- public void ClearFocusTarget() => this.SetFocusTarget(IntPtr.Zero);
-
- ///
- /// Clears the previous target.
- ///
- [Obsolete("Use PreviousTarget Property", false)]
- public void ClearPreviousTarget() => this.SetPreviousTarget(IntPtr.Zero);
-
- ///
- /// Clears the soft target.
- ///
- [Obsolete("Use SoftTarget Property", false)]
- public void ClearSoftTarget() => this.SetSoftTarget(IntPtr.Zero);
}
diff --git a/Dalamud/Game/ClientState/Objects/Types/BattleChara.cs b/Dalamud/Game/ClientState/Objects/Types/BattleChara.cs
index 63a5b828a..0c5d16675 100644
--- a/Dalamud/Game/ClientState/Objects/Types/BattleChara.cs
+++ b/Dalamud/Game/ClientState/Objects/Types/BattleChara.cs
@@ -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
///
/// Gets the total casting time of the spell being cast by the chara.
///
+ ///
+ /// This can only be a portion of the total cast for some actions.
+ /// Use AdjustedTotalCastTime if you always need the total cast time.
+ ///
+ [Api10ToDo("Rename so it is not confused with AdjustedTotalCastTime")]
public float TotalCastTime => this.Struct->GetCastInfo->TotalCastTime;
+ ///
+ /// Gets the plus any adjustments from the game, such as Action offset 2B. Used for display purposes.
+ ///
+ ///
+ /// This is the actual total cast time for all actions.
+ ///
+ [Api10ToDo("Rename so it is not confused with TotalCastTime")]
+ public float AdjustedTotalCastTime => this.Struct->GetCastInfo->AdjustedTotalCastTime;
+
///
/// Gets the underlying structure.
///
diff --git a/Dalamud/Game/ClientState/Objects/Types/Character.cs b/Dalamud/Game/ClientState/Objects/Types/Character.cs
index ee8418362..ac11bcdd0 100644
--- a/Dalamud/Game/ClientState/Objects/Types/Character.cs
+++ b/Dalamud/Game/ClientState/Objects/Types/Character.cs
@@ -1,5 +1,3 @@
-using System;
-
using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Game.ClientState.Resolvers;
using Dalamud.Game.Text.SeStringHandling;
@@ -63,6 +61,11 @@ public unsafe class Character : GameObject
///
public uint MaxCp => this.Struct->CharacterData.MaxCraftingPoints;
+ ///
+ /// Gets the shield percentage of this Chara.
+ ///
+ public byte ShieldPercentage => this.Struct->CharacterData.ShieldValue;
+
///
/// Gets the ClassJob of this Chara.
///
@@ -87,7 +90,7 @@ public unsafe class Character : GameObject
///
/// Gets the target object ID of the character.
///
- public override ulong TargetObjectId => this.Struct->TargetObjectID;
+ public override ulong TargetObjectId => this.Struct->TargetId;
///
/// Gets the name ID of the character.
@@ -115,5 +118,6 @@ public unsafe class Character : GameObject
///
/// Gets the underlying structure.
///
- protected internal new FFXIVClientStructs.FFXIV.Client.Game.Character.Character* Struct => (FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)this.Address;
+ protected internal new FFXIVClientStructs.FFXIV.Client.Game.Character.Character* Struct =>
+ (FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)this.Address;
}
diff --git a/Dalamud/Game/ClientState/Party/PartyList.cs b/Dalamud/Game/ClientState/Party/PartyList.cs
index 529b57b6f..946c73245 100644
--- a/Dalamud/Game/ClientState/Party/PartyList.cs
+++ b/Dalamud/Game/ClientState/Party/PartyList.cs
@@ -19,7 +19,7 @@ namespace Dalamud.Game.ClientState.Party;
#pragma warning disable SA1015
[ResolveVia]
#pragma warning restore SA1015
-public sealed unsafe partial class PartyList : IServiceType, IPartyList
+internal sealed unsafe partial class PartyList : IServiceType, IPartyList
{
private const int GroupLength = 8;
private const int AllianceLength = 20;
@@ -130,7 +130,7 @@ public sealed unsafe partial class PartyList : IServiceType, IPartyList
///
/// This collection represents the party members present in your party or alliance.
///
-public sealed partial class PartyList
+internal sealed partial class PartyList
{
///
int IReadOnlyCollection.Count => this.Length;
diff --git a/Dalamud/Game/ClientState/Statuses/StatusList.cs b/Dalamud/Game/ClientState/Statuses/StatusList.cs
index bcff50360..fce59e29b 100644
--- a/Dalamud/Game/ClientState/Statuses/StatusList.cs
+++ b/Dalamud/Game/ClientState/Statuses/StatusList.cs
@@ -10,8 +10,6 @@ namespace Dalamud.Game.ClientState.Statuses;
///
public sealed unsafe partial class StatusList
{
- private const int StatusListLength = 30;
-
///
/// Initializes a new instance of the class.
///
@@ -38,7 +36,7 @@ public sealed unsafe partial class StatusList
///
/// Gets the amount of status effect slots the actor has.
///
- public int Length => StatusListLength;
+ public int Length => Struct->NumValidStatuses;
private static int StatusSize { get; } = Marshal.SizeOf();
@@ -53,7 +51,7 @@ public sealed unsafe partial class StatusList
{
get
{
- if (index < 0 || index > StatusListLength)
+ if (index < 0 || index > this.Length)
return null;
var addr = this.GetStatusAddress(index);
@@ -107,7 +105,7 @@ public sealed unsafe partial class StatusList
/// The memory address of the party member.
public IntPtr GetStatusAddress(int index)
{
- if (index < 0 || index >= StatusListLength)
+ if (index < 0 || index >= this.Length)
return IntPtr.Zero;
return (IntPtr)(this.Struct->Status + (index * StatusSize));
@@ -134,7 +132,7 @@ public sealed partial class StatusList : IReadOnlyCollection, ICollectio
///
public IEnumerator GetEnumerator()
{
- for (var i = 0; i < StatusListLength; i++)
+ for (var i = 0; i < this.Length; i++)
{
var status = this[i];
diff --git a/Dalamud/Game/Command/CommandInfo.cs b/Dalamud/Game/Command/CommandInfo.cs
index 9b559599a..bc0250a66 100644
--- a/Dalamud/Game/Command/CommandInfo.cs
+++ b/Dalamud/Game/Command/CommandInfo.cs
@@ -15,7 +15,6 @@ public sealed class CommandInfo
public CommandInfo(HandlerDelegate handler)
{
this.Handler = handler;
- this.LoaderAssemblyName = Assembly.GetCallingAssembly()?.GetName()?.Name;
}
///
diff --git a/Dalamud/Game/Command/CommandManager.cs b/Dalamud/Game/Command/CommandManager.cs
index 63a1a3d09..7dcca763b 100644
--- a/Dalamud/Game/Command/CommandManager.cs
+++ b/Dalamud/Game/Command/CommandManager.cs
@@ -1,4 +1,3 @@
-using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
@@ -9,22 +8,21 @@ using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
+using Dalamud.Logging.Internal;
+using Dalamud.Plugin.Internal.Types;
using Dalamud.Plugin.Services;
-using Serilog;
namespace Dalamud.Game.Command;
///
/// This class manages registered in-game slash commands.
///
-[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
-#pragma warning disable SA1015
-[ResolveVia]
-#pragma warning restore SA1015
-public sealed class CommandManager : IServiceType, IDisposable, ICommandManager
+internal sealed class CommandManager : IInternalDisposableService, ICommandManager
{
+ private static readonly ModuleLog Log = new("Command");
+
private readonly ConcurrentDictionary commandMap = new();
private readonly Regex commandRegexEn = new(@"^The command (?.+) does not exist\.$", RegexOptions.Compiled);
private readonly Regex commandRegexJp = new(@"^そのコマンドはありません。: (?.+)$", RegexOptions.Compiled);
@@ -37,15 +35,15 @@ public sealed class CommandManager : IServiceType, IDisposable, ICommandManager
private readonly ChatGui chatGui = Service.Get();
[ServiceManager.ServiceConstructor]
- private CommandManager(DalamudStartInfo startInfo)
+ private CommandManager(Dalamud dalamud)
{
- this.currentLangCommandRegex = startInfo.Language switch
+ this.currentLangCommandRegex = (ClientLanguage)dalamud.StartInfo.Language switch
{
ClientLanguage.Japanese => this.commandRegexJp,
ClientLanguage.English => this.commandRegexEn,
ClientLanguage.German => this.commandRegexDe,
ClientLanguage.French => this.commandRegexFr,
- _ => this.currentLangCommandRegex,
+ _ => this.commandRegexEn,
};
this.chatGui.CheckMessageHandled += this.OnCheckMessageHandled;
@@ -84,7 +82,7 @@ public sealed class CommandManager : IServiceType, IDisposable, ICommandManager
// => command: 0-12 (12 chars)
// => argument: 13-17 (4 chars)
// => content.IndexOf(' ') == 12
- command = content.Substring(0, separatorPosition);
+ command = content[..separatorPosition];
var argStart = separatorPosition + 1;
argument = content[argStart..];
@@ -132,7 +130,7 @@ public sealed class CommandManager : IServiceType, IDisposable, ICommandManager
}
///
- void IDisposable.Dispose()
+ void IInternalDisposableService.DisposeService()
{
this.chatGui.CheckMessageHandled -= this.OnCheckMessageHandled;
}
@@ -162,3 +160,93 @@ public sealed class CommandManager : IServiceType, IDisposable, ICommandManager
}
}
}
+
+///
+/// Plugin-scoped version of a AddonLifecycle service.
+///
+[PluginInterface]
+[InterfaceVersion("1.0")]
+[ServiceManager.ScopedService]
+#pragma warning disable SA1015
+[ResolveVia]
+#pragma warning restore SA1015
+internal class CommandManagerPluginScoped : IInternalDisposableService, ICommandManager
+{
+ private static readonly ModuleLog Log = new("Command");
+
+ [ServiceManager.ServiceDependency]
+ private readonly CommandManager commandManagerService = Service.Get();
+
+ private readonly List pluginRegisteredCommands = new();
+ private readonly LocalPlugin pluginInfo;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Info for the plugin that requests this service.
+ public CommandManagerPluginScoped(LocalPlugin localPlugin)
+ {
+ this.pluginInfo = localPlugin;
+ }
+
+ ///
+ public ReadOnlyDictionary Commands => this.commandManagerService.Commands;
+
+ ///
+ void IInternalDisposableService.DisposeService()
+ {
+ foreach (var command in this.pluginRegisteredCommands)
+ {
+ this.commandManagerService.RemoveHandler(command);
+ }
+
+ this.pluginRegisteredCommands.Clear();
+ }
+
+ ///
+ public bool ProcessCommand(string content)
+ => this.commandManagerService.ProcessCommand(content);
+
+ ///
+ public void DispatchCommand(string command, string argument, CommandInfo info)
+ => this.commandManagerService.DispatchCommand(command, argument, info);
+
+ ///
+ public bool AddHandler(string command, CommandInfo info)
+ {
+ if (!this.pluginRegisteredCommands.Contains(command))
+ {
+ info.LoaderAssemblyName = this.pluginInfo.InternalName;
+ if (this.commandManagerService.AddHandler(command, info))
+ {
+ this.pluginRegisteredCommands.Add(command);
+ return true;
+ }
+ }
+ else
+ {
+ Log.Error($"Command {command} is already registered.");
+ }
+
+ return false;
+ }
+
+ ///
+ public bool RemoveHandler(string command)
+ {
+ if (this.pluginRegisteredCommands.Contains(command))
+ {
+ if (this.commandManagerService.RemoveHandler(command))
+ {
+ this.pluginRegisteredCommands.Remove(command);
+ return true;
+ }
+ }
+ else
+ {
+ Log.Error($"Command {command} not found.");
+ }
+
+ return false;
+ }
+}
diff --git a/Dalamud/Game/Config/GameConfig.cs b/Dalamud/Game/Config/GameConfig.cs
index dfdb8b5d2..a021025b1 100644
--- a/Dalamud/Game/Config/GameConfig.cs
+++ b/Dalamud/Game/Config/GameConfig.cs
@@ -1,4 +1,5 @@
-using System;
+using System.Threading.Tasks;
+
using Dalamud.Hooking;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
@@ -13,47 +14,86 @@ namespace Dalamud.Game.Config;
/// This class represents the game's configuration.
///
[InterfaceVersion("1.0")]
-[PluginInterface]
-[ServiceManager.EarlyLoadedService]
-#pragma warning disable SA1015
-[ResolveVia]
-#pragma warning restore SA1015
-public sealed class GameConfig : IServiceType, IGameConfig, IDisposable
+[ServiceManager.BlockingEarlyLoadedService]
+internal sealed class GameConfig : IInternalDisposableService, IGameConfig
{
+ private readonly TaskCompletionSource tcsInitialization = new();
+ private readonly TaskCompletionSource tcsSystem = new();
+ private readonly TaskCompletionSource tcsUiConfig = new();
+ private readonly TaskCompletionSource tcsUiControl = new();
+
private readonly GameConfigAddressResolver address = new();
private Hook? configChangeHook;
[ServiceManager.ServiceConstructor]
- private unsafe GameConfig(Framework framework, SigScanner sigScanner)
+ private unsafe GameConfig(Framework framework, TargetSigScanner sigScanner)
{
framework.RunOnTick(() =>
{
- Log.Verbose("[GameConfig] Initializing");
- var csFramework = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework.Instance();
- var commonConfig = &csFramework->SystemConfig.CommonSystemConfig;
- this.System = new GameConfigSection("System", framework, &commonConfig->ConfigBase);
- this.UiConfig = new GameConfigSection("UiConfig", framework, &commonConfig->UiConfig);
- this.UiControl = new GameConfigSection("UiControl", framework, () => this.UiConfig.TryGetBool("PadMode", out var padMode) && padMode ? &commonConfig->UiControlGamepadConfig : &commonConfig->UiControlConfig);
-
- this.address.Setup(sigScanner);
- this.configChangeHook = Hook.FromAddress(this.address.ConfigChangeAddress, this.OnConfigChanged);
- this.configChangeHook?.Enable();
+ try
+ {
+ Log.Verbose("[GameConfig] Initializing");
+ var csFramework = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework.Instance();
+ var commonConfig = &csFramework->SystemConfig.CommonSystemConfig;
+ this.tcsSystem.SetResult(new("System", framework, &commonConfig->ConfigBase));
+ this.tcsUiConfig.SetResult(new("UiConfig", framework, &commonConfig->UiConfig));
+ this.tcsUiControl.SetResult(
+ new(
+ "UiControl",
+ framework,
+ () => this.UiConfig.TryGetBool("PadMode", out var padMode) && padMode
+ ? &commonConfig->UiControlGamepadConfig
+ : &commonConfig->UiControlConfig));
+
+ this.address.Setup(sigScanner);
+ this.configChangeHook = Hook.FromAddress(
+ this.address.ConfigChangeAddress,
+ this.OnConfigChanged);
+ this.configChangeHook.Enable();
+ this.tcsInitialization.SetResult();
+ }
+ catch (Exception ex)
+ {
+ this.tcsInitialization.SetExceptionIfIncomplete(ex);
+ }
});
}
private unsafe delegate nint ConfigChangeDelegate(ConfigBase* configBase, ConfigEntry* configEntry);
///
- public event EventHandler Changed;
+ public event EventHandler? Changed;
+
+#pragma warning disable 67
+ ///
+ /// Unused internally, used as a proxy for System.Changed via GameConfigPluginScoped
+ ///
+ public event EventHandler? SystemChanged;
- ///
- public GameConfigSection System { get; private set; }
+ ///
+ /// Unused internally, used as a proxy for UiConfig.Changed via GameConfigPluginScoped
+ ///
+ public event EventHandler? UiConfigChanged;
+
+ ///
+ /// Unused internally, used as a proxy for UiControl.Changed via GameConfigPluginScoped
+ ///
+ public event EventHandler? UiControlChanged;
+#pragma warning restore 67
+
+ ///
+ /// Gets a task representing the initialization state of this class.
+ ///
+ public Task InitializationTask => this.tcsInitialization.Task;
///
- public GameConfigSection UiConfig { get; private set; }
+ public GameConfigSection System => this.tcsSystem.Task.Result;
///
- public GameConfigSection UiControl { get; private set; }
+ public GameConfigSection UiConfig => this.tcsUiConfig.Task.Result;
+
+ ///
+ public GameConfigSection UiControl => this.tcsUiControl.Task.Result;
///
public bool TryGet(SystemConfigOption option, out bool value) => this.System.TryGet(option.GetName(), out value);
@@ -155,8 +195,13 @@ public sealed class GameConfig : IServiceType, IGameConfig, IDisposable
public void Set(UiControlOption option, string value) => this.UiControl.Set(option.GetName(), value);
///
- void IDisposable.Dispose()
+ void IInternalDisposableService.DisposeService()
{
+ var ode = new ObjectDisposedException(nameof(GameConfig));
+ this.tcsInitialization.SetExceptionIfIncomplete(ode);
+ this.tcsSystem.SetExceptionIfIncomplete(ode);
+ this.tcsUiConfig.SetExceptionIfIncomplete(ode);
+ this.tcsUiControl.SetExceptionIfIncomplete(ode);
this.configChangeHook?.Disable();
this.configChangeHook?.Dispose();
}
@@ -193,3 +238,219 @@ public sealed class GameConfig : IServiceType, IGameConfig, IDisposable
return returnValue;
}
}
+
+///
+/// Plugin-scoped version of a GameConfig service.
+///
+[PluginInterface]
+[InterfaceVersion("1.0")]
+[ServiceManager.ScopedService]
+#pragma warning disable SA1015
+[ResolveVia]
+#pragma warning restore SA1015
+internal class GameConfigPluginScoped : IInternalDisposableService, IGameConfig
+{
+ [ServiceManager.ServiceDependency]
+ private readonly GameConfig gameConfigService = Service.Get();
+
+ private readonly Task initializationTask;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ internal GameConfigPluginScoped()
+ {
+ this.gameConfigService.Changed += this.ConfigChangedForward;
+ this.initializationTask = this.gameConfigService.InitializationTask.ContinueWith(
+ r =>
+ {
+ if (!r.IsCompletedSuccessfully)
+ return r;
+ this.gameConfigService.System.Changed += this.SystemConfigChangedForward;
+ this.gameConfigService.UiConfig.Changed += this.UiConfigConfigChangedForward;
+ this.gameConfigService.UiControl.Changed += this.UiControlConfigChangedForward;
+ return Task.CompletedTask;
+ }).Unwrap();
+ }
+
+ ///
+ public event EventHandler? Changed;
+
+ ///
+ public event EventHandler? SystemChanged;
+
+ ///
+ public event EventHandler? UiConfigChanged;
+
+ ///
+ public event EventHandler? UiControlChanged;
+
+ ///
+ public GameConfigSection System => this.gameConfigService.System;
+
+ ///
+ public GameConfigSection UiConfig => this.gameConfigService.UiConfig;
+
+ ///
+ public GameConfigSection UiControl => this.gameConfigService.UiControl;
+
+ ///
+ void IInternalDisposableService.DisposeService()
+ {
+ this.gameConfigService.Changed -= this.ConfigChangedForward;
+ this.initializationTask.ContinueWith(
+ r =>
+ {
+ if (!r.IsCompletedSuccessfully)
+ return;
+ this.gameConfigService.System.Changed -= this.SystemConfigChangedForward;
+ this.gameConfigService.UiConfig.Changed -= this.UiConfigConfigChangedForward;
+ this.gameConfigService.UiControl.Changed -= this.UiControlConfigChangedForward;
+ });
+
+ this.Changed = null;
+ this.SystemChanged = null;
+ this.UiConfigChanged = null;
+ this.UiControlChanged = null;
+ }
+
+ ///
+ public bool TryGet(SystemConfigOption option, out bool value)
+ => this.gameConfigService.TryGet(option, out value);
+
+ ///
+ public bool TryGet(SystemConfigOption option, out uint value)
+ => this.gameConfigService.TryGet(option, out value);
+
+ ///
+ public bool TryGet(SystemConfigOption option, out float value)
+ => this.gameConfigService.TryGet(option, out value);
+
+ ///
+ public bool TryGet(SystemConfigOption option, out string value)
+ => this.gameConfigService.TryGet(option, out value);
+
+ ///
+ public bool TryGet(SystemConfigOption option, out UIntConfigProperties? properties)
+ => this.gameConfigService.TryGet(option, out properties);
+
+ ///
+ public bool TryGet(SystemConfigOption option, out FloatConfigProperties? properties)
+ => this.gameConfigService.TryGet(option, out properties);
+
+ ///
+ public bool TryGet(SystemConfigOption option, out StringConfigProperties? properties)
+ => this.gameConfigService.TryGet(option, out properties);
+
+ ///
+ public bool TryGet(UiConfigOption option, out bool value)
+ => this.gameConfigService.TryGet(option, out value);
+
+ ///
+ public bool TryGet(UiConfigOption option, out uint value)
+ => this.gameConfigService.TryGet(option, out value);
+
+ ///
+ public bool TryGet(UiConfigOption option, out float value)
+ => this.gameConfigService.TryGet(option, out value);
+
+ ///
+ public bool TryGet(UiConfigOption option, out string value)
+ => this.gameConfigService.TryGet(option, out value);
+
+ ///
+ public bool TryGet(UiConfigOption option, out UIntConfigProperties? properties)
+ => this.gameConfigService.TryGet(option, out properties);
+
+ ///
+ public bool TryGet(UiConfigOption option, out FloatConfigProperties? properties)
+ => this.gameConfigService.TryGet(option, out properties);
+
+ ///
+ public bool TryGet(UiConfigOption option, out StringConfigProperties? properties)
+ => this.gameConfigService.TryGet(option, out properties);
+
+ ///
+ public bool TryGet(UiControlOption option, out bool value)
+ => this.gameConfigService.TryGet(option, out value);
+
+ ///
+ public bool TryGet(UiControlOption option, out uint value)
+ => this.gameConfigService.TryGet(option, out value);
+
+ ///
+ public bool TryGet(UiControlOption option, out float value)
+ => this.gameConfigService.TryGet(option, out value);
+
+ ///
+ public bool TryGet(UiControlOption option, out string value)
+ => this.gameConfigService.TryGet(option, out value);
+
+ ///
+ public bool TryGet(UiControlOption option, out UIntConfigProperties? properties)
+ => this.gameConfigService.TryGet(option, out properties);
+
+ ///
+ public bool TryGet(UiControlOption option, out FloatConfigProperties? properties)
+ => this.gameConfigService.TryGet(option, out properties);
+
+ ///
+ public bool TryGet(UiControlOption option, out StringConfigProperties? properties)
+ => this.gameConfigService.TryGet(option, out properties);
+
+ ///
+ public void Set(SystemConfigOption option, bool value)
+ => this.gameConfigService.Set(option, value);
+
+ ///
+ public void Set(SystemConfigOption option, uint value)
+ => this.gameConfigService.Set(option, value);
+
+ ///
+ public void Set(SystemConfigOption option, float value)
+ => this.gameConfigService.Set(option, value);
+
+ ///
+ public void Set(SystemConfigOption option, string value)
+ => this.gameConfigService.Set(option, value);
+
+ ///
+ public void Set(UiConfigOption option, bool value)
+ => this.gameConfigService.Set(option, value);
+
+ ///
+ public void Set(UiConfigOption option, uint value)
+ => this.gameConfigService.Set(option, value);
+
+ ///
+ public void Set(UiConfigOption option, float value)
+ => this.gameConfigService.Set(option, value);
+
+ ///
+ public void Set(UiConfigOption option, string value)
+ => this.gameConfigService.Set(option, value);
+
+ ///
+ public void Set(UiControlOption option, bool value)
+ => this.gameConfigService.Set(option, value);
+
+ ///
+ public void Set(UiControlOption option, uint value)
+ => this.gameConfigService.Set(option, value);
+
+ ///
+ public void Set(UiControlOption option, float value)
+ => this.gameConfigService.Set(option, value);
+
+ ///
+ public void Set(UiControlOption option, string value)
+ => this.gameConfigService.Set(option, value);
+
+ private void ConfigChangedForward(object sender, ConfigChangeEvent data) => this.Changed?.Invoke(sender, data);
+
+ private void SystemConfigChangedForward(object sender, ConfigChangeEvent data) => this.SystemChanged?.Invoke(sender, data);
+
+ private void UiConfigConfigChangedForward(object sender, ConfigChangeEvent data) => this.UiConfigChanged?.Invoke(sender, data);
+
+ private void UiControlConfigChangedForward(object sender, ConfigChangeEvent data) => this.UiControlChanged?.Invoke(sender, data);
+}
diff --git a/Dalamud/Game/Config/GameConfigAddressResolver.cs b/Dalamud/Game/Config/GameConfigAddressResolver.cs
index 6a207807a..c171932a9 100644
--- a/Dalamud/Game/Config/GameConfigAddressResolver.cs
+++ b/Dalamud/Game/Config/GameConfigAddressResolver.cs
@@ -3,7 +3,7 @@
///
/// Game config system address resolver.
///
-public sealed class GameConfigAddressResolver : BaseAddressResolver
+internal sealed class GameConfigAddressResolver : BaseAddressResolver
{
///
/// Gets the address of the method called when any config option is changed.
@@ -11,7 +11,7 @@ public sealed class GameConfigAddressResolver : BaseAddressResolver
public nint ConfigChangeAddress { get; private set; }
///
- protected override void Setup64Bit(SigScanner scanner)
+ protected override void Setup64Bit(ISigScanner scanner)
{
this.ConfigChangeAddress = scanner.ScanText("E8 ?? ?? ?? ?? 48 8B 3F 49 3B 3E");
}
diff --git a/Dalamud/Game/Config/GameConfigSection.cs b/Dalamud/Game/Config/GameConfigSection.cs
index 6c87ad3cf..31e4a0b3f 100644
--- a/Dalamud/Game/Config/GameConfigSection.cs
+++ b/Dalamud/Game/Config/GameConfigSection.cs
@@ -1,5 +1,4 @@
-using System;
-using System.Collections.Concurrent;
+using System.Collections.Concurrent;
using System.Diagnostics;
using Dalamud.Memory;
@@ -18,11 +17,6 @@ public class GameConfigSection
private readonly ConcurrentDictionary indexMap = new();
private readonly ConcurrentDictionary enumMap = new();
- ///
- /// Event which is fired when a game config option is changed within the section.
- ///
- public event EventHandler Changed;
-
///
/// Initializes a new instance of the class.
///
@@ -54,6 +48,11 @@ public class GameConfigSection
/// Pointer to unmanaged ConfigBase.
internal unsafe delegate ConfigBase* GetConfigBaseDelegate();
+ ///
+ /// Event which is fired when a game config option is changed within the section.
+ ///
+ internal event EventHandler? Changed;
+
///
/// Gets the number of config entries contained within the section.
/// Some entries may be empty with no data.
diff --git a/Dalamud/Game/Config/UiConfigOption.cs b/Dalamud/Game/Config/UiConfigOption.cs
index 82f823ffe..aaa86230a 100644
--- a/Dalamud/Game/Config/UiConfigOption.cs
+++ b/Dalamud/Game/Config/UiConfigOption.cs
@@ -3473,4 +3473,67 @@ public enum UiConfigOption
///
[GameConfigOption("ItemInventryStoreEnd", ConfigType.UInt)]
ItemInventryStoreEnd,
+
+ ///
+ /// System option with the internal name HotbarXHBEditEnable.
+ /// This option is a UInt.
+ ///
+ [GameConfigOption("HotbarXHBEditEnable", ConfigType.UInt)]
+ HotbarXHBEditEnable,
+
+ ///
+ /// System option with the internal name NamePlateDispJobIconInPublicParty.
+ /// This option is a UInt.
+ ///
+ [GameConfigOption("NamePlateDispJobIconInPublicParty", ConfigType.UInt)]
+ NamePlateDispJobIconInPublicParty,
+
+ ///
+ /// System option with the internal name NamePlateDispJobIconInPublicOther.
+ /// This option is a UInt.
+ ///
+ [GameConfigOption("NamePlateDispJobIconInPublicOther", ConfigType.UInt)]
+ NamePlateDispJobIconInPublicOther,
+
+ ///
+ /// System option with the internal name NamePlateDispJobIconInInstanceParty.
+ /// This option is a UInt.
+ ///
+ [GameConfigOption("NamePlateDispJobIconInInstanceParty", ConfigType.UInt)]
+ NamePlateDispJobIconInInstanceParty,
+
+ ///
+ /// System option with the internal name NamePlateDispJobIconInInstanceOther.
+ /// This option is a UInt.
+ ///
+ [GameConfigOption("NamePlateDispJobIconInInstanceOther", ConfigType.UInt)]
+ NamePlateDispJobIconInInstanceOther,
+
+ ///
+ /// System option with the internal name CCProgressAllyFixLeftSide.
+ /// This option is a UInt.
+ ///
+ [GameConfigOption("CCProgressAllyFixLeftSide", ConfigType.UInt)]
+ CCProgressAllyFixLeftSide,
+
+ ///
+ /// System option with the internal name CCMapAllyFixLeftSide.
+ /// This option is a UInt.
+ ///
+ [GameConfigOption("CCMapAllyFixLeftSide", ConfigType.UInt)]
+ CCMapAllyFixLeftSide,
+
+ ///
+ /// System option with the internal name DispCCCountDown.
+ /// This option is a UInt.
+ ///
+ [GameConfigOption("DispCCCountDown", ConfigType.UInt)]
+ DispCCCountDown,
+
+ ///
+ /// System option with the internal name TelepoCategoryType.
+ /// This option is a UInt.
+ ///
+ [GameConfigOption("TelepoCategoryType", ConfigType.UInt)]
+ TelepoCategoryType,
}
diff --git a/Dalamud/Game/DutyState/DutyState.cs b/Dalamud/Game/DutyState/DutyState.cs
index 49fc874e3..e2e4aef15 100644
--- a/Dalamud/Game/DutyState/DutyState.cs
+++ b/Dalamud/Game/DutyState/DutyState.cs
@@ -1,25 +1,19 @@
-using System;
-using System.Runtime.InteropServices;
+using System.Runtime.InteropServices;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Hooking;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services;
-using Dalamud.Utility;
namespace Dalamud.Game.DutyState;
///
/// This class represents the state of the currently occupied duty.
///
-[PluginInterface]
[InterfaceVersion("1.0")]
-[ServiceManager.EarlyLoadedService]
-#pragma warning disable SA1015
-[ResolveVia]
-#pragma warning restore SA1015
-public unsafe class DutyState : IDisposable, IServiceType, IDutyState
+[ServiceManager.BlockingEarlyLoadedService]
+internal unsafe class DutyState : IInternalDisposableService, IDutyState
{
private readonly DutyStateAddressResolver address;
private readonly Hook contentDirectorNetworkMessageHook;
@@ -34,7 +28,7 @@ public unsafe class DutyState : IDisposable, IServiceType, IDutyState
private readonly ClientState.ClientState clientState = Service.Get();
[ServiceManager.ServiceConstructor]
- private DutyState(SigScanner sigScanner)
+ private DutyState(TargetSigScanner sigScanner)
{
this.address = new DutyStateAddressResolver();
this.address.Setup(sigScanner);
@@ -43,22 +37,24 @@ public unsafe class DutyState : IDisposable, IServiceType, IDutyState
this.framework.Update += this.FrameworkOnUpdateEvent;
this.clientState.TerritoryChanged += this.TerritoryOnChangedEvent;
+
+ this.contentDirectorNetworkMessageHook.Enable();
}
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate byte SetupContentDirectNetworkMessageDelegate(IntPtr a1, IntPtr a2, ushort* a3);
///
- public event EventHandler DutyStarted;
+ public event EventHandler? DutyStarted;
///
- public event EventHandler DutyWiped;
+ public event EventHandler? DutyWiped;
///
- public event EventHandler DutyRecommenced;
+ public event EventHandler? DutyRecommenced;
///
- public event EventHandler DutyCompleted;
+ public event EventHandler? DutyCompleted;
///
public bool IsDutyStarted { get; private set; }
@@ -66,19 +62,13 @@ public unsafe class DutyState : IDisposable, IServiceType, IDutyState
private bool CompletedThisTerritory { get; set; }
///
- void IDisposable.Dispose()
+ void IInternalDisposableService.DisposeService()
{
this.contentDirectorNetworkMessageHook.Dispose();
this.framework.Update -= this.FrameworkOnUpdateEvent;
this.clientState.TerritoryChanged -= this.TerritoryOnChangedEvent;
}
- [ServiceManager.CallWhenServicesReady]
- private void ContinueConstruction()
- {
- this.contentDirectorNetworkMessageHook.Enable();
- }
-
private byte ContentDirectorNetworkMessageDetour(IntPtr a1, IntPtr a2, ushort* a3)
{
var category = *a3;
@@ -92,33 +82,33 @@ public unsafe class DutyState : IDisposable, IServiceType, IDutyState
// Duty Commenced
case 0x4000_0001:
this.IsDutyStarted = true;
- this.DutyStarted.InvokeSafely(this, this.clientState.TerritoryType);
+ this.DutyStarted?.Invoke(this, this.clientState.TerritoryType);
break;
// Party Wipe
case 0x4000_0005:
this.IsDutyStarted = false;
- this.DutyWiped.InvokeSafely(this, this.clientState.TerritoryType);
+ this.DutyWiped?.Invoke(this, this.clientState.TerritoryType);
break;
// Duty Recommence
case 0x4000_0006:
this.IsDutyStarted = true;
- this.DutyRecommenced.InvokeSafely(this, this.clientState.TerritoryType);
+ this.DutyRecommenced?.Invoke(this, this.clientState.TerritoryType);
break;
// Duty Completed Flytext Shown
case 0x4000_0002 when !this.CompletedThisTerritory:
this.IsDutyStarted = false;
this.CompletedThisTerritory = true;
- this.DutyCompleted.InvokeSafely(this, this.clientState.TerritoryType);
+ this.DutyCompleted?.Invoke(this, this.clientState.TerritoryType);
break;
// Duty Completed
case 0x4000_0003 when !this.CompletedThisTerritory:
this.IsDutyStarted = false;
this.CompletedThisTerritory = true;
- this.DutyCompleted.InvokeSafely(this, this.clientState.TerritoryType);
+ this.DutyCompleted?.Invoke(this, this.clientState.TerritoryType);
break;
}
}
@@ -126,7 +116,7 @@ public unsafe class DutyState : IDisposable, IServiceType, IDutyState
return this.contentDirectorNetworkMessageHook.Original(a1, a2, a3);
}
- private void TerritoryOnChangedEvent(object? sender, ushort e)
+ private void TerritoryOnChangedEvent(ushort territoryId)
{
if (this.IsDutyStarted)
{
@@ -141,7 +131,7 @@ public unsafe class DutyState : IDisposable, IServiceType, IDutyState
/// Joining a duty in progress, or disconnecting and reconnecting will cause the player to miss the event.
///
/// Framework reference.
- private void FrameworkOnUpdateEvent(Framework framework1)
+ private void FrameworkOnUpdateEvent(IFramework framework1)
{
// If the duty hasn't been started, and has not been completed yet this territory
if (!this.IsDutyStarted && !this.CompletedThisTerritory)
@@ -161,11 +151,73 @@ public unsafe class DutyState : IDisposable, IServiceType, IDutyState
}
private bool IsBoundByDuty()
+ => this.condition.Any(ConditionFlag.BoundByDuty,
+ ConditionFlag.BoundByDuty56,
+ ConditionFlag.BoundByDuty95);
+
+ private bool IsInCombat()
+ => this.condition.Any(ConditionFlag.InCombat);
+}
+
+///
+/// Plugin scoped version of DutyState.
+///
+[PluginInterface]
+[InterfaceVersion("1.0")]
+[ServiceManager.ScopedService]
+#pragma warning disable SA1015
+[ResolveVia]
+#pragma warning restore SA1015
+internal class DutyStatePluginScoped : IInternalDisposableService, IDutyState
+{
+ [ServiceManager.ServiceDependency]
+ private readonly DutyState dutyStateService = Service.Get();
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ internal DutyStatePluginScoped()
{
- return this.condition[ConditionFlag.BoundByDuty] ||
- this.condition[ConditionFlag.BoundByDuty56] ||
- this.condition[ConditionFlag.BoundByDuty95];
+ this.dutyStateService.DutyStarted += this.DutyStartedForward;
+ this.dutyStateService.DutyWiped += this.DutyWipedForward;
+ this.dutyStateService.DutyRecommenced += this.DutyRecommencedForward;
+ this.dutyStateService.DutyCompleted += this.DutyCompletedForward;
}
- private bool IsInCombat() => this.condition[ConditionFlag.InCombat];
+ ///
+ public event EventHandler? DutyStarted;
+
+ ///
+ public event EventHandler? DutyWiped;
+
+ ///
+ public event EventHandler? DutyRecommenced;
+
+ ///
+ public event EventHandler? DutyCompleted;
+
+ ///
+ public bool IsDutyStarted => this.dutyStateService.IsDutyStarted;
+
+ ///
+ void IInternalDisposableService.DisposeService()
+ {
+ this.dutyStateService.DutyStarted -= this.DutyStartedForward;
+ this.dutyStateService.DutyWiped -= this.DutyWipedForward;
+ this.dutyStateService.DutyRecommenced -= this.DutyRecommencedForward;
+ this.dutyStateService.DutyCompleted -= this.DutyCompletedForward;
+
+ this.DutyStarted = null;
+ this.DutyWiped = null;
+ this.DutyRecommenced = null;
+ this.DutyCompleted = null;
+ }
+
+ private void DutyStartedForward(object sender, ushort territoryId) => this.DutyStarted?.Invoke(sender, territoryId);
+
+ private void DutyWipedForward(object sender, ushort territoryId) => this.DutyWiped?.Invoke(sender, territoryId);
+
+ private void DutyRecommencedForward(object sender, ushort territoryId) => this.DutyRecommenced?.Invoke(sender, territoryId);
+
+ private void DutyCompletedForward(object sender, ushort territoryId) => this.DutyCompleted?.Invoke(sender, territoryId);
}
diff --git a/Dalamud/Game/DutyState/DutyStateAddressResolver.cs b/Dalamud/Game/DutyState/DutyStateAddressResolver.cs
index 801e5ef55..c7160bddb 100644
--- a/Dalamud/Game/DutyState/DutyStateAddressResolver.cs
+++ b/Dalamud/Game/DutyState/DutyStateAddressResolver.cs
@@ -1,11 +1,9 @@
-using System;
-
namespace Dalamud.Game.DutyState;
///
/// Duty state memory address resolver.
///
-public class DutyStateAddressResolver : BaseAddressResolver
+internal class DutyStateAddressResolver : BaseAddressResolver
{
///
/// Gets the address of the method which is called when the client receives a content director update.
@@ -16,7 +14,7 @@ public class DutyStateAddressResolver : BaseAddressResolver
/// Scan for and setup any configured address pointers.
///
/// The signature scanner to facilitate setup.
- protected override void Setup64Bit(SigScanner sig)
+ protected override void Setup64Bit(ISigScanner sig)
{
this.ContentDirectorNetworkMessage = sig.ScanText("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC ?? 48 8B D9 49 8B F8 41 0F B7 08");
}
diff --git a/Dalamud/Game/Framework.cs b/Dalamud/Game/Framework.cs
index b3083e913..9e520daab 100644
--- a/Dalamud/Game/Framework.cs
+++ b/Dalamud/Game/Framework.cs
@@ -1,4 +1,4 @@
-using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
@@ -12,19 +12,21 @@ using Dalamud.Game.Gui.Toast;
using Dalamud.Hooking;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
+using Dalamud.Logging.Internal;
+using Dalamud.Plugin.Services;
using Dalamud.Utility;
-using Serilog;
namespace Dalamud.Game;
///
/// This class represents the Framework of the native game client and grants access to various subsystems.
///
-[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
-public sealed class Framework : IDisposable, IServiceType
+internal sealed class Framework : IInternalDisposableService, IFramework
{
+ private static readonly ModuleLog Log = new("Framework");
+
private static readonly Stopwatch StatsStopwatch = new();
private readonly GameLifecycle lifecycle;
@@ -35,34 +37,43 @@ public sealed class Framework : IDisposable, IServiceType
private readonly Hook updateHook;
private readonly Hook destroyHook;
+ private readonly FrameworkAddressResolver addressResolver;
+
[ServiceManager.ServiceDependency]
private readonly DalamudConfiguration configuration = Service.Get();
- private readonly object runOnNextTickTaskListSync = new();
- private List runOnNextTickTaskList = new();
- private List runOnNextTickTaskList2 = new();
+ private readonly CancellationTokenSource frameworkDestroy;
+ private readonly ThreadBoundTaskScheduler frameworkThreadTaskScheduler;
- private Thread? frameworkUpdateThread;
+ private readonly ConcurrentDictionary
+ tickDelayedTaskCompletionSources = new();
+
+ private ulong tickCounter;
[ServiceManager.ServiceConstructor]
- private Framework(SigScanner sigScanner, GameLifecycle lifecycle)
+ private Framework(TargetSigScanner sigScanner, GameLifecycle lifecycle)
{
this.lifecycle = lifecycle;
this.hitchDetector = new HitchDetector("FrameworkUpdate", this.configuration.FrameworkUpdateHitch);
- this.Address = new FrameworkAddressResolver();
- this.Address.Setup(sigScanner);
+ this.addressResolver = new FrameworkAddressResolver();
+ this.addressResolver.Setup(sigScanner);
- this.updateHook = Hook.FromAddress(this.Address.TickAddress, this.HandleFrameworkUpdate);
- this.destroyHook = Hook.FromAddress(this.Address.DestroyAddress, this.HandleFrameworkDestroy);
+ this.frameworkDestroy = new();
+ this.frameworkThreadTaskScheduler = new();
+ this.FrameworkThreadTaskFactory = new(
+ this.frameworkDestroy.Token,
+ TaskCreationOptions.None,
+ TaskContinuationOptions.None,
+ this.frameworkThreadTaskScheduler);
+
+ this.updateHook = Hook.FromAddress(this.addressResolver.TickAddress, this.HandleFrameworkUpdate);
+ this.destroyHook = Hook.FromAddress(this.addressResolver.DestroyAddress, this.HandleFrameworkDestroy);
+
+ this.updateHook.Enable();
+ this.destroyHook.Enable();
}
- ///
- /// A delegate type used with the event.
- ///
- /// The Framework instance.
- public delegate void OnUpdateDelegate(Framework framework);
-
///
/// A delegate type used during the native Framework::destroy.
///
@@ -70,21 +81,11 @@ public sealed class Framework : IDisposable, IServiceType
/// A value indicating if the call was successful.
public delegate bool OnRealDestroyDelegate(IntPtr framework);
- ///
- /// A delegate type used during the native Framework::free.
- ///
- /// The native Framework address.
- public delegate IntPtr OnDestroyDelegate();
-
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate bool OnUpdateDetour(IntPtr framework);
- private delegate IntPtr OnDestroyDetour(); // OnDestroyDelegate
-
- ///
- /// Event that gets fired every time the game framework updates.
- ///
- public event OnUpdateDelegate Update;
+ ///
+ public event IFramework.OnUpdateDelegate? Update;
///
/// Gets or sets a value indicating whether the collection of stats is enabled.
@@ -96,55 +97,86 @@ public sealed class Framework : IDisposable, IServiceType
///
public static Dictionary> StatsHistory { get; } = new();
- ///
- /// Gets a raw pointer to the instance of Client::Framework.
- ///
- public FrameworkAddressResolver Address { get; }
-
- ///
- /// Gets the last time that the Framework Update event was triggered.
- ///
+ ///
public DateTime LastUpdate { get; private set; } = DateTime.MinValue;
- ///
- /// Gets the last time in UTC that the Framework Update event was triggered.
- ///
+ ///
public DateTime LastUpdateUTC { get; private set; } = DateTime.MinValue;
- ///
- /// Gets the delta between the last Framework Update and the currently executing one.
- ///
+ ///
public TimeSpan UpdateDelta { get; private set; } = TimeSpan.Zero;
- ///
- /// Gets a value indicating whether currently executing code is running in the game's framework update thread.
- ///
- public bool IsInFrameworkUpdateThread => Thread.CurrentThread == this.frameworkUpdateThread;
+ ///
+ public bool IsInFrameworkUpdateThread => this.frameworkThreadTaskScheduler.IsOnBoundThread;
+
+ ///
+ public bool IsFrameworkUnloading => this.frameworkDestroy.IsCancellationRequested;
///
- /// Gets a value indicating whether game Framework is unloading.
+ /// Gets the list of update sub-delegates that didn't get updated this frame.
///
- public bool IsFrameworkUnloading { get; internal set; }
+ internal List NonUpdatedSubDelegates { get; private set; } = new();
///
/// Gets or sets a value indicating whether to dispatch update events.
///
internal bool DispatchUpdateEvents { get; set; } = true;
- ///
- /// Run given function right away if this function has been called from game's Framework.Update thread, or otherwise run on next Framework.Update call.
- ///
- /// Return type.
- /// Function to call.
- /// Task representing the pending or already completed function.
+ private TaskFactory FrameworkThreadTaskFactory { get; }
+
+ ///
+ public TaskFactory GetTaskFactory() => this.FrameworkThreadTaskFactory;
+
+ ///
+ 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;
+ }
+
+ ///
+ public Task Run(Action action, CancellationToken cancellationToken = default)
+ {
+ if (cancellationToken == default)
+ cancellationToken = this.FrameworkThreadTaskFactory.CancellationToken;
+ return this.FrameworkThreadTaskFactory.StartNew(action, cancellationToken);
+ }
+
+ ///
+ public Task Run(Func action, CancellationToken cancellationToken = default)
+ {
+ if (cancellationToken == default)
+ cancellationToken = this.FrameworkThreadTaskFactory.CancellationToken;
+ return this.FrameworkThreadTaskFactory.StartNew(action, cancellationToken);
+ }
+
+ ///
+ public Task Run(Func action, CancellationToken cancellationToken = default)
+ {
+ if (cancellationToken == default)
+ cancellationToken = this.FrameworkThreadTaskFactory.CancellationToken;
+ return this.FrameworkThreadTaskFactory.StartNew(action, cancellationToken).Unwrap();
+ }
+
+ ///
+ public Task Run(Func> action, CancellationToken cancellationToken = default)
+ {
+ if (cancellationToken == default)
+ cancellationToken = this.FrameworkThreadTaskFactory.CancellationToken;
+ return this.FrameworkThreadTaskFactory.StartNew(action, cancellationToken).Unwrap();
+ }
+
+ ///
public Task RunOnFrameworkThread(Func func) =>
this.IsInFrameworkUpdateThread || this.IsFrameworkUnloading ? Task.FromResult(func()) : this.RunOnTick(func);
- ///
- /// Run given function right away if this function has been called from game's Framework.Update thread, or otherwise run on next Framework.Update call.
- ///
- /// Function to call.
- /// Task representing the pending or already completed function.
+ ///
public Task RunOnFrameworkThread(Action action)
{
if (this.IsInFrameworkUpdateThread || this.IsFrameworkUnloading)
@@ -165,32 +197,15 @@ public sealed class Framework : IDisposable, IServiceType
}
}
- ///
- /// Run given function right away if this function has been called from game's Framework.Update thread, or otherwise run on next Framework.Update call.
- ///
- /// Return type.
- /// Function to call.
- /// Task representing the pending or already completed function.
+ ///
public Task RunOnFrameworkThread(Func> func) =>
this.IsInFrameworkUpdateThread || this.IsFrameworkUnloading ? func() : this.RunOnTick(func);
- ///
- /// Run given function right away if this function has been called from game's Framework.Update thread, or otherwise run on next Framework.Update call.
- ///
- /// Function to call.
- /// Task representing the pending or already completed function.
+ ///
public Task RunOnFrameworkThread(Func func) =>
this.IsInFrameworkUpdateThread || this.IsFrameworkUnloading ? func() : this.RunOnTick(func);
- ///
- /// Run given function in upcoming Framework.Tick call.
- ///
- /// Return type.
- /// Function to call.
- /// Wait for given timespan before calling this function.
- /// Count given number of Framework.Tick calls before calling this function. This takes precedence over delay parameter.
- /// Cancellation token which will prevent the execution of this function if wait conditions are not met.
- /// Task representing the pending function.
+ ///
public Task RunOnTick(Func func, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default)
{
if (this.IsFrameworkUnloading)
@@ -203,30 +218,21 @@ public sealed class Framework : IDisposable, IServiceType
return Task.FromCanceled(cts.Token);
}
- var tcs = new TaskCompletionSource();
- lock (this.runOnNextTickTaskListSync)
- {
- this.runOnNextTickTaskList.Add(new RunOnNextTickTaskFunc()
+ 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);
}
- ///
- /// Run given function in upcoming Framework.Tick call.
- ///
- /// Function to call.
- /// Wait for given timespan before calling this function.
- /// Count given number of Framework.Tick calls before calling this function. This takes precedence over delay parameter.
- /// Cancellation token which will prevent the execution of this function if wait conditions are not met.
- /// Task representing the pending function.
+ ///
public Task RunOnTick(Action action, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default)
{
if (this.IsFrameworkUnloading)
@@ -239,31 +245,21 @@ public sealed class Framework : IDisposable, IServiceType
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);
}
- ///
- /// Run given function in upcoming Framework.Tick call.
- ///
- /// Return type.
- /// Function to call.
- /// Wait for given timespan before calling this function.
- /// Count given number of Framework.Tick calls before calling this function. This takes precedence over delay parameter.
- /// Cancellation token which will prevent the execution of this function if wait conditions are not met.
- /// Task representing the pending function.
+ ///
public Task RunOnTick(Func> func, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default)
{
if (this.IsFrameworkUnloading)
@@ -276,30 +272,21 @@ public sealed class Framework : IDisposable, IServiceType
return Task.FromCanceled(cts.Token);
}
- var tcs = new TaskCompletionSource>();
- lock (this.runOnNextTickTaskListSync)
- {
- this.runOnNextTickTaskList.Add(new RunOnNextTickTaskFunc>()
+ 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();
}
- ///
- /// Run given function in upcoming Framework.Tick call.
- ///
- /// Function to call.
- /// Wait for given timespan before calling this function.
- /// Count given number of Framework.Tick calls before calling this function. This takes precedence over delay parameter.
- /// Cancellation token which will prevent the execution of this function if wait conditions are not met.
- /// Task representing the pending function.
+ ///
public Task RunOnTick(Func func, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default)
{
if (this.IsFrameworkUnloading)
@@ -312,26 +299,24 @@ public sealed class Framework : IDisposable, IServiceType
return Task.FromCanceled(cts.Token);
}
- var tcs = new TaskCompletionSource();
- lock (this.runOnNextTickTaskListSync)
- {
- this.runOnNextTickTaskList.Add(new RunOnNextTickTaskFunc()
+ 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();
}
///
/// Dispose of managed and unmanaged resources.
///
- void IDisposable.Dispose()
+ void IInternalDisposableService.DisposeService()
{
this.RunOnFrameworkThread(() =>
{
@@ -348,31 +333,62 @@ public sealed class Framework : IDisposable, IServiceType
this.updateStopwatch.Reset();
StatsStopwatch.Reset();
}
-
- [ServiceManager.CallWhenServicesReady]
- private void ContinueConstruction()
+
+ ///
+ /// Adds a update time to the stats history.
+ ///
+ /// Delegate Name.
+ /// Runtime.
+ internal static void AddToStats(string key, double ms)
{
- this.updateHook.Enable();
- this.destroyHook.Enable();
+ if (!StatsHistory.ContainsKey(key))
+ StatsHistory.Add(key, new List());
+
+ StatsHistory[key].Add(ms);
+
+ if (StatsHistory[key].Count > 1000)
+ {
+ StatsHistory[key].RemoveRange(0, StatsHistory[key].Count - 1000);
+ }
}
- private void RunPendingTickTasks()
+ ///
+ /// Profiles each sub-delegate in the eventDelegate and logs to StatsHistory.
+ ///
+ /// The Delegate to Profile.
+ /// The Framework Instance to pass to delegate.
+ internal void ProfileAndInvoke(IFramework.OnUpdateDelegate? eventDelegate, IFramework frameworkInstance)
{
- if (this.runOnNextTickTaskList.Count == 0 && this.runOnNextTickTaskList2.Count == 0)
- return;
+ if (eventDelegate is null) return;
+
+ var invokeList = eventDelegate.GetInvocationList();
- for (var i = 0; i < 2; i++)
+ // Individually invoke OnUpdate handlers and time them.
+ foreach (var d in invokeList)
{
- lock (this.runOnNextTickTaskListSync)
- (this.runOnNextTickTaskList, this.runOnNextTickTaskList2) = (this.runOnNextTickTaskList2, this.runOnNextTickTaskList);
+ var stopwatch = Stopwatch.StartNew();
+ try
+ {
+ d.Method.Invoke(d.Target, new object[] { frameworkInstance });
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "Exception while dispatching Framework::Update event.");
+ }
- this.runOnNextTickTaskList2.RemoveAll(x => x.Run());
+ stopwatch.Stop();
+
+ var key = $"{d.Target}::{d.Method.Name}";
+ if (this.NonUpdatedSubDelegates.Contains(key))
+ this.NonUpdatedSubDelegates.Remove(key);
+
+ AddToStats(key, stopwatch.Elapsed.TotalMilliseconds);
}
}
private bool HandleFrameworkUpdate(IntPtr framework)
{
- this.frameworkUpdateThread ??= Thread.CurrentThread;
+ this.frameworkThreadTaskScheduler.BoundThread ??= Thread.CurrentThread;
ThreadSafety.MarkMainThread();
@@ -404,65 +420,42 @@ public sealed class Framework : IDisposable, IServiceType
this.LastUpdate = DateTime.Now;
this.LastUpdateUTC = DateTime.UtcNow;
-
- void AddToStats(string key, double ms)
+ this.tickCounter++;
+ foreach (var (k, (expiry, ct)) in this.tickDelayedTaskCompletionSources)
{
- if (!StatsHistory.ContainsKey(key))
- StatsHistory.Add(key, new List());
+ if (ct.IsCancellationRequested)
+ k.SetCanceled(ct);
+ else if (expiry <= this.tickCounter)
+ k.SetResult();
+ else
+ continue;
- StatsHistory[key].Add(ms);
-
- if (StatsHistory[key].Count > 1000)
- {
- StatsHistory[key].RemoveRange(0, StatsHistory[key].Count - 1000);
- }
+ 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)
{
// Stat Tracking for Framework Updates
- var invokeList = this.Update.GetInvocationList();
- var notUpdated = StatsHistory.Keys.ToList();
-
- // Individually invoke OnUpdate handlers and time them.
- foreach (var d in invokeList)
- {
- StatsStopwatch.Restart();
- try
- {
- d.Method.Invoke(d.Target, new object[] { this });
- }
- catch (Exception ex)
- {
- Log.Error(ex, "Exception while dispatching Framework::Update event.");
- }
-
- StatsStopwatch.Stop();
-
- var key = $"{d.Target}::{d.Method.Name}";
- if (notUpdated.Contains(key))
- notUpdated.Remove(key);
-
- AddToStats(key, StatsStopwatch.Elapsed.TotalMilliseconds);
- }
+ this.NonUpdatedSubDelegates = StatsHistory.Keys.ToList();
+ this.ProfileAndInvoke(this.Update, this);
// Cleanup handlers that are no longer being called
- foreach (var key in notUpdated)
+ foreach (var key in this.NonUpdatedSubDelegates)
{
- if (key == nameof(this.RunPendingTickTasks))
+ if (key == nameof(this.FrameworkThreadTaskFactory))
continue;
if (StatsHistory[key].Count > 0)
@@ -489,8 +482,11 @@ public sealed class Framework : IDisposable, IServiceType
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();
@@ -498,93 +494,126 @@ public sealed class Framework : IDisposable, IServiceType
Log.Information("Framework::Destroy!");
Service.Get().Unload();
- this.RunPendingTickTasks();
+ this.frameworkThreadTaskScheduler.Run();
ServiceManager.WaitForServiceUnload();
Log.Information("Framework::Destroy OK!");
return this.destroyHook.OriginalDisposeSafe(framework);
}
+}
- private abstract class RunOnNextTickTaskBase
+///
+/// Plugin-scoped version of a Framework service.
+///
+[PluginInterface]
+[InterfaceVersion("1.0")]
+[ServiceManager.ScopedService]
+#pragma warning disable SA1015
+[ResolveVia]
+#pragma warning restore SA1015
+internal class FrameworkPluginScoped : IInternalDisposableService, IFramework
+{
+ [ServiceManager.ServiceDependency]
+ private readonly Framework frameworkService = Service.Get();
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ internal FrameworkPluginScoped()
{
- 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();
+ this.frameworkService.Update += this.OnUpdateForward;
}
- private class RunOnNextTickTaskFunc : RunOnNextTickTaskBase
+ ///
+ public event IFramework.OnUpdateDelegate? Update;
+
+ ///
+ public DateTime LastUpdate => this.frameworkService.LastUpdate;
+
+ ///
+ public DateTime LastUpdateUTC => this.frameworkService.LastUpdateUTC;
+
+ ///
+ public TimeSpan UpdateDelta => this.frameworkService.UpdateDelta;
+
+ ///
+ public bool IsInFrameworkUpdateThread => this.frameworkService.IsInFrameworkUpdateThread;
+
+ ///
+ public bool IsFrameworkUnloading => this.frameworkService.IsFrameworkUnloading;
+
+ ///
+ void IInternalDisposableService.DisposeService()
{
- internal TaskCompletionSource TaskCompletionSource { get; init; }
+ this.frameworkService.Update -= this.OnUpdateForward;
- internal Func 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();
- }
+ this.Update = null;
}
- private class RunOnNextTickTaskAction : RunOnNextTickTaskBase
+ ///
+ public TaskFactory GetTaskFactory() => this.frameworkService.GetTaskFactory();
+
+ ///
+ public Task DelayTicks(long numTicks, CancellationToken cancellationToken = default) =>
+ this.frameworkService.DelayTicks(numTicks, cancellationToken);
+
+ ///
+ public Task Run(Action action, CancellationToken cancellationToken = default) =>
+ this.frameworkService.Run(action, cancellationToken);
+
+ ///
+ public Task Run(Func action, CancellationToken cancellationToken = default) =>
+ this.frameworkService.Run(action, cancellationToken);
+
+ ///
+ public Task Run(Func action, CancellationToken cancellationToken = default) =>
+ this.frameworkService.Run(action, cancellationToken);
+
+ ///
+ public Task Run(Func> action, CancellationToken cancellationToken = default) =>
+ this.frameworkService.Run(action, cancellationToken);
+
+ ///
+ public Task RunOnFrameworkThread(Func func)
+ => this.frameworkService.RunOnFrameworkThread(func);
+
+ ///
+ public Task RunOnFrameworkThread(Action action)
+ => this.frameworkService.RunOnFrameworkThread(action);
+
+ ///
+ public Task RunOnFrameworkThread(Func> func)
+ => this.frameworkService.RunOnFrameworkThread(func);
+
+ ///
+ public Task RunOnFrameworkThread(Func func)
+ => this.frameworkService.RunOnFrameworkThread(func);
+
+ ///
+ public Task RunOnTick(Func func, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default)
+ => this.frameworkService.RunOnTick(func, delay, delayTicks, cancellationToken);
+
+ ///
+ public Task RunOnTick(Action action, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default)
+ => this.frameworkService.RunOnTick(action, delay, delayTicks, cancellationToken);
+
+ ///
+ public Task RunOnTick(Func> func, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default)
+ => this.frameworkService.RunOnTick(func, delay, delayTicks, cancellationToken);
+
+ ///
+ public Task RunOnTick(Func func, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default)
+ => this.frameworkService.RunOnTick(func, delay, delayTicks, cancellationToken);
+
+ private void OnUpdateForward(IFramework framework)
{
- internal TaskCompletionSource TaskCompletionSource { get; init; }
-
- internal Action Action { get; init; }
-
- protected override void RunImpl()
+ if (Framework.StatsEnabled && this.Update != null)
{
- try
- {
- this.Action();
- this.TaskCompletionSource.SetResult();
- }
- catch (Exception ex)
- {
- this.TaskCompletionSource.SetException(ex);
- }
+ this.frameworkService.ProfileAndInvoke(this.Update, framework);
}
-
- protected override void CancelImpl()
+ else
{
- this.TaskCompletionSource.SetCanceled();
+ this.Update?.Invoke(framework);
}
}
}
diff --git a/Dalamud/Game/FrameworkAddressResolver.cs b/Dalamud/Game/FrameworkAddressResolver.cs
index e3d128f0f..39ae15155 100644
--- a/Dalamud/Game/FrameworkAddressResolver.cs
+++ b/Dalamud/Game/FrameworkAddressResolver.cs
@@ -5,14 +5,8 @@ namespace Dalamud.Game;
///
/// The address resolver for the class.
///
-public sealed unsafe class FrameworkAddressResolver : BaseAddressResolver
+internal sealed class FrameworkAddressResolver : BaseAddressResolver
{
- ///
- /// Gets the base address of the Framework object.
- ///
- [Obsolete("Please use FFXIVClientStructs.FFXIV.Client.System.Framework.Framework.Instance() instead.")]
- public IntPtr BaseAddress => new(FFXIVClientStructs.FFXIV.Client.System.Framework.Framework.Instance());
-
///
/// Gets the address for the function that is called once the Framework is destroyed.
///
@@ -29,12 +23,12 @@ public sealed unsafe class FrameworkAddressResolver : BaseAddressResolver
public IntPtr TickAddress { get; private set; }
///
- protected override void Setup64Bit(SigScanner sig)
+ protected override void Setup64Bit(ISigScanner sig)
{
this.SetupFramework(sig);
}
- private void SetupFramework(SigScanner scanner)
+ private void SetupFramework(ISigScanner scanner)
{
this.DestroyAddress =
scanner.ScanText("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 48 8B 3D ?? ?? ?? ?? 48 8B D9 48 85 FF");
diff --git a/Dalamud/Game/GameLifecycle.cs b/Dalamud/Game/GameLifecycle.cs
index 5c1acc989..4192d055b 100644
--- a/Dalamud/Game/GameLifecycle.cs
+++ b/Dalamud/Game/GameLifecycle.cs
@@ -15,7 +15,7 @@ namespace Dalamud.Game;
#pragma warning disable SA1015
[ResolveVia]
#pragma warning restore SA1015
-public class GameLifecycle : IServiceType, IGameLifecycle
+internal class GameLifecycle : IServiceType, IGameLifecycle
{
private readonly CancellationTokenSource dalamudUnloadCts = new();
private readonly CancellationTokenSource gameShutdownCts = new();
diff --git a/Dalamud/Game/Gui/ChatGui.cs b/Dalamud/Game/Gui/ChatGui.cs
index 93185caf9..e0b90b382 100644
--- a/Dalamud/Game/Gui/ChatGui.cs
+++ b/Dalamud/Game/Gui/ChatGui.cs
@@ -1,29 +1,38 @@
-using System;
using System.Collections.Generic;
+using System.Collections.Immutable;
using System.Linq;
using System.Runtime.InteropServices;
using Dalamud.Configuration.Internal;
-using Dalamud.Game.Libc;
using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads;
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 Serilog;
+using FFXIVClientStructs.FFXIV.Client.System.String;
+using FFXIVClientStructs.FFXIV.Client.UI.Misc;
namespace Dalamud.Game.Gui;
+// TODO(api10): Update IChatGui, ChatGui and XivChatEntry to use correct types and names:
+// "uint SenderId" should be "int Timestamp".
+// "IntPtr Parameters" should be something like "bool Silent". It suppresses new message sounds in certain channels.
+// This has to be a 1 byte boolean, so only change it to bool if marshalling is disabled.
+
///
/// This class handles interacting with the native chat UI.
///
-[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
-public sealed class ChatGui : IDisposable, IServiceType
+internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
{
+ private static readonly ModuleLog Log = new("ChatGui");
+
private readonly ChatGuiAddressResolver address;
private readonly Queue chatQueue = new();
@@ -36,62 +45,25 @@ public sealed class ChatGui : IDisposable, IServiceType
[ServiceManager.ServiceDependency]
private readonly DalamudConfiguration configuration = Service.Get();
- [ServiceManager.ServiceDependency]
- private readonly LibcFunction libcFunction = Service.Get();
-
- private IntPtr baseAddress = IntPtr.Zero;
+ private ImmutableDictionary<(string PluginName, uint CommandId), Action>? dalamudLinkHandlersCopy;
[ServiceManager.ServiceConstructor]
- private ChatGui(SigScanner sigScanner)
+ private ChatGui(TargetSigScanner sigScanner)
{
this.address = new ChatGuiAddressResolver();
this.address.Setup(sigScanner);
- this.printMessageHook = Hook.FromAddress(this.address.PrintMessage, this.HandlePrintMessageDetour);
+ this.printMessageHook = Hook.FromAddress((nint)RaptureLogModule.Addresses.PrintMessage.Value, this.HandlePrintMessageDetour);
this.populateItemLinkHook = Hook.FromAddress(this.address.PopulateItemLinkObject, this.HandlePopulateItemLinkDetour);
this.interactableLinkClickedHook = Hook.FromAddress(this.address.InteractableLinkClicked, this.InteractableLinkClickedDetour);
+
+ this.printMessageHook.Enable();
+ this.populateItemLinkHook.Enable();
+ this.interactableLinkClickedHook.Enable();
}
-
- ///
- /// A delegate type used with the event.
- ///
- /// The type of chat.
- /// The sender ID.
- /// The sender name.
- /// The message sent.
- /// A value indicating whether the message was handled or should be propagated.
- public delegate void OnMessageDelegate(XivChatType type, uint senderId, ref SeString sender, ref SeString message, ref bool isHandled);
-
- ///
- /// A delegate type used with the event.
- ///
- /// The type of chat.
- /// The sender ID.
- /// The sender name.
- /// The message sent.
- /// A value indicating whether the message was handled or should be propagated.
- public delegate void OnCheckMessageHandledDelegate(XivChatType type, uint senderId, ref SeString sender, ref SeString message, ref bool isHandled);
-
- ///
- /// A delegate type used with the event.
- ///
- /// The type of chat.
- /// The sender ID.
- /// The sender name.
- /// The message sent.
- public delegate void OnMessageHandledDelegate(XivChatType type, uint senderId, SeString sender, SeString message);
-
- ///
- /// A delegate type used with the event.
- ///
- /// The type of chat.
- /// The sender ID.
- /// The sender name.
- /// The message sent.
- public delegate void OnMessageUnhandledDelegate(XivChatType type, uint senderId, SeString sender, SeString message);
-
+
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
- private delegate IntPtr PrintMessageDelegate(IntPtr manager, XivChatType chatType, IntPtr senderName, IntPtr message, uint senderId, IntPtr parameter);
+ private delegate uint PrintMessageDelegate(RaptureLogModule* manager, XivChatType chatType, Utf8String* sender, Utf8String* message, int timestamp, byte silent);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate void PopulateItemLinkDelegate(IntPtr linkObjectPtr, IntPtr itemInfoPtr);
@@ -99,116 +71,81 @@ public sealed class ChatGui : IDisposable, IServiceType
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate void InteractableLinkClickedDelegate(IntPtr managerPtr, IntPtr messagePtr);
- ///
- /// Event that will be fired when a chat message is sent to chat by the game.
- ///
- public event OnMessageDelegate ChatMessage;
+ ///
+ public event IChatGui.OnMessageDelegate? ChatMessage;
- ///
- /// Event that allows you to stop messages from appearing in chat by setting the isHandled parameter to true.
- ///
- public event OnCheckMessageHandledDelegate CheckMessageHandled;
+ ///
+ public event IChatGui.OnCheckMessageHandledDelegate? CheckMessageHandled;
- ///
- /// Event that will be fired when a chat message is handled by Dalamud or a Plugin.
- ///
- public event OnMessageHandledDelegate ChatMessageHandled;
+ ///
+ public event IChatGui.OnMessageHandledDelegate? ChatMessageHandled;
- ///
- /// Event that will be fired when a chat message is not handled by Dalamud or a Plugin.
- ///
- public event OnMessageUnhandledDelegate ChatMessageUnhandled;
+ ///
+ public event IChatGui.OnMessageUnhandledDelegate? ChatMessageUnhandled;
- ///
- /// Gets the ID of the last linked item.
- ///
+ ///
public int LastLinkedItemId { get; private set; }
- ///
- /// Gets the flags of the last linked item.
- ///
+ ///
public byte LastLinkedItemFlags { get; private set; }
+ ///
+ public IReadOnlyDictionary<(string PluginName, uint CommandId), Action> RegisteredLinkHandlers
+ {
+ get
+ {
+ var copy = this.dalamudLinkHandlersCopy;
+ if (copy is not null)
+ return copy;
+
+ lock (this.dalamudLinkHandlers)
+ {
+ return this.dalamudLinkHandlersCopy ??=
+ this.dalamudLinkHandlers.ToImmutableDictionary(x => x.Key, x => x.Value);
+ }
+ }
+ }
+
///
/// Dispose of managed and unmanaged resources.
///
- void IDisposable.Dispose()
+ void IInternalDisposableService.DisposeService()
{
this.printMessageHook.Dispose();
this.populateItemLinkHook.Dispose();
this.interactableLinkClickedHook.Dispose();
}
- ///
- /// Queue a chat message. While method is named as PrintChat, it only add a entry to the queue,
- /// later to be processed when UpdateQueue() is called.
- ///
- /// A message to send.
- public void PrintChat(XivChatEntry chat)
+ ///
+ public void Print(XivChatEntry chat)
{
this.chatQueue.Enqueue(chat);
}
-
- ///
- /// Queue a chat message. While method is named as PrintChat (it calls it internally), it only add a entry to the queue,
- /// later to be processed when UpdateQueue() is called.
- ///
- /// A message to send.
- public void Print(string message)
+
+ ///
+ public void Print(string message, string? messageTag = null, ushort? tagColor = null)
{
- // Log.Verbose("[CHATGUI PRINT REGULAR]{0}", message);
- this.PrintChat(new XivChatEntry
- {
- Message = message,
- Type = this.configuration.GeneralChatType,
- });
+ this.PrintTagged(message, this.configuration.GeneralChatType, messageTag, tagColor);
}
-
- ///
- /// Queue a chat message. While method is named as PrintChat (it calls it internally), it only add a entry to the queue,
- /// later to be processed when UpdateQueue() is called.
- ///
- /// A message to send.
- public void Print(SeString message)
+
+ ///
+ public void Print(SeString message, string? messageTag = null, ushort? tagColor = null)
{
- // Log.Verbose("[CHATGUI PRINT SESTRING]{0}", message.TextValue);
- this.PrintChat(new XivChatEntry
- {
- Message = message,
- Type = this.configuration.GeneralChatType,
- });
+ this.PrintTagged(message, this.configuration.GeneralChatType, messageTag, tagColor);
}
-
- ///
- /// Queue an error chat message. While method is named as PrintChat (it calls it internally), it only add a entry to
- /// the queue, later to be processed when UpdateQueue() is called.
- ///
- /// A message to send.
- public void PrintError(string message)
+
+ ///
+ public void PrintError(string message, string? messageTag = null, ushort? tagColor = null)
{
- // Log.Verbose("[CHATGUI PRINT REGULAR ERROR]{0}", message);
- this.PrintChat(new XivChatEntry
- {
- Message = message,
- Type = XivChatType.Urgent,
- });
+ this.PrintTagged(message, XivChatType.Urgent, messageTag, tagColor);
}
-
- ///
- /// Queue an error chat message. While method is named as PrintChat (it calls it internally), it only add a entry to
- /// the queue, later to be processed when UpdateQueue() is called.
- ///
- /// A message to send.
- public void PrintError(SeString message)
+
+ ///
+ public void PrintError(SeString message, string? messageTag = null, ushort? tagColor = null)
{
- // Log.Verbose("[CHATGUI PRINT SESTRING ERROR]{0}", message.TextValue);
- this.PrintChat(new XivChatEntry
- {
- Message = message,
- Type = XivChatType.Urgent,
- });
+ this.PrintTagged(message, XivChatType.Urgent, messageTag, tagColor);
}
-
+
///
/// Process a chat queue.
///
@@ -218,18 +155,13 @@ public sealed class ChatGui : IDisposable, IServiceType
{
var chat = this.chatQueue.Dequeue();
- if (this.baseAddress == IntPtr.Zero)
- {
- continue;
- }
+ var sender = Utf8String.FromSequence(chat.Name.Encode());
+ var message = Utf8String.FromSequence(chat.Message.Encode());
- var senderRaw = (chat.Name ?? string.Empty).Encode();
- using var senderOwned = this.libcFunction.NewString(senderRaw);
+ this.HandlePrintMessageDetour(RaptureLogModule.Instance(), chat.Type, sender, message, (int)chat.SenderId, (byte)(chat.Parameters != 0 ? 1 : 0));
- var messageRaw = (chat.Message ?? string.Empty).Encode();
- using var messageOwned = this.libcFunction.NewString(messageRaw);
-
- this.HandlePrintMessageDetour(this.baseAddress, chat.Type, senderOwned.Address, messageOwned.Address, chat.SenderId, chat.Parameters);
+ sender->Dtor(true);
+ message->Dtor(true);
}
}
@@ -242,8 +174,13 @@ public sealed class ChatGui : IDisposable, IServiceType
/// A payload for handling.
internal DalamudLinkPayload AddChatLinkHandler(string pluginName, uint commandId, Action commandAction)
{
- var payload = new DalamudLinkPayload() { Plugin = pluginName, CommandId = commandId };
- this.dalamudLinkHandlers.Add((pluginName, commandId), commandAction);
+ var payload = new DalamudLinkPayload { Plugin = pluginName, CommandId = commandId };
+ lock (this.dalamudLinkHandlers)
+ {
+ this.dalamudLinkHandlers.Add((pluginName, commandId), commandAction);
+ this.dalamudLinkHandlersCopy = null;
+ }
+
return payload;
}
@@ -253,9 +190,14 @@ public sealed class ChatGui : IDisposable, IServiceType
/// The name of the plugin handling the links.
internal void RemoveChatLinkHandler(string pluginName)
{
- foreach (var handler in this.dalamudLinkHandlers.Keys.ToList().Where(k => k.PluginName == pluginName))
+ lock (this.dalamudLinkHandlers)
{
- this.dalamudLinkHandlers.Remove(handler);
+ var changed = false;
+
+ foreach (var handler in this.RegisteredLinkHandlers.Keys.Where(k => k.PluginName == pluginName))
+ changed |= this.dalamudLinkHandlers.Remove(handler);
+ if (changed)
+ this.dalamudLinkHandlersCopy = null;
}
}
@@ -266,18 +208,57 @@ public sealed class ChatGui : IDisposable, IServiceType
/// The ID of the command to be removed.
internal void RemoveChatLinkHandler(string pluginName, uint commandId)
{
- if (this.dalamudLinkHandlers.ContainsKey((pluginName, commandId)))
+ lock (this.dalamudLinkHandlers)
{
- this.dalamudLinkHandlers.Remove((pluginName, commandId));
+ if (this.dalamudLinkHandlers.Remove((pluginName, commandId)))
+ this.dalamudLinkHandlersCopy = null;
}
}
- [ServiceManager.CallWhenServicesReady]
- private void ContinueConstruction(GameGui gameGui, LibcFunction libcFunction)
+ private void PrintTagged(string message, XivChatType channel, string? tag, ushort? color)
{
- this.printMessageHook.Enable();
- this.populateItemLinkHook.Enable();
- this.interactableLinkClickedHook.Enable();
+ var builder = new SeStringBuilder();
+
+ if (!tag.IsNullOrEmpty())
+ {
+ if (color is not null)
+ {
+ builder.AddUiForeground($"[{tag}] ", color.Value);
+ }
+ else
+ {
+ 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)
+ {
+ builder.AddUiForeground($"[{tag}] ", color.Value);
+ }
+ else
+ {
+ builder.AddText($"[{tag}] ");
+ }
+ }
+
+ this.Print(new XivChatEntry
+ {
+ Message = builder.Build().Append(message),
+ Type = channel,
+ });
}
private void HandlePopulateItemLinkDetour(IntPtr linkObjectPtr, IntPtr itemInfoPtr)
@@ -298,40 +279,28 @@ public sealed class ChatGui : IDisposable, IServiceType
}
}
- private IntPtr HandlePrintMessageDetour(IntPtr manager, XivChatType chattype, IntPtr pSenderName, IntPtr pMessage, uint senderid, IntPtr parameter)
+ private uint HandlePrintMessageDetour(RaptureLogModule* manager, XivChatType chatType, Utf8String* sender, Utf8String* message, int timestamp, byte silent)
{
- var retVal = IntPtr.Zero;
+ var messageId = 0u;
try
{
- var sender = StdString.ReadFromPointer(pSenderName);
- var parsedSender = SeString.Parse(sender.RawData);
- var originalSenderData = (byte[])sender.RawData.Clone();
- var oldEditedSender = parsedSender.Encode();
- var senderPtr = pSenderName;
- OwnedStdString allocatedString = null;
+ var originalSenderData = sender->AsSpan().ToArray();
+ var originalMessageData = message->AsSpan().ToArray();
- var message = StdString.ReadFromPointer(pMessage);
- var parsedMessage = SeString.Parse(message.RawData);
- var originalMessageData = (byte[])message.RawData.Clone();
- var oldEdited = parsedMessage.Encode();
- var messagePtr = pMessage;
- OwnedStdString allocatedStringSender = null;
-
- // Log.Verbose("[CHATGUI][{0}][{1}]", parsedSender.TextValue, parsedMessage.TextValue);
-
- // Log.Debug($"HandlePrintMessageDetour {manager} - [{chattype}] [{BitConverter.ToString(message.RawData).Replace("-", " ")}] {message.Value} from {senderName.Value}");
+ var parsedSender = SeString.Parse(originalSenderData);
+ var parsedMessage = SeString.Parse(originalMessageData);
// Call events
var isHandled = false;
- var invocationList = this.CheckMessageHandled.GetInvocationList();
+ var invocationList = this.CheckMessageHandled!.GetInvocationList();
foreach (var @delegate in invocationList)
{
try
{
- var messageHandledDelegate = @delegate as OnCheckMessageHandledDelegate;
- messageHandledDelegate!.Invoke(chattype, senderid, ref parsedSender, ref parsedMessage, ref isHandled);
+ var messageHandledDelegate = @delegate as IChatGui.OnCheckMessageHandledDelegate;
+ messageHandledDelegate!.Invoke(chatType, (uint)timestamp, ref parsedSender, ref parsedMessage, ref isHandled);
}
catch (Exception e)
{
@@ -341,13 +310,13 @@ public sealed class ChatGui : IDisposable, IServiceType
if (!isHandled)
{
- invocationList = this.ChatMessage.GetInvocationList();
+ invocationList = this.ChatMessage!.GetInvocationList();
foreach (var @delegate in invocationList)
{
try
{
- var messageHandledDelegate = @delegate as OnMessageDelegate;
- messageHandledDelegate!.Invoke(chattype, senderid, ref parsedSender, ref parsedMessage, ref isHandled);
+ var messageHandledDelegate = @delegate as IChatGui.OnMessageDelegate;
+ messageHandledDelegate!.Invoke(chatType, (uint)timestamp, ref parsedSender, ref parsedMessage, ref isHandled);
}
catch (Exception e)
{
@@ -356,61 +325,39 @@ public sealed class ChatGui : IDisposable, IServiceType
}
}
- var newEdited = parsedMessage.Encode();
- if (!Util.FastByteArrayCompare(oldEdited, newEdited))
+ var possiblyModifiedSenderData = parsedSender.Encode();
+ var possiblyModifiedMessageData = parsedMessage.Encode();
+
+ if (!Util.FastByteArrayCompare(originalSenderData, possiblyModifiedSenderData))
{
- Log.Verbose("SeString was edited, taking precedence over StdString edit.");
- message.RawData = newEdited;
- // Log.Debug($"\nOLD: {BitConverter.ToString(originalMessageData)}\nNEW: {BitConverter.ToString(newEdited)}");
+ Log.Verbose($"HandlePrintMessageDetour Sender modified: {SeString.Parse(originalSenderData)} -> {parsedSender}");
+ sender->SetString(possiblyModifiedSenderData);
}
- if (!Util.FastByteArrayCompare(originalMessageData, message.RawData))
+ if (!Util.FastByteArrayCompare(originalMessageData, possiblyModifiedMessageData))
{
- allocatedString = this.libcFunction.NewString(message.RawData);
- Log.Debug($"HandlePrintMessageDetour String modified: {originalMessageData}({messagePtr}) -> {message}({allocatedString.Address})");
- messagePtr = allocatedString.Address;
- }
-
- var newEditedSender = parsedSender.Encode();
- if (!Util.FastByteArrayCompare(oldEditedSender, newEditedSender))
- {
- Log.Verbose("SeString was edited, taking precedence over StdString edit.");
- sender.RawData = newEditedSender;
- // Log.Debug($"\nOLD: {BitConverter.ToString(originalMessageData)}\nNEW: {BitConverter.ToString(newEdited)}");
- }
-
- if (!Util.FastByteArrayCompare(originalSenderData, sender.RawData))
- {
- allocatedStringSender = this.libcFunction.NewString(sender.RawData);
- Log.Debug(
- $"HandlePrintMessageDetour Sender modified: {originalSenderData}({senderPtr}) -> {sender}({allocatedStringSender.Address})");
- senderPtr = allocatedStringSender.Address;
+ Log.Verbose($"HandlePrintMessageDetour Message modified: {SeString.Parse(originalMessageData)} -> {parsedMessage}");
+ message->SetString(possiblyModifiedMessageData);
}
// Print the original chat if it's handled.
if (isHandled)
{
- this.ChatMessageHandled?.Invoke(chattype, senderid, parsedSender, parsedMessage);
+ this.ChatMessageHandled?.Invoke(chatType, (uint)timestamp, parsedSender, parsedMessage);
}
else
{
- retVal = this.printMessageHook.Original(manager, chattype, senderPtr, messagePtr, senderid, parameter);
- this.ChatMessageUnhandled?.Invoke(chattype, senderid, parsedSender, parsedMessage);
+ messageId = this.printMessageHook.Original(manager, chatType, sender, message, timestamp, silent);
+ this.ChatMessageUnhandled?.Invoke(chatType, (uint)timestamp, parsedSender, parsedMessage);
}
-
- if (this.baseAddress == IntPtr.Zero)
- this.baseAddress = manager;
-
- allocatedString?.Dispose();
- allocatedStringSender?.Dispose();
}
catch (Exception ex)
{
Log.Error(ex, "Exception on OnChatMessage hook.");
- retVal = this.printMessageHook.Original(manager, chattype, pSenderName, pMessage, senderid, parameter);
+ messageId = this.printMessageHook.Original(manager, chatType, sender, message, timestamp, silent);
}
- return retVal;
+ return messageId;
}
private void InteractableLinkClickedDetour(IntPtr managerPtr, IntPtr messagePtr)
@@ -428,21 +375,17 @@ public sealed class ChatGui : IDisposable, IServiceType
Log.Verbose($"InteractableLinkClicked: {Payload.EmbeddedInfoType.DalamudLink}");
var payloadPtr = Marshal.ReadIntPtr(messagePtr, 0x10);
- var messageSize = 0;
- while (Marshal.ReadByte(payloadPtr, messageSize) != 0) messageSize++;
- var payloadBytes = new byte[messageSize];
- Marshal.Copy(payloadPtr, payloadBytes, 0, messageSize);
- var seStr = SeString.Parse(payloadBytes);
+ var seStr = MemoryHelper.ReadSeStringNullTerminated(payloadPtr);
var terminatorIndex = seStr.Payloads.IndexOf(RawPayload.LinkTerminator);
var payloads = terminatorIndex >= 0 ? seStr.Payloads.Take(terminatorIndex + 1).ToList() : seStr.Payloads;
if (payloads.Count == 0) return;
var linkPayload = payloads[0];
if (linkPayload is DalamudLinkPayload link)
{
- if (this.dalamudLinkHandlers.ContainsKey((link.Plugin, link.CommandId)))
+ if (this.RegisteredLinkHandlers.TryGetValue((link.Plugin, link.CommandId), out var value))
{
Log.Verbose($"Sending DalamudLink to {link.Plugin}: {link.CommandId}");
- this.dalamudLinkHandlers[(link.Plugin, link.CommandId)].Invoke(link.CommandId, new SeString(payloads));
+ value.Invoke(link.CommandId, new SeString(payloads));
}
else
{
@@ -456,3 +399,96 @@ public sealed class ChatGui : IDisposable, IServiceType
}
}
}
+
+///
+/// Plugin scoped version of ChatGui.
+///
+[PluginInterface]
+[InterfaceVersion("1.0")]
+[ServiceManager.ScopedService]
+#pragma warning disable SA1015
+[ResolveVia]
+#pragma warning restore SA1015
+internal class ChatGuiPluginScoped : IInternalDisposableService, IChatGui
+{
+ [ServiceManager.ServiceDependency]
+ private readonly ChatGui chatGuiService = Service.Get();
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ internal ChatGuiPluginScoped()
+ {
+ this.chatGuiService.ChatMessage += this.OnMessageForward;
+ this.chatGuiService.CheckMessageHandled += this.OnCheckMessageForward;
+ this.chatGuiService.ChatMessageHandled += this.OnMessageHandledForward;
+ this.chatGuiService.ChatMessageUnhandled += this.OnMessageUnhandledForward;
+ }
+
+ ///
+ public event IChatGui.OnMessageDelegate? ChatMessage;
+
+ ///
+ public event IChatGui.OnCheckMessageHandledDelegate? CheckMessageHandled;
+
+ ///
+ public event IChatGui.OnMessageHandledDelegate? ChatMessageHandled;
+
+ ///
+ public event IChatGui.OnMessageUnhandledDelegate? ChatMessageUnhandled;
+
+ ///
+ public int LastLinkedItemId => this.chatGuiService.LastLinkedItemId;
+
+ ///
+ public byte LastLinkedItemFlags => this.chatGuiService.LastLinkedItemFlags;
+
+ ///
+ public IReadOnlyDictionary<(string PluginName, uint CommandId), Action> RegisteredLinkHandlers => this.chatGuiService.RegisteredLinkHandlers;
+
+ ///
+ void IInternalDisposableService.DisposeService()
+ {
+ this.chatGuiService.ChatMessage -= this.OnMessageForward;
+ this.chatGuiService.CheckMessageHandled -= this.OnCheckMessageForward;
+ this.chatGuiService.ChatMessageHandled -= this.OnMessageHandledForward;
+ this.chatGuiService.ChatMessageUnhandled -= this.OnMessageUnhandledForward;
+
+ this.ChatMessage = null;
+ this.CheckMessageHandled = null;
+ this.ChatMessageHandled = null;
+ this.ChatMessageUnhandled = null;
+ }
+
+ ///
+ public void Print(XivChatEntry chat)
+ => this.chatGuiService.Print(chat);
+
+ ///
+ public void Print(string message, string? messageTag = null, ushort? tagColor = null)
+ => this.chatGuiService.Print(message, messageTag, tagColor);
+
+ ///
+ public void Print(SeString message, string? messageTag = null, ushort? tagColor = null)
+ => this.chatGuiService.Print(message, messageTag, tagColor);
+
+ ///
+ public void PrintError(string message, string? messageTag = null, ushort? tagColor = null)
+ => this.chatGuiService.PrintError(message, messageTag, tagColor);
+
+ ///
+ public void PrintError(SeString message, string? messageTag = null, ushort? tagColor = null)
+ => this.chatGuiService.PrintError(message, messageTag, tagColor);
+
+ private void OnMessageForward(XivChatType type, uint senderId, ref SeString sender, ref SeString message, ref bool isHandled)
+ => this.ChatMessage?.Invoke(type, senderId, ref sender, ref message, ref isHandled);
+
+ private void OnCheckMessageForward(XivChatType type, uint senderId, ref SeString sender, ref SeString message, ref bool isHandled)
+ => this.CheckMessageHandled?.Invoke(type, senderId, ref sender, ref message, ref isHandled);
+
+ private void OnMessageHandledForward(XivChatType type, uint senderId, SeString sender, SeString message)
+ => this.ChatMessageHandled?.Invoke(type, senderId, sender, message);
+
+ private void OnMessageUnhandledForward(XivChatType type, uint senderId, SeString sender, SeString message)
+ => this.ChatMessageUnhandled?.Invoke(type, senderId, sender, message);
+}
diff --git a/Dalamud/Game/Gui/ChatGuiAddressResolver.cs b/Dalamud/Game/Gui/ChatGuiAddressResolver.cs
index 4686d5725..ae53f90e9 100644
--- a/Dalamud/Game/Gui/ChatGuiAddressResolver.cs
+++ b/Dalamud/Game/Gui/ChatGuiAddressResolver.cs
@@ -1,17 +1,10 @@
-using System;
-
namespace Dalamud.Game.Gui;
///
/// The address resolver for the class.
///
-public sealed class ChatGuiAddressResolver : BaseAddressResolver
+internal sealed class ChatGuiAddressResolver : BaseAddressResolver
{
- ///
- /// Gets the address of the native PrintMessage method.
- ///
- public IntPtr PrintMessage { get; private set; }
-
///
/// Gets the address of the native PopulateItemLinkObject method.
///
@@ -22,77 +15,9 @@ public sealed class ChatGuiAddressResolver : BaseAddressResolver
///
public IntPtr InteractableLinkClicked { get; private set; }
- /*
- --- for reference: 4.57 ---
- .text:00000001405CD210 ; __int64 __fastcall Xiv::Gui::ChatGui::PrintMessage(__int64 handler, unsigned __int16 chatType, __int64 senderName, __int64 message, int senderActorId, char isLocal)
- .text:00000001405CD210 Xiv__Gui__ChatGui__PrintMessage proc near
- .text:00000001405CD210 ; CODE XREF: sub_1401419F0+201↑p
- .text:00000001405CD210 ; sub_140141D10+220↑p ...
- .text:00000001405CD210
- .text:00000001405CD210 var_220 = qword ptr -220h
- .text:00000001405CD210 var_218 = byte ptr -218h
- .text:00000001405CD210 var_210 = word ptr -210h
- .text:00000001405CD210 var_208 = byte ptr -208h
- .text:00000001405CD210 var_200 = word ptr -200h
- .text:00000001405CD210 var_1FC = dword ptr -1FCh
- .text:00000001405CD210 var_1F8 = qword ptr -1F8h
- .text:00000001405CD210 var_1F0 = qword ptr -1F0h
- .text:00000001405CD210 var_1E8 = qword ptr -1E8h
- .text:00000001405CD210 var_1E0 = dword ptr -1E0h
- .text:00000001405CD210 var_1DC = word ptr -1DCh
- .text:00000001405CD210 var_1DA = word ptr -1DAh
- .text:00000001405CD210 var_1D8 = qword ptr -1D8h
- .text:00000001405CD210 var_1D0 = byte ptr -1D0h
- .text:00000001405CD210 var_1C8 = qword ptr -1C8h
- .text:00000001405CD210 var_1B0 = dword ptr -1B0h
- .text:00000001405CD210 var_1AC = dword ptr -1ACh
- .text:00000001405CD210 var_1A8 = dword ptr -1A8h
- .text:00000001405CD210 var_1A4 = dword ptr -1A4h
- .text:00000001405CD210 var_1A0 = dword ptr -1A0h
- .text:00000001405CD210 var_160 = dword ptr -160h
- .text:00000001405CD210 var_15C = dword ptr -15Ch
- .text:00000001405CD210 var_140 = dword ptr -140h
- .text:00000001405CD210 var_138 = dword ptr -138h
- .text:00000001405CD210 var_130 = byte ptr -130h
- .text:00000001405CD210 var_C0 = byte ptr -0C0h
- .text:00000001405CD210 var_50 = qword ptr -50h
- .text:00000001405CD210 var_38 = qword ptr -38h
- .text:00000001405CD210 var_30 = qword ptr -30h
- .text:00000001405CD210 var_28 = qword ptr -28h
- .text:00000001405CD210 var_20 = qword ptr -20h
- .text:00000001405CD210 senderActorId = dword ptr 30h
- .text:00000001405CD210 isLocal = byte ptr 38h
- .text:00000001405CD210
- .text:00000001405CD210 ; __unwind { // __GSHandlerCheck
- .text:00000001405CD210 push rbp
- .text:00000001405CD212 push rdi
- .text:00000001405CD213 push r14
- .text:00000001405CD215 push r15
- .text:00000001405CD217 lea rbp, [rsp-128h]
- .text:00000001405CD21F sub rsp, 228h
- .text:00000001405CD226 mov rax, cs:__security_cookie
- .text:00000001405CD22D xor rax, rsp
- .text:00000001405CD230 mov [rbp+140h+var_50], rax
- .text:00000001405CD237 xor r10b, r10b
- .text:00000001405CD23A mov [rsp+240h+var_1F8], rcx
- .text:00000001405CD23F xor eax, eax
- .text:00000001405CD241 mov r11, r9
- .text:00000001405CD244 mov r14, r8
- .text:00000001405CD247 mov r9d, eax
- .text:00000001405CD24A movzx r15d, dx
- .text:00000001405CD24E lea r8, [rcx+0C10h]
- .text:00000001405CD255 mov rdi, rcx
- */
-
///
- protected override void Setup64Bit(SigScanner sig)
+ protected override void Setup64Bit(ISigScanner sig)
{
- // PrintMessage = sig.ScanText("4055 57 41 ?? 41 ?? 488DAC24D8FEFFFF 4881EC28020000 488B05???????? 4833C4 488985F0000000 4532D2 48894C2448"); LAST PART FOR 5.1???
- this.PrintMessage = sig.ScanText("40 55 53 56 41 54 41 57 48 8D AC 24 ?? ?? ?? ?? 48 81 EC 20 02 00 00 48 8B 05");
- // PrintMessage = sig.ScanText("4055 57 41 ?? 41 ?? 488DAC24E8FEFFFF 4881EC18020000 488B05???????? 4833C4 488985E0000000 4532D2 48894C2438"); old
-
- // PrintMessage = sig.ScanText("40 55 57 41 56 41 57 48 8D AC 24 D8 FE FF FF 48 81 EC 28 02 00 00 48 8B 05 63 47 4A 01 48 33 C4 48 89 85 F0 00 00 00 45 32 D2 48 89 4C 24 48 33");
-
// PopulateItemLinkObject = sig.ScanText("48 89 5C 24 08 57 48 83 EC 20 80 7A 06 00 48 8B DA 48 8B F9 74 14 48 8B CA E8 32 03 00 00 48 8B C8 E8 FA F2 B0 FF 8B C8 EB 1D 0F B6 42 14 8B 4A");
// PopulateItemLinkObject = sig.ScanText( "48 89 5C 24 08 57 48 83 EC 20 80 7A 06 00 48 8B DA 48 8B F9 74 14 48 8B CA E8 32 03 00 00 48 8B C8 E8 ?? ?? B0 FF 8B C8 EB 1D 0F B6 42 14 8B 4A"); 5.0
diff --git a/Dalamud/Game/Gui/ContextMenu/ContextMenu.cs b/Dalamud/Game/Gui/ContextMenu/ContextMenu.cs
new file mode 100644
index 000000000..f136d017a
--- /dev/null
+++ b/Dalamud/Game/Gui/ContextMenu/ContextMenu.cs
@@ -0,0 +1,560 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+using Dalamud.Game.Text;
+using Dalamud.Game.Text.SeStringHandling;
+using Dalamud.Hooking;
+using Dalamud.IoC;
+using Dalamud.IoC.Internal;
+using Dalamud.Logging.Internal;
+using Dalamud.Memory;
+using Dalamud.Plugin.Services;
+using Dalamud.Utility;
+
+using FFXIVClientStructs.FFXIV.Client.System.Memory;
+using FFXIVClientStructs.FFXIV.Client.UI;
+using FFXIVClientStructs.FFXIV.Client.UI.Agent;
+using FFXIVClientStructs.FFXIV.Component.GUI;
+using FFXIVClientStructs.Interop;
+
+using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType;
+
+namespace Dalamud.Game.Gui.ContextMenu;
+
+///
+/// This class handles interacting with the game's (right-click) context menu.
+///
+[InterfaceVersion("1.0")]
+[ServiceManager.EarlyLoadedService]
+internal sealed unsafe class ContextMenu : IInternalDisposableService, IContextMenu
+{
+ private static readonly ModuleLog Log = new("ContextMenu");
+
+ private readonly Hook raptureAtkModuleOpenAddonByAgentHook;
+ private readonly Hook addonContextMenuOnMenuSelectedHook;
+ private readonly RaptureAtkModuleOpenAddonDelegate raptureAtkModuleOpenAddon;
+
+ [ServiceManager.ServiceConstructor]
+ private ContextMenu()
+ {
+ this.raptureAtkModuleOpenAddonByAgentHook = Hook.FromAddress((nint)RaptureAtkModule.Addresses.OpenAddonByAgent.Value, this.RaptureAtkModuleOpenAddonByAgentDetour);
+ this.addonContextMenuOnMenuSelectedHook = Hook.FromAddress((nint)AddonContextMenu.StaticVTable.OnMenuSelected, this.AddonContextMenuOnMenuSelectedDetour);
+ this.raptureAtkModuleOpenAddon = Marshal.GetDelegateForFunctionPointer((nint)RaptureAtkModule.Addresses.OpenAddon.Value);
+
+ this.raptureAtkModuleOpenAddonByAgentHook.Enable();
+ this.addonContextMenuOnMenuSelectedHook.Enable();
+ }
+
+ private unsafe delegate ushort RaptureAtkModuleOpenAddonByAgentDelegate(RaptureAtkModule* module, byte* addonName, AtkUnitBase* addon, int valueCount, AtkValue* values, AgentInterface* agent, nint a7, ushort parentAddonId);
+
+ private unsafe delegate bool AddonContextMenuOnMenuSelectedDelegate(AddonContextMenu* addon, int selectedIdx, byte a3);
+
+ private unsafe delegate ushort RaptureAtkModuleOpenAddonDelegate(RaptureAtkModule* a1, uint addonNameId, uint valueCount, AtkValue* values, AgentInterface* parentAgent, ulong unk, ushort parentAddonId, int unk2);
+
+ ///
+ public event IContextMenu.OnMenuOpenedDelegate OnMenuOpened;
+
+ private Dictionary> MenuItems { get; } = new();
+
+ private object MenuItemsLock { get; } = new();
+
+ private AgentInterface* SelectedAgent { get; set; }
+
+ private ContextMenuType? SelectedMenuType { get; set; }
+
+ private List