From 4e87b4b0076460195f3be5e2fe76630e41ebec2e Mon Sep 17 00:00:00 2001 From: goaaats Date: Wed, 12 Nov 2025 20:15:12 +0100 Subject: [PATCH 01/27] Retarget to .NET 10 --- Directory.Build.props | 2 +- global.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 4ed87c809..5f6da3d94 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -2,7 +2,7 @@ - net9.0-windows + net10.0-windows x64 x64 13.0 diff --git a/global.json b/global.json index ab1a4a2ec..93dd0dd1f 100644 --- a/global.json +++ b/global.json @@ -1,7 +1,7 @@ { "sdk": { - "version": "9.0.0", + "version": "10.0.0", "rollForward": "latestMinor", "allowPrerelease": true } -} +} \ No newline at end of file From 7d76d275559bd82ef0b53a9709a8d1530f007630 Mon Sep 17 00:00:00 2001 From: goaaats Date: Wed, 12 Nov 2025 20:31:28 +0100 Subject: [PATCH 02/27] Upgrade packages --- Directory.Packages.props | 120 ++++++++++++++++++--------------------- build/build.csproj | 2 +- 2 files changed, 57 insertions(+), 65 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index a1cef517e..91875e63e 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,65 +1,57 @@ - - true - false - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + true + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/build/build.csproj b/build/build.csproj index b4aaa959d..32907677f 100644 --- a/build/build.csproj +++ b/build/build.csproj @@ -12,6 +12,6 @@ - + From e0eff2fe74a91a4d234c3b916da7d61760cb9c9f Mon Sep 17 00:00:00 2001 From: goaaats Date: Wed, 12 Nov 2025 21:02:07 +0100 Subject: [PATCH 03/27] Use standard apphost for Dalamud.Injector --- .../Dalamud.Injector.Boot.vcxproj | 111 ------------------ .../Dalamud.Injector.Boot.vcxproj.filters | 67 ----------- Dalamud.Injector.Boot/main.cpp | 48 -------- Dalamud.Injector.Boot/pch.h | 1 - Dalamud.Injector.Boot/resources.rc | 1 - Dalamud.Injector/Dalamud.Injector.csproj | 3 +- .../{EntryPoint.cs => Program.cs} | 26 +--- .../dalamud.ico | Bin Dalamud.sln | 12 +- 9 files changed, 9 insertions(+), 260 deletions(-) delete mode 100644 Dalamud.Injector.Boot/Dalamud.Injector.Boot.vcxproj delete mode 100644 Dalamud.Injector.Boot/Dalamud.Injector.Boot.vcxproj.filters delete mode 100644 Dalamud.Injector.Boot/main.cpp delete mode 100644 Dalamud.Injector.Boot/pch.h delete mode 100644 Dalamud.Injector.Boot/resources.rc rename Dalamud.Injector/{EntryPoint.cs => Program.cs} (98%) rename {Dalamud.Injector.Boot => Dalamud.Injector}/dalamud.ico (100%) diff --git a/Dalamud.Injector.Boot/Dalamud.Injector.Boot.vcxproj b/Dalamud.Injector.Boot/Dalamud.Injector.Boot.vcxproj deleted file mode 100644 index 7f8de3843..000000000 --- a/Dalamud.Injector.Boot/Dalamud.Injector.Boot.vcxproj +++ /dev/null @@ -1,111 +0,0 @@ - - - - {8874326B-E755-4D13-90B4-59AB263A3E6B} - Dalamud_Injector_Boot - Debug - x64 - - - - Debug - x64 - - - Release - x64 - - - - 16.0 - Win32Proj - 10.0 - Dalamud.Injector - - - - Application - true - v143 - false - Unicode - ..\bin\$(Configuration)\ - obj\$(Configuration)\ - - - - - Level3 - true - true - stdcpp23 - pch.h - ProgramDatabase - CPPDLLTEMPLATE_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) - - - Console - true - false - ..\lib\CoreCLR;%(AdditionalLibraryDirectories) - $(OutDir)$(TargetName).Boot.pdb - - - - - true - false - MultiThreadedDebugDLL - _DEBUG;%(PreprocessorDefinitions) - - - false - false - - - - - true - true - MultiThreadedDLL - NDEBUG;%(PreprocessorDefinitions) - - - true - true - - - - - - nethost.dll - PreserveNewest - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Dalamud.Injector.Boot/Dalamud.Injector.Boot.vcxproj.filters b/Dalamud.Injector.Boot/Dalamud.Injector.Boot.vcxproj.filters deleted file mode 100644 index 8f4372d89..000000000 --- a/Dalamud.Injector.Boot/Dalamud.Injector.Boot.vcxproj.filters +++ /dev/null @@ -1,67 +0,0 @@ - - - - - {4FC737F1-C7A5-4376-A066-2A32D752A2FF} - cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx - - - {93995380-89BD-4b04-88EB-625FBE52EBFB} - h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd - - - {4faac519-3a73-4b2b-96e7-fb597f02c0be} - ico;rc - - - - - Resource Files - - - - - Resource Files - - - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - \ No newline at end of file diff --git a/Dalamud.Injector.Boot/main.cpp b/Dalamud.Injector.Boot/main.cpp deleted file mode 100644 index df4120009..000000000 --- a/Dalamud.Injector.Boot/main.cpp +++ /dev/null @@ -1,48 +0,0 @@ -#define WIN32_LEAN_AND_MEAN - -#include -#include -#include -#include "..\Dalamud.Boot\logging.h" -#include "..\lib\CoreCLR\CoreCLR.h" -#include "..\lib\CoreCLR\boot.h" - -int wmain(int argc, wchar_t** argv) -{ - // Take care: don't redirect stderr/out here, we need to write our pid to stdout for XL to read - //logging::start_file_logging("dalamud.injector.boot.log", false); - logging::I("Dalamud Injector, (c) 2021 XIVLauncher Contributors"); - logging::I("Built at : " __DATE__ "@" __TIME__); - - wchar_t _module_path[MAX_PATH]; - GetModuleFileNameW(NULL, _module_path, sizeof _module_path / 2); - std::filesystem::path fs_module_path(_module_path); - - std::wstring runtimeconfig_path = _wcsdup(fs_module_path.replace_filename(L"Dalamud.Injector.runtimeconfig.json").c_str()); - std::wstring module_path = _wcsdup(fs_module_path.replace_filename(L"Dalamud.Injector.dll").c_str()); - - // =========================================================================== // - - void* entrypoint_vfn; - const auto result = InitializeClrAndGetEntryPoint( - GetModuleHandleW(nullptr), - false, - runtimeconfig_path, - module_path, - L"Dalamud.Injector.EntryPoint, Dalamud.Injector", - L"Main", - L"Dalamud.Injector.EntryPoint+MainDelegate, Dalamud.Injector", - &entrypoint_vfn); - - if (FAILED(result)) - return result; - - 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..."); - const auto ret = entrypoint_fn(argc, argv); - logging::I("Done!"); - - return ret; -} diff --git a/Dalamud.Injector.Boot/pch.h b/Dalamud.Injector.Boot/pch.h deleted file mode 100644 index 6f70f09be..000000000 --- a/Dalamud.Injector.Boot/pch.h +++ /dev/null @@ -1 +0,0 @@ -#pragma once diff --git a/Dalamud.Injector.Boot/resources.rc b/Dalamud.Injector.Boot/resources.rc deleted file mode 100644 index 8369e82a1..000000000 --- a/Dalamud.Injector.Boot/resources.rc +++ /dev/null @@ -1 +0,0 @@ -MAINICON ICON "dalamud.ico" diff --git a/Dalamud.Injector/Dalamud.Injector.csproj b/Dalamud.Injector/Dalamud.Injector.csproj index 4a55174a1..a0b4f6451 100644 --- a/Dalamud.Injector/Dalamud.Injector.csproj +++ b/Dalamud.Injector/Dalamud.Injector.csproj @@ -13,12 +13,13 @@ - Library + Exe ..\bin\$(Configuration)\ false false true false + dalamud.ico diff --git a/Dalamud.Injector/EntryPoint.cs b/Dalamud.Injector/Program.cs similarity index 98% rename from Dalamud.Injector/EntryPoint.cs rename to Dalamud.Injector/Program.cs index b876aa6ed..e224791e6 100644 --- a/Dalamud.Injector/EntryPoint.cs +++ b/Dalamud.Injector/Program.cs @@ -25,34 +25,20 @@ namespace Dalamud.Injector /// /// Entrypoint to the program. /// - public sealed class EntryPoint + public sealed class Program { - /// - /// A delegate used during initialization of the CLR from Dalamud.Injector.Boot. - /// - /// Count of arguments. - /// char** string arguments. - /// Return value (HRESULT). - public delegate int MainDelegate(int argc, IntPtr argvPtr); - /// /// Start the Dalamud injector. /// - /// Count of arguments. - /// byte** string arguments. + /// Command line arguments. /// Return value (HRESULT). - public static int Main(int argc, IntPtr argvPtr) + public static int Main(string[] argsArray) { try { - List args = new(argc); - - unsafe - { - var argv = (IntPtr*)argvPtr; - for (var i = 0; i < argc; i++) - args.Add(Marshal.PtrToStringUni(argv[i])); - } + // API14 TODO: Refactor + var args = argsArray.ToList(); + args.Insert(0, Assembly.GetExecutingAssembly().Location); Init(args); args.Remove("-v"); // Remove "verbose" flag diff --git a/Dalamud.Injector.Boot/dalamud.ico b/Dalamud.Injector/dalamud.ico similarity index 100% rename from Dalamud.Injector.Boot/dalamud.ico rename to Dalamud.Injector/dalamud.ico diff --git a/Dalamud.sln b/Dalamud.sln index c3af00f44..ee3c75b25 100644 --- a/Dalamud.sln +++ b/Dalamud.sln @@ -1,4 +1,4 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 +Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.1.32319.34 MinimumVisualStudioVersion = 10.0.40219.1 @@ -27,8 +27,6 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Dalamud.Boot", "Dalamud.Boo EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dalamud.Injector", "Dalamud.Injector\Dalamud.Injector.csproj", "{5B832F73-5F54-4ADC-870F-D0095EF72C9A}" EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Dalamud.Injector.Boot", "Dalamud.Injector.Boot\Dalamud.Injector.Boot.vcxproj", "{8874326B-E755-4D13-90B4-59AB263A3E6B}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dalamud.Test", "Dalamud.Test\Dalamud.Test.csproj", "{C8004563-1806-4329-844F-0EF6274291FC}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Dependencies", "Dependencies", "{E15BDA6D-E881-4482-94BA-BE5527E917FF}" @@ -49,8 +47,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InteropGenerator", "lib\FFX EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InteropGenerator.Runtime", "lib\FFXIVClientStructs\InteropGenerator.Runtime\InteropGenerator.Runtime.csproj", "{A6AA1C3F-9470-4922-9D3F-D4549657AB22}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Injector", "Injector", "{19775C83-7117-4A5F-AA00-18889F46A490}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Utilities", "Utilities", "{8F079208-C227-4D96-9427-2BEBE0003944}" EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "cimgui", "external\cimgui\cimgui.vcxproj", "{8430077C-F736-4246-A052-8EA1CECE844E}" @@ -103,10 +99,6 @@ Global {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Debug|Any CPU.Build.0 = Debug|x64 {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|Any CPU.ActiveCfg = Release|x64 {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|Any CPU.Build.0 = Release|x64 - {8874326B-E755-4D13-90B4-59AB263A3E6B}.Debug|Any CPU.ActiveCfg = Debug|x64 - {8874326B-E755-4D13-90B4-59AB263A3E6B}.Debug|Any CPU.Build.0 = Debug|x64 - {8874326B-E755-4D13-90B4-59AB263A3E6B}.Release|Any CPU.ActiveCfg = Release|x64 - {8874326B-E755-4D13-90B4-59AB263A3E6B}.Release|Any CPU.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}.Release|Any CPU.ActiveCfg = Release|x64 @@ -188,8 +180,6 @@ Global HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {5B832F73-5F54-4ADC-870F-D0095EF72C9A} = {19775C83-7117-4A5F-AA00-18889F46A490} - {8874326B-E755-4D13-90B4-59AB263A3E6B} = {19775C83-7117-4A5F-AA00-18889F46A490} {4AFDB34A-7467-4D41-B067-53BC4101D9D0} = {8F079208-C227-4D96-9427-2BEBE0003944} {C9B87BD7-AF49-41C3-91F1-D550ADEB7833} = {8BBACF2D-7AB8-4610-A115-0E363D35C291} {E0D51896-604F-4B40-8CFE-51941607B3A1} = {8BBACF2D-7AB8-4610-A115-0E363D35C291} From a37a13e0ba0ab5a661f71add85c0b9740dabdf1c Mon Sep 17 00:00:00 2001 From: goaaats Date: Wed, 12 Nov 2025 21:03:14 +0100 Subject: [PATCH 04/27] Use .NET 10 in CI --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index be44afacc..299d71e95 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -23,7 +23,7 @@ jobs: uses: microsoft/setup-msbuild@v1.0.2 - uses: actions/setup-dotnet@v3 with: - dotnet-version: '9.0.200' + dotnet-version: '10.0.100' - name: Define VERSION run: | $env:COMMIT = $env:GITHUB_SHA.Substring(0, 7) From 7bc921f54328924e2b29ef481da08d481a60e4b7 Mon Sep 17 00:00:00 2001 From: goaaats Date: Wed, 12 Nov 2025 21:09:21 +0100 Subject: [PATCH 05/27] No analyzers on nuke build --- build/build.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/build/build.csproj b/build/build.csproj index 32907677f..1e1416d92 100644 --- a/build/build.csproj +++ b/build/build.csproj @@ -13,5 +13,6 @@ + From 928fbba4893ae2022a5b8b637c3fa875bc4afec4 Mon Sep 17 00:00:00 2001 From: goaaats Date: Wed, 12 Nov 2025 21:13:50 +0100 Subject: [PATCH 06/27] Remove Injector.Boot targets --- build/DalamudBuild.cs | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/build/DalamudBuild.cs b/build/DalamudBuild.cs index d374c79f8..ba2b09a4d 100644 --- a/build/DalamudBuild.cs +++ b/build/DalamudBuild.cs @@ -42,10 +42,7 @@ public class DalamudBuild : NukeBuild AbsolutePath InjectorProjectDir => RootDirectory / "Dalamud.Injector"; AbsolutePath InjectorProjectFile => InjectorProjectDir / "Dalamud.Injector.csproj"; - - AbsolutePath InjectorBootProjectDir => RootDirectory / "Dalamud.Injector.Boot"; - AbsolutePath InjectorBootProjectFile => InjectorBootProjectDir / "Dalamud.Injector.Boot.vcxproj"; - + AbsolutePath TestProjectDir => RootDirectory / "Dalamud.Test"; AbsolutePath TestProjectFile => TestProjectDir / "Dalamud.Test.csproj"; @@ -172,14 +169,6 @@ public class DalamudBuild : NukeBuild .EnableNoRestore()); }); - Target CompileInjectorBoot => _ => _ - .Executes(() => - { - MSBuildTasks.MSBuild(s => s - .SetTargetPath(InjectorBootProjectFile) - .SetConfiguration(Configuration)); - }); - Target SetCILogging => _ => _ .DependentFor(Compile) .OnlyWhenStatic(() => IsCIBuild) @@ -196,7 +185,6 @@ public class DalamudBuild : NukeBuild .DependsOn(CompileDalamudBoot) .DependsOn(CompileDalamudCrashHandler) .DependsOn(CompileInjector) - .DependsOn(CompileInjectorBoot) ; Target CI => _ => _ @@ -250,11 +238,6 @@ public class DalamudBuild : NukeBuild .SetProject(InjectorProjectFile) .SetConfiguration(Configuration)); - MSBuildTasks.MSBuild(s => s - .SetProjectFile(InjectorBootProjectFile) - .SetConfiguration(Configuration) - .SetTargets("Clean")); - FileSystemTasks.DeleteDirectory(ArtifactsDirectory); Directory.CreateDirectory(ArtifactsDirectory); }); From 6340afb6921bfd8915e22300bcf623c956ea0c1f Mon Sep 17 00:00:00 2001 From: goaaats Date: Wed, 12 Nov 2025 21:39:38 +0100 Subject: [PATCH 07/27] Nuke schema, also remove analyzers from imgui testbed --- .nuke/build.schema.json | 2 -- imgui/StandaloneImGuiTestbed/StandaloneImGuiTestbed.csproj | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json index 8331affcc..03211ce8f 100644 --- a/.nuke/build.schema.json +++ b/.nuke/build.schema.json @@ -87,7 +87,6 @@ "CompileDalamudCrashHandler", "CompileImGuiNatives", "CompileInjector", - "CompileInjectorBoot", "Restore", "SetCILogging", "Test" @@ -115,7 +114,6 @@ "CompileDalamudCrashHandler", "CompileImGuiNatives", "CompileInjector", - "CompileInjectorBoot", "Restore", "SetCILogging", "Test" diff --git a/imgui/StandaloneImGuiTestbed/StandaloneImGuiTestbed.csproj b/imgui/StandaloneImGuiTestbed/StandaloneImGuiTestbed.csproj index d56faa31e..da31c9a8e 100644 --- a/imgui/StandaloneImGuiTestbed/StandaloneImGuiTestbed.csproj +++ b/imgui/StandaloneImGuiTestbed/StandaloneImGuiTestbed.csproj @@ -26,6 +26,7 @@ + From 2b2f628096f25b00994f5fc5abec1acc2eb6327e Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Thu, 13 Nov 2025 18:43:49 +0100 Subject: [PATCH 08/27] Convert ObjectTable enumerator to struct --- .../Game/ClientState/Objects/ObjectTable.cs | 40 ++----------------- 1 file changed, 3 insertions(+), 37 deletions(-) diff --git a/Dalamud/Game/ClientState/Objects/ObjectTable.cs b/Dalamud/Game/ClientState/Objects/ObjectTable.cs index 84c1b5693..598daf518 100644 --- a/Dalamud/Game/ClientState/Objects/ObjectTable.cs +++ b/Dalamud/Game/ClientState/Objects/ObjectTable.cs @@ -12,8 +12,6 @@ using Dalamud.Utility; using FFXIVClientStructs.Interop; -using Microsoft.Extensions.ObjectPool; - using CSGameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject; using CSGameObjectManager = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObjectManager; @@ -34,8 +32,6 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable private readonly ClientState clientState; private readonly CachedEntry[] cachedObjectTable; - private readonly Enumerator?[] frameworkThreadEnumerators = new Enumerator?[4]; - [ServiceManager.ServiceConstructor] private unsafe ObjectTable(ClientState clientState) { @@ -47,9 +43,6 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable this.cachedObjectTable = new CachedEntry[objectTableLength]; for (var i = 0; i < this.cachedObjectTable.Length; i++) this.cachedObjectTable[i] = new(nativeObjectTable.GetPointer(i)); - - for (var i = 0; i < this.frameworkThreadEnumerators.Length; i++) - this.frameworkThreadEnumerators[i] = new(this, i); } /// @@ -239,30 +232,14 @@ internal sealed partial class ObjectTable public IEnumerator GetEnumerator() { ThreadSafety.AssertMainThread(); - - // If we're on the framework thread, see if there's an already allocated enumerator available for use. - foreach (ref var x in this.frameworkThreadEnumerators.AsSpan()) - { - if (x is not null) - { - var t = x; - x = null; - t.Reset(); - return t; - } - } - - // No reusable enumerator is available; allocate a new temporary one. - return new Enumerator(this, -1); + return new Enumerator(this); } /// IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); - private sealed class Enumerator(ObjectTable owner, int slotId) : IEnumerator, IResettable + private struct Enumerator(ObjectTable owner) : IEnumerator { - private ObjectTable? owner = owner; - private int index = -1; public IGameObject Current { get; private set; } = null!; @@ -274,7 +251,7 @@ internal sealed partial class ObjectTable if (this.index == objectTableLength) return false; - var cache = this.owner!.cachedObjectTable.AsSpan(); + var cache = owner.cachedObjectTable.AsSpan(); for (this.index++; this.index < objectTableLength; this.index++) { if (cache[this.index].Update() is { } ao) @@ -291,17 +268,6 @@ internal sealed partial class ObjectTable public void Dispose() { - if (this.owner is not { } o) - return; - - if (slotId != -1) - o.frameworkThreadEnumerators[slotId] = this; - } - - public bool TryReset() - { - this.Reset(); - return true; } } } From dd70c5b8eea1627566f0b355fc5ce7807007e803 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Thu, 13 Nov 2025 18:36:45 +0100 Subject: [PATCH 09/27] Add struct enumerator to AetheryteList --- .../ClientState/Aetherytes/AetheryteList.cs | 31 ++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/Dalamud/Game/ClientState/Aetherytes/AetheryteList.cs b/Dalamud/Game/ClientState/Aetherytes/AetheryteList.cs index a3d44d423..f72339ed2 100644 --- a/Dalamud/Game/ClientState/Aetherytes/AetheryteList.cs +++ b/Dalamud/Game/ClientState/Aetherytes/AetheryteList.cs @@ -87,10 +87,7 @@ internal sealed partial class AetheryteList /// public IEnumerator GetEnumerator() { - for (var i = 0; i < this.Length; i++) - { - yield return this[i]; - } + return new Enumerator(this); } /// @@ -98,4 +95,30 @@ internal sealed partial class AetheryteList { return this.GetEnumerator(); } + + private struct Enumerator(AetheryteList aetheryteList) : IEnumerator + { + private int index = 0; + + public IAetheryteEntry Current { get; private set; } + + object IEnumerator.Current => this.Current; + + public bool MoveNext() + { + if (this.index == aetheryteList.Length) return false; + this.Current = aetheryteList[this.index]; + this.index++; + return true; + } + + public void Reset() + { + this.index = 0; + } + + public void Dispose() + { + } + } } From 520e3ea028044395925e8d73d29a1a2fb4f5410f Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Thu, 13 Nov 2025 18:37:15 +0100 Subject: [PATCH 10/27] Convert AetheryteEntry to readonly struct --- .../ClientState/Aetherytes/AetheryteEntry.cs | 36 +++++++------------ 1 file changed, 13 insertions(+), 23 deletions(-) diff --git a/Dalamud/Game/ClientState/Aetherytes/AetheryteEntry.cs b/Dalamud/Game/ClientState/Aetherytes/AetheryteEntry.cs index 89dd8b8b1..e0a5df06d 100644 --- a/Dalamud/Game/ClientState/Aetherytes/AetheryteEntry.cs +++ b/Dalamud/Game/ClientState/Aetherytes/AetheryteEntry.cs @@ -63,47 +63,37 @@ public interface IAetheryteEntry } /// -/// Class representing an aetheryte entry available to the game. +/// This struct represents an aetheryte entry available to the game. /// -internal sealed class AetheryteEntry : IAetheryteEntry +/// Data read from the Aetheryte List. +internal readonly struct AetheryteEntry(TeleportInfo data) : IAetheryteEntry { - private readonly TeleportInfo data; - - /// - /// Initializes a new instance of the class. - /// - /// Data read from the Aetheryte List. - internal AetheryteEntry(TeleportInfo data) - { - this.data = data; - } + /// + public uint AetheryteId => data.AetheryteId; /// - public uint AetheryteId => this.data.AetheryteId; + public uint TerritoryId => data.TerritoryId; /// - public uint TerritoryId => this.data.TerritoryId; + public byte SubIndex => data.SubIndex; /// - public byte SubIndex => this.data.SubIndex; + public byte Ward => data.Ward; /// - public byte Ward => this.data.Ward; + public byte Plot => data.Plot; /// - public byte Plot => this.data.Plot; + public uint GilCost => data.GilCost; /// - public uint GilCost => this.data.GilCost; + public bool IsFavourite => data.IsFavourite; /// - public bool IsFavourite => this.data.IsFavourite; + public bool IsSharedHouse => data.IsSharedHouse; /// - public bool IsSharedHouse => this.data.IsSharedHouse; - - /// - public bool IsApartment => this.data.IsApartment; + public bool IsApartment => data.IsApartment; /// public RowRef AetheryteData => LuminaUtils.CreateRef(this.AetheryteId); From 8a9b47c7a472987c33240594fbea645727fb7dc3 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Thu, 13 Nov 2025 18:37:35 +0100 Subject: [PATCH 11/27] Add struct enumerator to BuddyList --- Dalamud/Game/ClientState/Buddy/BuddyList.cs | 31 ++++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/Dalamud/Game/ClientState/Buddy/BuddyList.cs b/Dalamud/Game/ClientState/Buddy/BuddyList.cs index 84cfd24a3..71121e54e 100644 --- a/Dalamud/Game/ClientState/Buddy/BuddyList.cs +++ b/Dalamud/Game/ClientState/Buddy/BuddyList.cs @@ -130,12 +130,35 @@ internal sealed partial class BuddyList /// public IEnumerator GetEnumerator() { - for (var i = 0; i < this.Length; i++) - { - yield return this[i]; - } + return new Enumerator(this); } /// IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + + private struct Enumerator(BuddyList buddyList) : IEnumerator + { + private int index = 0; + + public IBuddyMember Current { get; private set; } + + object IEnumerator.Current => this.Current; + + public bool MoveNext() + { + if (this.index == buddyList.Length) return false; + this.Current = buddyList[this.index]; + this.index++; + return true; + } + + public void Reset() + { + this.index = 0; + } + + public void Dispose() + { + } + } } From 23e7c164d86ca1d59713f7f7f8955f2eed2b29d6 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Thu, 13 Nov 2025 18:42:07 +0100 Subject: [PATCH 12/27] Convert BuddyMember to readonly struct --- Dalamud/Game/ClientState/Buddy/BuddyList.cs | 33 +++++----- Dalamud/Game/ClientState/Buddy/BuddyMember.cs | 60 ++++++++++++------- 2 files changed, 56 insertions(+), 37 deletions(-) diff --git a/Dalamud/Game/ClientState/Buddy/BuddyList.cs b/Dalamud/Game/ClientState/Buddy/BuddyList.cs index 71121e54e..78809f8ba 100644 --- a/Dalamud/Game/ClientState/Buddy/BuddyList.cs +++ b/Dalamud/Game/ClientState/Buddy/BuddyList.cs @@ -8,6 +8,9 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game.UI; +using CSBuddy = FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy; +using CSBuddyMember = FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy.BuddyMember; + namespace Dalamud.Game.ClientState.Buddy; /// @@ -21,7 +24,7 @@ namespace Dalamud.Game.ClientState.Buddy; #pragma warning restore SA1015 internal sealed partial class BuddyList : IServiceType, IBuddyList { - private const uint InvalidObjectID = 0xE0000000; + private const uint InvalidEntityId = 0xE0000000; [ServiceManager.ServiceDependency] private readonly ClientState clientState = Service.Get(); @@ -69,7 +72,7 @@ internal sealed partial class BuddyList : IServiceType, IBuddyList } } - private unsafe FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy* BuddyListStruct => &UIState.Instance()->Buddy; + private unsafe CSBuddy* BuddyListStruct => &UIState.Instance()->Buddy; /// public IBuddyMember? this[int index] @@ -82,37 +85,37 @@ internal sealed partial class BuddyList : IServiceType, IBuddyList } /// - public unsafe IntPtr GetCompanionBuddyMemberAddress() + public unsafe nint GetCompanionBuddyMemberAddress() { - return (IntPtr)this.BuddyListStruct->CompanionInfo.Companion; + return (nint)this.BuddyListStruct->CompanionInfo.Companion; } /// - public unsafe IntPtr GetPetBuddyMemberAddress() + public unsafe nint GetPetBuddyMemberAddress() { - return (IntPtr)this.BuddyListStruct->PetInfo.Pet; + return (nint)this.BuddyListStruct->PetInfo.Pet; } /// - public unsafe IntPtr GetBattleBuddyMemberAddress(int index) + public unsafe nint GetBattleBuddyMemberAddress(int index) { if (index < 0 || index >= 3) - return IntPtr.Zero; + return 0; - return (IntPtr)Unsafe.AsPointer(ref this.BuddyListStruct->BattleBuddies[index]); + return (nint)Unsafe.AsPointer(ref this.BuddyListStruct->BattleBuddies[index]); } /// - public IBuddyMember? CreateBuddyMemberReference(IntPtr address) + public unsafe IBuddyMember? CreateBuddyMemberReference(nint address) { + if (address == 0) + return null; + if (this.clientState.LocalContentId == 0) return null; - if (address == IntPtr.Zero) - return null; - - var buddy = new BuddyMember(address); - if (buddy.ObjectId == InvalidObjectID) + var buddy = new BuddyMember((CSBuddyMember*)address); + if (buddy.EntityId == InvalidEntityId) return null; return buddy; diff --git a/Dalamud/Game/ClientState/Buddy/BuddyMember.cs b/Dalamud/Game/ClientState/Buddy/BuddyMember.cs index 393598d32..8018bafaf 100644 --- a/Dalamud/Game/ClientState/Buddy/BuddyMember.cs +++ b/Dalamud/Game/ClientState/Buddy/BuddyMember.cs @@ -1,20 +1,24 @@ +using System.Diagnostics.CodeAnalysis; + using Dalamud.Data; using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState.Objects.Types; using Lumina.Excel; +using CSBuddyMember = FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy.BuddyMember; + namespace Dalamud.Game.ClientState.Buddy; /// /// Interface representing represents a buddy such as the chocobo companion, summoned pets, squadron groups and trust parties. /// -public interface IBuddyMember +public interface IBuddyMember : IEquatable { /// /// Gets the address of the buddy in memory. /// - IntPtr Address { get; } + nint Address { get; } /// /// Gets the object ID of this buddy. @@ -67,42 +71,34 @@ public interface IBuddyMember } /// -/// This class represents a buddy such as the chocobo companion, summoned pets, squadron groups and trust parties. +/// This struct represents a buddy such as the chocobo companion, summoned pets, squadron groups and trust parties. /// -internal unsafe class BuddyMember : IBuddyMember +/// A pointer to the BuddyMember. +internal readonly unsafe struct BuddyMember(CSBuddyMember* ptr) : IBuddyMember { [ServiceManager.ServiceDependency] private readonly ObjectTable objectTable = Service.Get(); - /// - /// Initializes a new instance of the class. - /// - /// Buddy address. - internal BuddyMember(IntPtr address) - { - this.Address = address; - } + /// + public nint Address => (nint)ptr; /// - public IntPtr Address { get; } + public uint ObjectId => this.EntityId; /// - public uint ObjectId => this.Struct->EntityId; + public uint EntityId => ptr->EntityId; /// - public uint EntityId => this.Struct->EntityId; + public IGameObject? GameObject => this.objectTable.SearchById(this.EntityId); /// - public IGameObject? GameObject => this.objectTable.SearchById(this.ObjectId); + public uint CurrentHP => ptr->CurrentHealth; /// - public uint CurrentHP => this.Struct->CurrentHealth; + public uint MaxHP => ptr->MaxHealth; /// - public uint MaxHP => this.Struct->MaxHealth; - - /// - public uint DataID => this.Struct->DataId; + public uint DataID => ptr->DataId; /// public RowRef MountData => LuminaUtils.CreateRef(this.DataID); @@ -113,5 +109,25 @@ internal unsafe class BuddyMember : IBuddyMember /// public RowRef TrustData => LuminaUtils.CreateRef(this.DataID); - private FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy.BuddyMember* Struct => (FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy.BuddyMember*)this.Address; + public static bool operator ==(BuddyMember x, BuddyMember y) => x.Equals(y); + + public static bool operator !=(BuddyMember x, BuddyMember y) => !(x == y); + + /// + public bool Equals(IBuddyMember? other) + { + return this.EntityId == other.EntityId; + } + + /// + public override bool Equals([NotNullWhen(true)] object? obj) + { + return obj is BuddyMember fate && this.Equals(fate); + } + + /// + public override int GetHashCode() + { + return this.EntityId.GetHashCode(); + } } From d1bed3ebc5e5f4ec8a7104c2e718babb4b546425 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Thu, 13 Nov 2025 18:42:31 +0100 Subject: [PATCH 13/27] Add struct enumerator to FateTable --- Dalamud/Game/ClientState/Fates/FateTable.cs | 31 ++++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/Dalamud/Game/ClientState/Fates/FateTable.cs b/Dalamud/Game/ClientState/Fates/FateTable.cs index 1bf557ad5..942d1561f 100644 --- a/Dalamud/Game/ClientState/Fates/FateTable.cs +++ b/Dalamud/Game/ClientState/Fates/FateTable.cs @@ -110,12 +110,35 @@ internal sealed partial class FateTable /// public IEnumerator GetEnumerator() { - for (var i = 0; i < this.Length; i++) - { - yield return this[i]; - } + return new Enumerator(this); } /// IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + + private struct Enumerator(FateTable fateTable) : IEnumerator + { + private int index = 0; + + public IFate Current { get; private set; } + + object IEnumerator.Current => this.Current; + + public bool MoveNext() + { + if (this.index == fateTable.Length) return false; + this.Current = fateTable[this.index]; + this.index++; + return true; + } + + public void Reset() + { + this.index = 0; + } + + public void Dispose() + { + } + } } From a48eead85e9f9fb98fcaa841c34752b7dca700e2 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Thu, 13 Nov 2025 18:43:06 +0100 Subject: [PATCH 14/27] Convert Fate to readonly struct --- Dalamud/Game/ClientState/Fates/Fate.cs | 130 ++++++++------------ Dalamud/Game/ClientState/Fates/FateTable.cs | 22 ++-- 2 files changed, 59 insertions(+), 93 deletions(-) diff --git a/Dalamud/Game/ClientState/Fates/Fate.cs b/Dalamud/Game/ClientState/Fates/Fate.cs index 504b690c3..c40a8960e 100644 --- a/Dalamud/Game/ClientState/Fates/Fate.cs +++ b/Dalamud/Game/ClientState/Fates/Fate.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using System.Numerics; using Dalamud.Data; @@ -6,10 +7,12 @@ using Dalamud.Memory; using Lumina.Excel; +using CSFateContext = FFXIVClientStructs.FFXIV.Client.Game.Fate.FateContext; + namespace Dalamud.Game.ClientState.Fates; /// -/// Interface representing an fate entry that can be seen in the current area. +/// Interface representing a fate entry that can be seen in the current area. /// public interface IFate : IEquatable { @@ -111,133 +114,96 @@ public interface IFate : IEquatable /// /// Gets the address of this Fate in memory. /// - IntPtr Address { get; } + nint Address { get; } } /// -/// This class represents an FFXIV Fate. +/// This struct represents a Fate. /// -internal unsafe partial class Fate +/// A pointer to the FateContext. +internal readonly unsafe struct Fate(CSFateContext* ptr) : IFate { - /// - /// Initializes a new instance of the class. - /// - /// The address of this fate in memory. - internal Fate(IntPtr address) - { - this.Address = address; - } - /// - public IntPtr Address { get; } - - private FFXIVClientStructs.FFXIV.Client.Game.Fate.FateContext* Struct => (FFXIVClientStructs.FFXIV.Client.Game.Fate.FateContext*)this.Address; - - public static bool operator ==(Fate fate1, Fate fate2) - { - if (fate1 is null || fate2 is null) - return Equals(fate1, fate2); - - return fate1.Equals(fate2); - } - - public static bool operator !=(Fate fate1, Fate fate2) => !(fate1 == fate2); - - /// - /// Gets a value indicating whether this Fate is still valid in memory. - /// - /// The fate to check. - /// True or false. - public static bool IsValid(Fate fate) - { - var clientState = Service.GetNullable(); - - if (fate == null || clientState == null) - return false; - - if (clientState.LocalContentId == 0) - return false; - - return true; - } - - /// - /// Gets a value indicating whether this actor is still valid in memory. - /// - /// True or false. - public bool IsValid() => IsValid(this); + public nint Address => (nint)ptr; /// - bool IEquatable.Equals(IFate other) => this.FateId == other?.FateId; - - /// - public override bool Equals(object obj) => ((IEquatable)this).Equals(obj as IFate); - - /// - public override int GetHashCode() => this.FateId.GetHashCode(); -} - -/// -/// This class represents an FFXIV Fate. -/// -internal unsafe partial class Fate : IFate -{ - /// - public ushort FateId => this.Struct->FateId; + public ushort FateId => ptr->FateId; /// public RowRef GameData => LuminaUtils.CreateRef(this.FateId); /// - public int StartTimeEpoch => this.Struct->StartTimeEpoch; + public int StartTimeEpoch => ptr->StartTimeEpoch; /// - public short Duration => this.Struct->Duration; + public short Duration => ptr->Duration; /// public long TimeRemaining => this.StartTimeEpoch + this.Duration - DateTimeOffset.Now.ToUnixTimeSeconds(); /// - public SeString Name => MemoryHelper.ReadSeString(&this.Struct->Name); + public SeString Name => MemoryHelper.ReadSeString(&ptr->Name); /// - public SeString Description => MemoryHelper.ReadSeString(&this.Struct->Description); + public SeString Description => MemoryHelper.ReadSeString(&ptr->Description); /// - public SeString Objective => MemoryHelper.ReadSeString(&this.Struct->Objective); + public SeString Objective => MemoryHelper.ReadSeString(&ptr->Objective); /// - public FateState State => (FateState)this.Struct->State; + public FateState State => (FateState)ptr->State; /// - public byte HandInCount => this.Struct->HandInCount; + public byte HandInCount => ptr->HandInCount; /// - public byte Progress => this.Struct->Progress; + public byte Progress => ptr->Progress; /// - public bool HasBonus => this.Struct->IsBonus; + public bool HasBonus => ptr->IsBonus; /// - public uint IconId => this.Struct->IconId; + public uint IconId => ptr->IconId; /// - public byte Level => this.Struct->Level; + public byte Level => ptr->Level; /// - public byte MaxLevel => this.Struct->MaxLevel; + public byte MaxLevel => ptr->MaxLevel; /// - public Vector3 Position => this.Struct->Location; + public Vector3 Position => ptr->Location; /// - public float Radius => this.Struct->Radius; + public float Radius => ptr->Radius; /// - public uint MapIconId => this.Struct->MapIconId; + public uint MapIconId => ptr->MapIconId; /// /// Gets the territory this is located in. /// - public RowRef TerritoryType => LuminaUtils.CreateRef(this.Struct->MapMarkers[0].MapMarkerData.TerritoryTypeId); + public RowRef TerritoryType => LuminaUtils.CreateRef(ptr->MapMarkers[0].MapMarkerData.TerritoryTypeId); + + public static bool operator ==(Fate x, Fate y) => x.Equals(y); + + public static bool operator !=(Fate x, Fate y) => !(x == y); + + /// + public bool Equals(IFate? other) + { + return this.FateId == other.FateId; + } + + /// + public override bool Equals([NotNullWhen(true)] object? obj) + { + return obj is Fate fate && this.Equals(fate); + } + + /// + public override int GetHashCode() + { + return this.FateId.GetHashCode(); + } } diff --git a/Dalamud/Game/ClientState/Fates/FateTable.cs b/Dalamud/Game/ClientState/Fates/FateTable.cs index 942d1561f..a6edf4a18 100644 --- a/Dalamud/Game/ClientState/Fates/FateTable.cs +++ b/Dalamud/Game/ClientState/Fates/FateTable.cs @@ -5,6 +5,7 @@ using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Plugin.Services; +using CSFateContext = FFXIVClientStructs.FFXIV.Client.Game.Fate.FateContext; using CSFateManager = FFXIVClientStructs.FFXIV.Client.Game.Fate.FateManager; namespace Dalamud.Game.ClientState.Fates; @@ -25,7 +26,7 @@ internal sealed partial class FateTable : IServiceType, IFateTable } /// - public unsafe IntPtr Address => (nint)CSFateManager.Instance(); + public unsafe nint Address => (nint)CSFateManager.Instance(); /// public unsafe int Length @@ -72,30 +73,29 @@ internal sealed partial class FateTable : IServiceType, IFateTable } /// - public unsafe IntPtr GetFateAddress(int index) + public unsafe nint GetFateAddress(int index) { if (index >= this.Length) - return IntPtr.Zero; + return 0; var fateManager = CSFateManager.Instance(); if (fateManager == null) - return IntPtr.Zero; + return 0; - return (IntPtr)fateManager->Fates[index].Value; + return (nint)fateManager->Fates[index].Value; } /// - public IFate? CreateFateReference(IntPtr offset) + public unsafe IFate? CreateFateReference(IntPtr address) { - var clientState = Service.Get(); + if (address == 0) + return null; + var clientState = Service.Get(); if (clientState.LocalContentId == 0) return null; - if (offset == IntPtr.Zero) - return null; - - return new Fate(offset); + return new Fate((CSFateContext*)address); } } From d1dc81318a8aa26fbf35da9de96fba3fb369edd5 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Thu, 13 Nov 2025 18:47:55 +0100 Subject: [PATCH 15/27] Add struct enumerator to PartyList --- Dalamud/Game/ClientState/Party/PartyList.cs | 46 ++++++++++++++++----- 1 file changed, 36 insertions(+), 10 deletions(-) diff --git a/Dalamud/Game/ClientState/Party/PartyList.cs b/Dalamud/Game/ClientState/Party/PartyList.cs index a016a8211..bfd423a79 100644 --- a/Dalamud/Game/ClientState/Party/PartyList.cs +++ b/Dalamud/Game/ClientState/Party/PartyList.cs @@ -133,18 +133,44 @@ internal sealed partial class PartyList /// public IEnumerator GetEnumerator() { - // Normally using Length results in a recursion crash, however we know the party size via ptr. - for (var i = 0; i < this.Length; i++) - { - var member = this[i]; - - if (member == null) - break; - - yield return member; - } + return new Enumerator(this); } /// IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + + private struct Enumerator(PartyList partyList) : IEnumerator + { + private int index = 0; + + public IPartyMember Current { get; private set; } + + object IEnumerator.Current => this.Current; + + public bool MoveNext() + { + if (this.index == partyList.Length) return false; + + for (; this.index < partyList.Length; this.index++) + { + var partyMember = partyList[this.index]; + if (partyMember != null) + { + this.Current = partyMember; + return true; + } + } + + return false; + } + + public void Reset() + { + this.index = 0; + } + + public void Dispose() + { + } + } } From 53b94caeb7470dc7f2ca639412188f38a7f19343 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Thu, 13 Nov 2025 18:53:03 +0100 Subject: [PATCH 16/27] Convert PartyMember to readonly struct --- Dalamud/Game/ClientState/Party/PartyList.cs | 31 ++++---- Dalamud/Game/ClientState/Party/PartyMember.cs | 79 +++++++++++-------- 2 files changed, 62 insertions(+), 48 deletions(-) diff --git a/Dalamud/Game/ClientState/Party/PartyList.cs b/Dalamud/Game/ClientState/Party/PartyList.cs index bfd423a79..ec22932ab 100644 --- a/Dalamud/Game/ClientState/Party/PartyList.cs +++ b/Dalamud/Game/ClientState/Party/PartyList.cs @@ -8,6 +8,7 @@ using Dalamud.IoC.Internal; using Dalamud.Plugin.Services; using CSGroupManager = FFXIVClientStructs.FFXIV.Client.Game.Group.GroupManager; +using CSPartyMember = FFXIVClientStructs.FFXIV.Client.Game.Group.PartyMember; namespace Dalamud.Game.ClientState.Party; @@ -42,20 +43,20 @@ internal sealed unsafe partial class PartyList : IServiceType, IPartyList public bool IsAlliance => this.GroupManagerStruct->MainGroup.AllianceFlags > 0; /// - public unsafe IntPtr GroupManagerAddress => (nint)CSGroupManager.Instance(); + public unsafe nint GroupManagerAddress => (nint)CSGroupManager.Instance(); /// - public IntPtr GroupListAddress => (IntPtr)Unsafe.AsPointer(ref GroupManagerStruct->MainGroup.PartyMembers[0]); + public nint GroupListAddress => (nint)Unsafe.AsPointer(ref GroupManagerStruct->MainGroup.PartyMembers[0]); /// - public IntPtr AllianceListAddress => (IntPtr)Unsafe.AsPointer(ref this.GroupManagerStruct->MainGroup.AllianceMembers[0]); + public nint AllianceListAddress => (nint)Unsafe.AsPointer(ref this.GroupManagerStruct->MainGroup.AllianceMembers[0]); /// public long PartyId => this.GroupManagerStruct->MainGroup.PartyId; - private static int PartyMemberSize { get; } = Marshal.SizeOf(); + private static int PartyMemberSize { get; } = Marshal.SizeOf(); - private FFXIVClientStructs.FFXIV.Client.Game.Group.GroupManager* GroupManagerStruct => (FFXIVClientStructs.FFXIV.Client.Game.Group.GroupManager*)this.GroupManagerAddress; + private CSGroupManager* GroupManagerStruct => (CSGroupManager*)this.GroupManagerAddress; /// public IPartyMember? this[int index] @@ -80,45 +81,45 @@ internal sealed unsafe partial class PartyList : IServiceType, IPartyList } /// - public IntPtr GetPartyMemberAddress(int index) + public nint GetPartyMemberAddress(int index) { if (index < 0 || index >= GroupLength) - return IntPtr.Zero; + return 0; return this.GroupListAddress + (index * PartyMemberSize); } /// - public IPartyMember? CreatePartyMemberReference(IntPtr address) + public IPartyMember? CreatePartyMemberReference(nint address) { if (this.clientState.LocalContentId == 0) return null; - if (address == IntPtr.Zero) + if (address == 0) return null; - return new PartyMember(address); + return new PartyMember((CSPartyMember*)address); } /// - public IntPtr GetAllianceMemberAddress(int index) + public nint GetAllianceMemberAddress(int index) { if (index < 0 || index >= AllianceLength) - return IntPtr.Zero; + return 0; return this.AllianceListAddress + (index * PartyMemberSize); } /// - public IPartyMember? CreateAllianceMemberReference(IntPtr address) + public IPartyMember? CreateAllianceMemberReference(nint address) { if (this.clientState.LocalContentId == 0) return null; - if (address == IntPtr.Zero) + if (address == 0) return null; - return new PartyMember(address); + return new PartyMember((CSPartyMember*)address); } } diff --git a/Dalamud/Game/ClientState/Party/PartyMember.cs b/Dalamud/Game/ClientState/Party/PartyMember.cs index 4c738d866..c9980d9f2 100644 --- a/Dalamud/Game/ClientState/Party/PartyMember.cs +++ b/Dalamud/Game/ClientState/Party/PartyMember.cs @@ -1,26 +1,27 @@ +using System.Diagnostics.CodeAnalysis; using System.Numerics; -using System.Runtime.CompilerServices; using Dalamud.Data; using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.ClientState.Statuses; using Dalamud.Game.Text.SeStringHandling; -using Dalamud.Memory; using Lumina.Excel; +using CSPartyMember = FFXIVClientStructs.FFXIV.Client.Game.Group.PartyMember; + namespace Dalamud.Game.ClientState.Party; /// /// Interface representing a party member. /// -public interface IPartyMember +public interface IPartyMember : IEquatable { /// /// Gets the address of this party member in memory. /// - IntPtr Address { get; } + nint Address { get; } /// /// Gets a list of buffs or debuffs applied to this party member. @@ -108,69 +109,81 @@ public interface IPartyMember } /// -/// This class represents a party member in the group manager. +/// This struct represents a party member in the group manager. /// -internal unsafe class PartyMember : IPartyMember +/// A pointer to the PartyMember. +internal unsafe readonly struct PartyMember(CSPartyMember* ptr) : IPartyMember { - /// - /// Initializes a new instance of the class. - /// - /// Address of the party member. - internal PartyMember(IntPtr address) - { - this.Address = address; - } + /// + public nint Address => (nint)ptr; /// - public IntPtr Address { get; } + public StatusList Statuses => new(&ptr->StatusManager); /// - public StatusList Statuses => new(&this.Struct->StatusManager); + public Vector3 Position => ptr->Position; /// - public Vector3 Position => this.Struct->Position; + public long ContentId => (long)ptr->ContentId; /// - public long ContentId => (long)this.Struct->ContentId; + public uint ObjectId => ptr->EntityId; /// - public uint ObjectId => this.Struct->EntityId; - - /// - public uint EntityId => this.Struct->EntityId; + public uint EntityId => ptr->EntityId; /// public IGameObject? GameObject => Service.Get().SearchById(this.EntityId); /// - public uint CurrentHP => this.Struct->CurrentHP; + public uint CurrentHP => ptr->CurrentHP; /// - public uint MaxHP => this.Struct->MaxHP; + public uint MaxHP => ptr->MaxHP; /// - public ushort CurrentMP => this.Struct->CurrentMP; + public ushort CurrentMP => ptr->CurrentMP; /// - public ushort MaxMP => this.Struct->MaxMP; + public ushort MaxMP => ptr->MaxMP; /// - public RowRef Territory => LuminaUtils.CreateRef(this.Struct->TerritoryType); + public RowRef Territory => LuminaUtils.CreateRef(ptr->TerritoryType); /// - public RowRef World => LuminaUtils.CreateRef(this.Struct->HomeWorld); + public RowRef World => LuminaUtils.CreateRef(ptr->HomeWorld); /// - public SeString Name => SeString.Parse(this.Struct->Name); + public SeString Name => SeString.Parse(ptr->Name); /// - public byte Sex => this.Struct->Sex; + public byte Sex => ptr->Sex; /// - public RowRef ClassJob => LuminaUtils.CreateRef(this.Struct->ClassJob); + public RowRef ClassJob => LuminaUtils.CreateRef(ptr->ClassJob); /// - public byte Level => this.Struct->Level; + public byte Level => ptr->Level; - private FFXIVClientStructs.FFXIV.Client.Game.Group.PartyMember* Struct => (FFXIVClientStructs.FFXIV.Client.Game.Group.PartyMember*)this.Address; + public static bool operator ==(PartyMember x, PartyMember y) => x.Equals(y); + + public static bool operator !=(PartyMember x, PartyMember y) => !(x == y); + + /// + public bool Equals(IPartyMember? other) + { + return this.EntityId == other.EntityId; + } + + /// + public override bool Equals([NotNullWhen(true)] object? obj) + { + return obj is PartyMember fate && this.Equals(fate); + } + + /// + public override int GetHashCode() + { + return this.EntityId.GetHashCode(); + } } From 7f2ed9adb6534934e6440b288fd6c753bddbe67c Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Thu, 13 Nov 2025 19:01:04 +0100 Subject: [PATCH 17/27] Convert Status to readonly struct and add interface --- Dalamud/Game/ClientState/Statuses/Status.cs | 86 +++++++++++++++------ 1 file changed, 61 insertions(+), 25 deletions(-) diff --git a/Dalamud/Game/ClientState/Statuses/Status.cs b/Dalamud/Game/ClientState/Statuses/Status.cs index 2775f8f9b..160b15de5 100644 --- a/Dalamud/Game/ClientState/Statuses/Status.cs +++ b/Dalamud/Game/ClientState/Statuses/Status.cs @@ -1,61 +1,49 @@ +using System.Diagnostics.CodeAnalysis; + using Dalamud.Data; using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState.Objects.Types; using Lumina.Excel; +using CSStatus = FFXIVClientStructs.FFXIV.Client.Game.Status; + namespace Dalamud.Game.ClientState.Statuses; /// -/// This class represents a status effect an actor is afflicted by. +/// Interface representing a status. /// -public unsafe class Status +public interface IStatus : IEquatable { - /// - /// Initializes a new instance of the class. - /// - /// Status address. - internal Status(IntPtr address) - { - this.Address = address; - } - /// /// Gets the address of the status in memory. /// - public IntPtr Address { get; } + nint Address { get; } /// /// Gets the status ID of this status. /// - public uint StatusId => this.Struct->StatusId; + uint StatusId { get; } /// /// Gets the GameData associated with this status. /// - public RowRef GameData => LuminaUtils.CreateRef(this.Struct->StatusId); + RowRef GameData { get; } /// /// Gets the parameter value of the status. /// - public ushort Param => this.Struct->Param; - - /// - /// Gets the stack count of this status. - /// Only valid if this is a non-food status. - /// - [Obsolete($"Replaced with {nameof(Param)}", true)] - public byte StackCount => (byte)this.Struct->Param; + ushort Param { get; } /// /// Gets the time remaining of this status. /// - public float RemainingTime => this.Struct->RemainingTime; + float RemainingTime { get; } /// /// Gets the source ID of this status. /// - public uint SourceId => this.Struct->SourceObject.ObjectId; + uint SourceId { get; } /// /// Gets the source actor associated with this status. @@ -63,7 +51,55 @@ public unsafe class Status /// /// This iterates the actor table, it should be used with care. /// + IGameObject? SourceObject { get; } +} + +/// +/// This struct represents a status effect an actor is afflicted by. +/// +/// A pointer to the Status. +internal unsafe readonly struct Status(CSStatus* ptr) : IStatus +{ + /// + public nint Address => (nint)ptr; + + /// + public uint StatusId => ptr->StatusId; + + /// + public RowRef GameData => LuminaUtils.CreateRef(ptr->StatusId); + + /// + public ushort Param => ptr->Param; + + /// + public float RemainingTime => ptr->RemainingTime; + + /// + public uint SourceId => ptr->SourceObject.ObjectId; + + /// public IGameObject? SourceObject => Service.Get().SearchById(this.SourceId); - private FFXIVClientStructs.FFXIV.Client.Game.Status* Struct => (FFXIVClientStructs.FFXIV.Client.Game.Status*)this.Address; + public static bool operator ==(Status x, Status y) => x.Equals(y); + + public static bool operator !=(Status x, Status y) => !(x == y); + + /// + public bool Equals(IStatus? other) + { + return this.StatusId == other.StatusId && this.SourceId == other.SourceId && this.Param == other.Param && this.RemainingTime == other.RemainingTime; + } + + /// + public override bool Equals([NotNullWhen(true)] object? obj) + { + return obj is Status fate && this.Equals(fate); + } + + /// + public override int GetHashCode() + { + return HashCode.Combine(this.StatusId, this.SourceId, this.Param, this.RemainingTime); + } } From 778c82fad2c369e59e442dab8b0a1e3eb7000373 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Thu, 13 Nov 2025 19:02:50 +0100 Subject: [PATCH 18/27] Add struct enumerator to StatusList --- .../Game/ClientState/Statuses/StatusList.cs | 77 +++++++++++++------ 1 file changed, 53 insertions(+), 24 deletions(-) diff --git a/Dalamud/Game/ClientState/Statuses/StatusList.cs b/Dalamud/Game/ClientState/Statuses/StatusList.cs index a38e45ea3..50d242d33 100644 --- a/Dalamud/Game/ClientState/Statuses/StatusList.cs +++ b/Dalamud/Game/ClientState/Statuses/StatusList.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using CSStatus = FFXIVClientStructs.FFXIV.Client.Game.Status; + namespace Dalamud.Game.ClientState.Statuses; /// @@ -14,7 +16,7 @@ public sealed unsafe partial class StatusList /// Initializes a new instance of the class. /// /// Address of the status list. - internal StatusList(IntPtr address) + internal StatusList(nint address) { this.Address = address; } @@ -24,14 +26,14 @@ public sealed unsafe partial class StatusList /// /// Pointer to the status list. internal unsafe StatusList(void* pointer) - : this((IntPtr)pointer) + : this((nint)pointer) { } /// /// Gets the address of the status list in memory. /// - public IntPtr Address { get; } + public nint Address { get; } /// /// Gets the amount of status effect slots the actor has. @@ -47,7 +49,7 @@ public sealed unsafe partial class StatusList /// /// Status Index. /// The status at the specified index. - public Status? this[int index] + public IStatus? this[int index] { get { @@ -64,7 +66,7 @@ public sealed unsafe partial class StatusList /// /// The address of the status list in memory. /// The status object containing the requested data. - public static StatusList? CreateStatusListReference(IntPtr address) + public static StatusList? CreateStatusListReference(nint address) { // The use case for CreateStatusListReference and CreateStatusReference to be static is so // fake status lists can be generated. Since they aren't exposed as services, it's either @@ -74,7 +76,7 @@ public sealed unsafe partial class StatusList if (clientState.LocalContentId == 0) return null; - if (address == IntPtr.Zero) + if (address == 0) return null; return new StatusList(address); @@ -85,17 +87,17 @@ public sealed unsafe partial class StatusList /// /// The address of the status effect in memory. /// The status object containing the requested data. - public static Status? CreateStatusReference(IntPtr address) + public static IStatus? CreateStatusReference(nint address) { var clientState = Service.Get(); if (clientState.LocalContentId == 0) return null; - if (address == IntPtr.Zero) + if (address == 0) return null; - return new Status(address); + return new Status((CSStatus*)address); } /// @@ -103,22 +105,22 @@ public sealed unsafe partial class StatusList /// /// The index of the status. /// The memory address of the status. - public IntPtr GetStatusAddress(int index) + public nint GetStatusAddress(int index) { if (index < 0 || index >= this.Length) - return IntPtr.Zero; + return 0; - return (IntPtr)Unsafe.AsPointer(ref this.Struct->Status[index]); + return (nint)Unsafe.AsPointer(ref this.Struct->Status[index]); } } /// /// This collection represents the status effects an actor is afflicted by. /// -public sealed partial class StatusList : IReadOnlyCollection, ICollection +public sealed partial class StatusList : IReadOnlyCollection, ICollection { /// - int IReadOnlyCollection.Count => this.Length; + int IReadOnlyCollection.Count => this.Length; /// int ICollection.Count => this.Length; @@ -130,17 +132,9 @@ public sealed partial class StatusList : IReadOnlyCollection, ICollectio object ICollection.SyncRoot => this; /// - public IEnumerator GetEnumerator() + public IEnumerator GetEnumerator() { - for (var i = 0; i < this.Length; i++) - { - var status = this[i]; - - if (status == null || status.StatusId == 0) - continue; - - yield return status; - } + return new Enumerator(this); } /// @@ -155,4 +149,39 @@ public sealed partial class StatusList : IReadOnlyCollection, ICollectio index++; } } + + private struct Enumerator(StatusList statusList) : IEnumerator + { + private int index = 0; + + public IStatus Current { get; private set; } + + object IEnumerator.Current => this.Current; + + public bool MoveNext() + { + if (this.index == statusList.Length) return false; + + for (; this.index < statusList.Length; this.index++) + { + var status = statusList[this.index]; + if (status != null && status.StatusId != 0) + { + this.Current = status; + return true; + } + } + + return false; + } + + public void Reset() + { + this.index = 0; + } + + public void Dispose() + { + } + } } From 9d0879148c740942523da5310c5b8039cb3be707 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Thu, 13 Nov 2025 19:05:25 +0100 Subject: [PATCH 19/27] Remove unused StatusEffect struct --- .../Game/ClientState/Structs/StatusEffect.cs | 35 ------------------- 1 file changed, 35 deletions(-) delete mode 100644 Dalamud/Game/ClientState/Structs/StatusEffect.cs diff --git a/Dalamud/Game/ClientState/Structs/StatusEffect.cs b/Dalamud/Game/ClientState/Structs/StatusEffect.cs deleted file mode 100644 index 2a60a7d3b..000000000 --- a/Dalamud/Game/ClientState/Structs/StatusEffect.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Runtime.InteropServices; - -namespace Dalamud.Game.ClientState.Structs; - -/// -/// Native memory representation of a FFXIV status effect. -/// -[StructLayout(LayoutKind.Sequential)] -public struct StatusEffect -{ - /// - /// The effect ID. - /// - public short EffectId; - - /// - /// How many stacks are present. - /// - public byte StackCount; - - /// - /// Additional parameters. - /// - public byte Param; - - /// - /// The duration remaining. - /// - public float Duration; - - /// - /// The ID of the actor that caused this effect. - /// - public int OwnerId; -} From 78ed4a2b01f2fb4f4ff86ea5eab2c7eca7e6b015 Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Sun, 16 Nov 2025 15:55:35 -0800 Subject: [PATCH 20/27] feat: Dalamud RPC service A draft for a simple RPC service for Dalamud. Enables use of Dalamud URIs, to be added later. --- Dalamud.Test/Pipes/DalamudUriTests.cs | 107 +++++++++++ Dalamud/Dalamud.csproj | 1 + .../Networking/Pipes/Api/PluginLinkHandler.cs | 53 ++++++ Dalamud/Networking/Pipes/DalamudUri.cs | 102 +++++++++++ .../Pipes/Internal/ClientHelloService.cs | 94 ++++++++++ .../Pipes/Internal/LinkHandlerService.cs | 129 ++++++++++++++ Dalamud/Networking/Pipes/Rpc/PipeRpcHost.cs | 167 ++++++++++++++++++ Dalamud/Networking/Pipes/Rpc/RpcConnection.cs | 92 ++++++++++ .../Networking/Pipes/Rpc/RpcHostService.cs | 49 +++++ .../Pipes/Rpc/RpcServiceRegistry.cs | 85 +++++++++ Dalamud/Plugin/Services/IPluginLinkHandler.cs | 20 +++ Directory.Packages.props | 13 +- 12 files changed, 911 insertions(+), 1 deletion(-) create mode 100644 Dalamud.Test/Pipes/DalamudUriTests.cs create mode 100644 Dalamud/Networking/Pipes/Api/PluginLinkHandler.cs create mode 100644 Dalamud/Networking/Pipes/DalamudUri.cs create mode 100644 Dalamud/Networking/Pipes/Internal/ClientHelloService.cs create mode 100644 Dalamud/Networking/Pipes/Internal/LinkHandlerService.cs create mode 100644 Dalamud/Networking/Pipes/Rpc/PipeRpcHost.cs create mode 100644 Dalamud/Networking/Pipes/Rpc/RpcConnection.cs create mode 100644 Dalamud/Networking/Pipes/Rpc/RpcHostService.cs create mode 100644 Dalamud/Networking/Pipes/Rpc/RpcServiceRegistry.cs create mode 100644 Dalamud/Plugin/Services/IPluginLinkHandler.cs diff --git a/Dalamud.Test/Pipes/DalamudUriTests.cs b/Dalamud.Test/Pipes/DalamudUriTests.cs new file mode 100644 index 000000000..4977f3814 --- /dev/null +++ b/Dalamud.Test/Pipes/DalamudUriTests.cs @@ -0,0 +1,107 @@ +using System; +using System.Linq; + +using Dalamud.Networking.Pipes; +using Xunit; + +namespace Dalamud.Test.Pipes +{ + public class DalamudUriTests + { + [Theory] + [InlineData("https://www.google.com/", false)] + [InlineData("dalamud://PluginInstaller/Dalamud.FindAnything", true)] + public void ValidatesScheme(string uri, bool valid) + { + Action act = () => { _ = DalamudUri.FromUri(uri); }; + + var ex = Record.Exception(act); + if (valid) + { + Assert.Null(ex); + } + else + { + Assert.NotNull(ex); + Assert.IsType(ex); + } + } + + [Theory] + [InlineData("dalamud://PluginInstaller/Dalamud.FindAnything", "plugininstaller")] + [InlineData("dalamud://Plugin/Dalamud.FindAnything/OpenWindow", "plugin")] + [InlineData("dalamud://Test", "test")] + public void ExtractsNamespace(string uri, string expectedNamespace) + { + var dalamudUri = DalamudUri.FromUri(uri); + Assert.Equal(expectedNamespace, dalamudUri.Namespace); + } + + [Theory] + [InlineData("dalamud://foo/bar/baz/qux/?cow=moo", "/bar/baz/qux/")] + [InlineData("dalamud://foo/bar/baz/qux?cow=moo", "/bar/baz/qux")] + [InlineData("dalamud://foo/bar/baz", "/bar/baz")] + [InlineData("dalamud://foo/bar", "/bar")] + [InlineData("dalamud://foo/bar/", "/bar/")] + [InlineData("dalamud://foo/", "/")] + public void ExtractsPath(string uri, string expectedPath) + { + var dalamudUri = DalamudUri.FromUri(uri); + Assert.Equal(expectedPath, dalamudUri.Path); + } + + [Theory] + [InlineData("dalamud://foo/bar/baz/qux/?cow=moo#frag", "/bar/baz/qux/?cow=moo#frag")] + [InlineData("dalamud://foo/bar/baz/qux/?cow=moo", "/bar/baz/qux/?cow=moo")] + [InlineData("dalamud://foo/bar/baz/qux?cow=moo", "/bar/baz/qux?cow=moo")] + [InlineData("dalamud://foo/bar/baz", "/bar/baz")] + [InlineData("dalamud://foo/bar?cow=moo", "/bar?cow=moo")] + [InlineData("dalamud://foo/bar", "/bar")] + [InlineData("dalamud://foo/bar/?cow=moo", "/bar/?cow=moo")] + [InlineData("dalamud://foo/bar/", "/bar/")] + [InlineData("dalamud://foo/?cow=moo#chicken", "/?cow=moo#chicken")] + [InlineData("dalamud://foo/?cow=moo", "/?cow=moo")] + [InlineData("dalamud://foo/", "/")] + public void ExtractsData(string uri, string expectedData) + { + var dalamudUri = DalamudUri.FromUri(uri); + + Assert.Equal(expectedData, dalamudUri.Data); + } + + [Theory] + [InlineData("dalamud://foo/bar", 0)] + [InlineData("dalamud://foo/bar?cow=moo", 1)] + [InlineData("dalamud://foo/bar?cow=moo&wolf=awoo", 2)] + [InlineData("dalamud://foo/bar?cow=moo&wolf=awoo&cat", 3)] + public void ExtractsQueryParams(string uri, int queryCount) + { + var dalamudUri = DalamudUri.FromUri(uri); + Assert.Equal(queryCount, dalamudUri.QueryParams.Count); + } + + [Theory] + [InlineData("dalamud://foo/bar/baz/qux/meh/?foo=bar", 5, true)] + [InlineData("dalamud://foo/bar/baz/qux/meh/", 5, true)] + [InlineData("dalamud://foo/bar/baz/qux/meh", 5)] + [InlineData("dalamud://foo/bar/baz/qux", 4)] + [InlineData("dalamud://foo/bar/baz", 3)] + [InlineData("dalamud://foo/bar/", 2)] + [InlineData("dalamud://foo/bar", 2)] + public void ExtractsSegments(string uri, int segmentCount, bool finalSegmentEndsWithSlash = false) + { + var dalamudUri = DalamudUri.FromUri(uri); + var segments = dalamudUri.Segments; + + // First segment must always be `/` + Assert.Equal("/", segments[0]); + + Assert.Equal(segmentCount, segments.Length); + + if (finalSegmentEndsWithSlash) + { + Assert.EndsWith("/", segments.Last()); + } + } + } +} diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 1e5f9f586..849a5ce7f 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -81,6 +81,7 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Dalamud/Networking/Pipes/Api/PluginLinkHandler.cs b/Dalamud/Networking/Pipes/Api/PluginLinkHandler.cs new file mode 100644 index 000000000..2c99901b4 --- /dev/null +++ b/Dalamud/Networking/Pipes/Api/PluginLinkHandler.cs @@ -0,0 +1,53 @@ +using System.Linq; + +using Dalamud.IoC; +using Dalamud.IoC.Internal; +using Dalamud.Networking.Pipes.Internal; +using Dalamud.Plugin.Internal.Types; +using Dalamud.Plugin.Services; + +namespace Dalamud.Networking.Pipes.Api; + +/// +[PluginInterface] +[ServiceManager.ScopedService] +[ResolveVia] +public class PluginLinkHandler : IInternalDisposableService, IPluginLinkHandler +{ + private readonly LinkHandlerService linkHandler; + private readonly LocalPlugin localPlugin; + + /// + /// Initializes a new instance of the class. + /// + /// The plugin to bind this service to. + /// The central link handler. + internal PluginLinkHandler(LocalPlugin localPlugin, LinkHandlerService linkHandler) + { + this.linkHandler = linkHandler; + this.localPlugin = localPlugin; + + this.linkHandler.Register("plugin", this.HandleUri); + } + + /// + public event IPluginLinkHandler.PluginUriReceived? OnUriReceived; + + /// + public void DisposeService() + { + this.OnUriReceived = null; + this.linkHandler.Unregister("plugin", this.HandleUri); + } + + private void HandleUri(DalamudUri uri) + { + var target = uri.Path.Split("/").FirstOrDefault(); + if (target == null || !string.Equals(target, this.localPlugin.InternalName, StringComparison.OrdinalIgnoreCase)) + { + return; + } + + this.OnUriReceived?.Invoke(uri); + } +} diff --git a/Dalamud/Networking/Pipes/DalamudUri.cs b/Dalamud/Networking/Pipes/DalamudUri.cs new file mode 100644 index 000000000..03ad15af1 --- /dev/null +++ b/Dalamud/Networking/Pipes/DalamudUri.cs @@ -0,0 +1,102 @@ +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Web; + +namespace Dalamud.Networking.Pipes; + +/// +/// A Dalamud Uri, in the format: +/// dalamud://{NAMESPACE}/{ARBITRARY} +/// +public record DalamudUri +{ + private readonly Uri rawUri; + + private DalamudUri(Uri uri) + { + if (uri.Scheme != "dalamud") + { + throw new ArgumentOutOfRangeException(nameof(uri), "URI must be of scheme dalamud."); + } + + this.rawUri = uri; + } + + /// + /// Gets the namespace that this URI should be routed to. Generally a high level component like "PluginInstaller". + /// + public string Namespace => this.rawUri.Authority; + + /// + /// Gets the raw (untargeted) path and query params for this URI. + /// + public string Data => + this.rawUri.GetComponents(UriComponents.PathAndQuery | UriComponents.Fragment, UriFormat.UriEscaped); + + /// + /// Gets the raw (untargeted) path for this URI. + /// + public string Path => this.rawUri.AbsolutePath; + + /// + /// Gets a list of segments based on the provided Data element. + /// + public string[] Segments => this.GetDataSegments(); + + /// + /// Gets the raw query parameters for this URI, if any. + /// + public string Query => this.rawUri.Query; + + /// + /// Gets the query params (as a parsed NameValueCollection) in this URI. + /// + public NameValueCollection QueryParams => HttpUtility.ParseQueryString(this.Query); + + /// + /// Gets the fragment (if one is specified) in this URI. + /// + public string Fragment => this.rawUri.Fragment; + + /// + public override string ToString() => this.rawUri.ToString(); + + private string[] GetDataSegments() + { + // reimplementation of the System.URI#Segments, under MIT license. + var path = this.Path; + + var segments = new List(); + var current = 0; + while (current < path.Length) + { + var next = path.IndexOf('/', current); + if (next == -1) + { + next = path.Length - 1; + } + + segments.Add(path.Substring(current, (next - current) + 1)); + current = next + 1; + } + + return segments.ToArray(); + } + + /// + /// Build a DalamudURI from a given URI. + /// + /// The URI to convert to a Dalamud URI. + /// Returns a DalamudUri. + public static DalamudUri FromUri(Uri uri) + { + return new DalamudUri(uri); + } + + /// + /// Build a DalamudURI from a URI in string format. + /// + /// The URI to convert to a Dalamud URI. + /// Returns a DalamudUri. + public static DalamudUri FromUri(string uri) => FromUri(new Uri(uri)); +} diff --git a/Dalamud/Networking/Pipes/Internal/ClientHelloService.cs b/Dalamud/Networking/Pipes/Internal/ClientHelloService.cs new file mode 100644 index 000000000..cc06560bd --- /dev/null +++ b/Dalamud/Networking/Pipes/Internal/ClientHelloService.cs @@ -0,0 +1,94 @@ +using System.Threading.Tasks; + +using Dalamud.Game; +using Dalamud.Game.ClientState; +using Dalamud.Networking.Pipes.Rpc; +using Dalamud.Utility; + +namespace Dalamud.Networking.Pipes.Internal; + +/// +/// A minimal service to respond with information about this client. +/// +[ServiceManager.EarlyLoadedService] +internal sealed class ClientHelloService : IInternalDisposableService +{ + /// + /// Initializes a new instance of the class. + /// + /// Injected host service. + [ServiceManager.ServiceConstructor] + public ClientHelloService(RpcHostService rpcHostService) + { + rpcHostService.AddMethod("hello", this.HandleHello); + } + + /// + /// Handle a hello request. + /// + /// . + /// Respond with information. + public async Task HandleHello(ClientHelloRequest request) + { + var framework = await Service.GetAsync(); + var dalamud = await Service.GetAsync(); + var clientState = await Service.GetAsync(); + + var response = await framework.RunOnFrameworkThread(() => new ClientHelloResponse + { + ApiVersion = "1.0", + DalamudVersion = Util.GetScmVersion(), + GameVersion = dalamud.StartInfo.GameVersion?.ToString() ?? "Unknown", + PlayerName = clientState.IsLoggedIn ? clientState.LocalPlayer?.Name.ToString() ?? "Unknown" : null, + }); + + return response; + } + + /// + public void DisposeService() + { + } +} + +/// +/// A request from a client to say hello. +/// +internal record ClientHelloRequest +{ + /// + /// Gets the API version this client is expecting. + /// + public string ApiVersion { get; init; } = string.Empty; + + /// + /// Gets the user agent of the client. + /// + public string UserAgent { get; init; } = string.Empty; +} + +/// +/// A response from Dalamud to a hello request. +/// +internal record ClientHelloResponse +{ + /// + /// Gets the API version this server has offered. + /// + public string? ApiVersion { get; init; } + + /// + /// Gets the current Dalamud version. + /// + public string? DalamudVersion { get; init; } + + /// + /// Gets the current game version. + /// + public string? GameVersion { get; init; } + + /// + /// Gets or sets the player name, or null if the player isn't logged in. + /// + public string? PlayerName { get; set; } +} diff --git a/Dalamud/Networking/Pipes/Internal/LinkHandlerService.cs b/Dalamud/Networking/Pipes/Internal/LinkHandlerService.cs new file mode 100644 index 000000000..79bb1e017 --- /dev/null +++ b/Dalamud/Networking/Pipes/Internal/LinkHandlerService.cs @@ -0,0 +1,129 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; + +using Dalamud.Logging.Internal; +using Dalamud.Networking.Pipes.Rpc; + +namespace Dalamud.Networking.Pipes.Internal; + +/// +/// A service responsible for handling Dalamud URIs and dispatching them accordingly. +/// +[ServiceManager.EarlyLoadedService] +internal class LinkHandlerService : IInternalDisposableService +{ + private readonly ModuleLog log = new("LinkHandler"); + + // key: namespace (e.g. "plugin" or "PluginInstaller") -> list of handlers + private readonly ConcurrentDictionary>> handlers + = new(StringComparer.OrdinalIgnoreCase); + + /// + /// Initializes a new instance of the class. + /// + /// The injected RPC host service. + [ServiceManager.ServiceConstructor] + public LinkHandlerService(RpcHostService rpcHostService) + { + rpcHostService.AddMethod("handleLink", this.HandleLinkCall); + } + + /// + public void DisposeService() + { + } + + /// + /// Register a handler for a namespace. All URIs with this namespace will be dispatched to the handler. + /// + /// The namespace to use for this subscription. + /// The command handler. + public void Register(string ns, Action handler) + { + if (string.IsNullOrWhiteSpace(ns)) + throw new ArgumentNullException(nameof(ns)); + + var list = this.handlers.GetOrAdd(ns, _ => []); + lock (list) + { + list.Add(handler); + } + + this.log.Verbose("Registered handler for {Namespace}", ns); + } + + /// + /// Unregister a handler. + /// + /// The namespace to use for this subscription. + /// The command handler. + public void Unregister(string ns, Action handler) + { + if (string.IsNullOrWhiteSpace(ns)) + return; + + if (!this.handlers.TryGetValue(ns, out var list)) + return; + + lock (list) + { + list.RemoveAll(x => x == handler); + } + + if (list.Count == 0) + this.handlers.TryRemove(ns, out _); + + this.log.Verbose("Unregistered handler for {Namespace}", ns); + } + + /// + /// Dispatch a URI to matching handlers. + /// + /// The URI to parse and dispatch. + public void Dispatch(DalamudUri uri) + { + this.log.Information("Received URI: {Uri}", uri.ToString()); + + var ns = uri.Namespace; + if (!this.handlers.TryGetValue(ns, out var list)) + return; + + Action[] snapshot; + lock (list) + { + snapshot = list.ToArray(); + } + + foreach (var h in snapshot) + { + try + { + h(uri); + } + catch (Exception e) + { + this.log.Warning(e, "Link handler threw for {UriPath}", uri.Path); + } + } + } + + /// + /// The RPC-invokable link handler. + /// + /// A plain-text URI to parse. + public void HandleLinkCall(string uri) + { + if (string.IsNullOrWhiteSpace(uri)) + return; + + try + { + var du = DalamudUri.FromUri(uri); + this.Dispatch(du); + } + catch (Exception) + { + // swallow parse errors; clients shouldn't crash the host + } + } +} diff --git a/Dalamud/Networking/Pipes/Rpc/PipeRpcHost.cs b/Dalamud/Networking/Pipes/Rpc/PipeRpcHost.cs new file mode 100644 index 000000000..07dc9d96a --- /dev/null +++ b/Dalamud/Networking/Pipes/Rpc/PipeRpcHost.cs @@ -0,0 +1,167 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO.Pipes; +using System.Security.AccessControl; +using System.Security.Principal; +using System.Threading; +using System.Threading.Tasks; + +using Dalamud.Logging.Internal; +using Dalamud.Utility; + +namespace Dalamud.Networking.Pipes.Rpc; + +/// +/// Simple multi-client JSON-RPC named pipe host using StreamJsonRpc. +/// +internal class PipeRpcHost : IDisposable +{ + private readonly ModuleLog log = new("RPC/Host"); + + private readonly RpcServiceRegistry registry = new(); + private readonly CancellationTokenSource cts = new(); + private readonly ConcurrentDictionary sessions = new(); + private Task? acceptLoopTask; + + /// + /// Initializes a new instance of the class. + /// + /// The pipe name to create. + public PipeRpcHost(string? pipeName = null) + { + // Default pipe name based on current process ID for uniqueness per Dalamud instance. + this.PipeName = pipeName ?? $"DalamudRPC.{Environment.ProcessId}"; + } + + /// + /// Gets the name of the named pipe this RPC host is using. + /// + public string PipeName { get; } + + /// Adds a local object exposing RPC methods callable by clients. + /// An arbitrary service object that will be introspected to add to RPC. + public void AddService(object service) => this.registry.AddService(service); + + /// + /// Adds a standalone JSON-RPC method callable by clients. + /// + /// The name to add. + /// The delegate that acts as the handler. + public void AddMethod(string name, Delegate handler) => this.registry.AddMethod(name, handler); + + /// Starts accepting client connections. + public void Start() + { + if (this.acceptLoopTask != null) return; + this.acceptLoopTask = Task.Run(this.AcceptLoopAsync); + } + + /// Invoke an RPC request on a specific client expecting a result. + /// The client ID to invoke. + /// The method to invoke. + /// Any arguments to invoke. + /// An optional return based on the specified RPC. + /// The expected response type. + public Task InvokeClientAsync(Guid clientId, string method, params object[] arguments) + { + if (!this.sessions.TryGetValue(clientId, out var session)) + throw new KeyNotFoundException($"No client {clientId}"); + + return session.Rpc.InvokeAsync(method, arguments); + } + + /// Send a notification to all connected clients (no response expected). + /// The method name to broadcast. + /// The arguments to broadcast. + /// Returns a Task when completed. + public Task BroadcastNotifyAsync(string method, params object[] arguments) + { + var list = this.sessions.Values; + var tasks = new List(list.Count); + foreach (var s in list) + { + tasks.Add(s.Rpc.NotifyAsync(method, arguments)); + } + + return Task.WhenAll(tasks); + } + + /// + /// Gets a list of connected client IDs. + /// + /// Connected client IDs. + public IReadOnlyCollection GetClientIds() => this.sessions.Keys.AsReadOnlyCollection(); + + /// + public void Dispose() + { + this.cts.Cancel(); + this.acceptLoopTask?.Wait(1000); + + foreach (var kv in this.sessions) + { + kv.Value.Dispose(); + } + + this.sessions.Clear(); + this.cts.Dispose(); + this.log.Information("PipeRpcHost disposed ({Pipe})", this.PipeName); + GC.SuppressFinalize(this); + } + + private PipeSecurity BuildPipeSecurity() + { + var ps = new PipeSecurity(); + ps.AddAccessRule(new PipeAccessRule(WindowsIdentity.GetCurrent().User!, PipeAccessRights.FullControl, AccessControlType.Allow)); + + return ps; + } + + private async Task AcceptLoopAsync() + { + this.log.Information("PipeRpcHost starting on pipe {Pipe}", this.PipeName); + var token = this.cts.Token; + var security = this.BuildPipeSecurity(); + + while (!token.IsCancellationRequested) + { + NamedPipeServerStream? server = null; + try + { + server = NamedPipeServerStreamAcl.Create( + this.PipeName, + PipeDirection.InOut, + NamedPipeServerStream.MaxAllowedServerInstances, + PipeTransmissionMode.Message, + PipeOptions.Asynchronous, + 65536, + 65536, + security); + + await server.WaitForConnectionAsync(token).ConfigureAwait(false); + + var session = new RpcConnection(server, this.registry); + this.sessions.TryAdd(session.Id, session); + + this.log.Debug("RPC connection created: {Id}", session.Id); + + _ = session.Completion.ContinueWith(t => + { + this.sessions.TryRemove(session.Id, out _); + this.log.Debug("RPC connection removed: {Id}", session.Id); + }, TaskScheduler.Default); + } + catch (OperationCanceledException) + { + server?.Dispose(); + break; + } + catch (Exception ex) + { + server?.Dispose(); + this.log.Error(ex, "Error in pipe accept loop"); + await Task.Delay(500, token).ConfigureAwait(false); + } + } + } +} diff --git a/Dalamud/Networking/Pipes/Rpc/RpcConnection.cs b/Dalamud/Networking/Pipes/Rpc/RpcConnection.cs new file mode 100644 index 000000000..8e1c3a085 --- /dev/null +++ b/Dalamud/Networking/Pipes/Rpc/RpcConnection.cs @@ -0,0 +1,92 @@ +using System.IO.Pipes; +using System.Threading; +using System.Threading.Tasks; + +using Serilog; +using StreamJsonRpc; + +namespace Dalamud.Networking.Pipes.Rpc; + +/// +/// A single RPC client session connected via named pipe. +/// +internal class RpcConnection : IDisposable +{ + private readonly NamedPipeServerStream pipe; + private readonly RpcServiceRegistry registry; + private readonly CancellationTokenSource cts = new(); + + /// + /// Initializes a new instance of the class. + /// + /// The named pipe that this connection will handle. + /// A registry of RPC services. + public RpcConnection(NamedPipeServerStream pipe, RpcServiceRegistry registry) + { + this.Id = Guid.CreateVersion7(); + this.pipe = pipe; + this.registry = registry; + + var formatter = new JsonMessageFormatter(); + var handler = new HeaderDelimitedMessageHandler(pipe, pipe, formatter); + + this.Rpc = new JsonRpc(handler); + this.Rpc.AllowModificationWhileListening = true; + this.Rpc.Disconnected += this.OnDisconnected; + this.registry.Attach(this.Rpc); + + this.Rpc.StartListening(); + } + + /// + /// Gets the GUID for this connection. + /// + public Guid Id { get; } + + /// + /// Gets the JsonRpc instance for this connection. + /// + public JsonRpc Rpc { get; } + + /// + /// Gets a task that's called on RPC completion. + /// + public Task Completion => this.Rpc.Completion; + + /// + public void Dispose() + { + if (!this.cts.IsCancellationRequested) + { + this.cts.Cancel(); + } + + try + { + this.Rpc.Dispose(); + } + catch (Exception ex) + { + Log.Debug(ex, "Error disposing JsonRpc for client {Id}", this.Id); + } + + try + { + this.pipe.Dispose(); + } + catch (Exception ex) + { + Log.Debug(ex, "Error disposing pipe for client {Id}", this.Id); + } + + this.cts.Dispose(); + GC.SuppressFinalize(this); + } + + private void OnDisconnected(object? sender, JsonRpcDisconnectedEventArgs e) + { + Log.Debug("RPC client {Id} disconnected: {Reason}", this.Id, e.Description); + this.registry.Detach(this.Rpc); + this.Dispose(); + } +} diff --git a/Dalamud/Networking/Pipes/Rpc/RpcHostService.cs b/Dalamud/Networking/Pipes/Rpc/RpcHostService.cs new file mode 100644 index 000000000..78df27323 --- /dev/null +++ b/Dalamud/Networking/Pipes/Rpc/RpcHostService.cs @@ -0,0 +1,49 @@ +using Dalamud.Logging.Internal; + +namespace Dalamud.Networking.Pipes.Rpc; + +/// +/// The Dalamud service repsonsible for hosting the RPC. +/// +[ServiceManager.EarlyLoadedService] +internal class RpcHostService : IServiceType, IInternalDisposableService +{ + private readonly ModuleLog log = new("RPC"); + private readonly PipeRpcHost host; + + /// + /// Initializes a new instance of the class. + /// + [ServiceManager.ServiceConstructor] + public RpcHostService() + { + this.host = new PipeRpcHost(); + this.host.Start(); + + this.log.Information("RpcHostService started on pipe {Pipe}", this.host.PipeName); + } + + /// + /// Gets the RPC host to drill down. + /// + public PipeRpcHost Host => this.host; + + /// + /// Add a new service Object to the RPC host. + /// + /// The object to add. + public void AddService(object service) => this.host.AddService(service); + + /// + /// Add a new standalone method to the RPC host. + /// + /// The method name to add. + /// The handler to add. + public void AddMethod(string name, Delegate handler) => this.host.AddMethod(name, handler); + + /// + public void DisposeService() + { + this.host.Dispose(); + } +} diff --git a/Dalamud/Networking/Pipes/Rpc/RpcServiceRegistry.cs b/Dalamud/Networking/Pipes/Rpc/RpcServiceRegistry.cs new file mode 100644 index 000000000..71037d45e --- /dev/null +++ b/Dalamud/Networking/Pipes/Rpc/RpcServiceRegistry.cs @@ -0,0 +1,85 @@ +using System.Collections.Generic; +using System.Threading; + +using StreamJsonRpc; + +namespace Dalamud.Networking.Pipes.Rpc; + +/// +/// Thread-safe registry of local RPC target objects that are exposed to every connected JsonRpc session. +/// New sessions get all previously registered targets; newly added targets are attached to all active sessions. +/// +internal class RpcServiceRegistry +{ + private readonly Lock sync = new(); + private readonly List targets = []; + private readonly List<(string Name, Delegate Handler)> methods = []; + private readonly List activeRpcs = []; + + /// + /// Registers a new local RPC target object. Its public JSON-RPC methods become callable by clients. + /// Adds to the registry and attaches it to all active RPC sessions. + /// + /// The service instance containing JSON-RPC callable methods to expose. + public void AddService(object service) + { + lock (this.sync) + { + this.targets.Add(service); + foreach (var rpc in this.activeRpcs) + { + rpc.AddLocalRpcTarget(service); + } + } + } + + /// + /// Registers a new standalone JSON-RPC method. + /// + /// The name of the method to add. + /// The handler to add. + public void AddMethod(string name, Delegate handler) + { + lock (this.sync) + { + this.methods.Add((name, handler)); + foreach (var rpc in this.activeRpcs) + { + rpc.AddLocalRpcMethod(name, handler); + } + } + } + + /// + /// Attaches a JsonRpc instance to the registry so it receives all existing service targets. + /// + /// The JsonRpc instance to attach and populate with current targets. + internal void Attach(JsonRpc rpc) + { + lock (this.sync) + { + this.activeRpcs.Add(rpc); + foreach (var t in this.targets) + { + rpc.AddLocalRpcTarget(t); + } + + foreach (var m in this.methods) + { + rpc.AddLocalRpcMethod(m.Name, m.Handler); + } + } + } + + /// + /// Detaches a JsonRpc instance from the registry (e.g. when a client disconnects). + /// + /// The JsonRpc instance being detached. + internal void Detach(JsonRpc rpc) + { + lock (this.sync) + { + this.activeRpcs.Remove(rpc); + } + } +} diff --git a/Dalamud/Plugin/Services/IPluginLinkHandler.cs b/Dalamud/Plugin/Services/IPluginLinkHandler.cs new file mode 100644 index 000000000..57f772768 --- /dev/null +++ b/Dalamud/Plugin/Services/IPluginLinkHandler.cs @@ -0,0 +1,20 @@ +using Dalamud.Networking.Pipes; + +namespace Dalamud.Plugin.Services; + +/// +/// A service to allow plugins to subscribe to dalamud:// URIs targeting them. Plugins will receive any URI sent to the +/// dalamud://plugin/{PLUGIN_INTERNAL_NAME}/... namespace. +/// +public interface IPluginLinkHandler +{ + /// + /// A delegate containing the received URI. + /// + delegate void PluginUriReceived(DalamudUri uri); + + /// + /// The event fired when a URI targeting this plugin is received. + /// + event PluginUriReceived OnUriReceived; +} diff --git a/Directory.Packages.props b/Directory.Packages.props index 91875e63e..903a8ee88 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -3,11 +3,13 @@ true false + + @@ -22,26 +24,35 @@ + + + + + + + + + @@ -54,4 +65,4 @@ - \ No newline at end of file + From 4937a2f4bd2e551669e7d158b44d0f6e681ffc1d Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Sun, 16 Nov 2025 18:14:02 -0800 Subject: [PATCH 21/27] CR changes --- .../Networking/Pipes/Api/PluginLinkHandler.cs | 4 ++- .../Pipes/Internal/LinkHandlerService.cs | 36 ++++--------------- Dalamud/Networking/Pipes/Rpc/PipeRpcHost.cs | 2 +- Dalamud/Plugin/Services/IPluginLinkHandler.cs | 5 ++- 4 files changed, 15 insertions(+), 32 deletions(-) diff --git a/Dalamud/Networking/Pipes/Api/PluginLinkHandler.cs b/Dalamud/Networking/Pipes/Api/PluginLinkHandler.cs index 2c99901b4..d8f43907c 100644 --- a/Dalamud/Networking/Pipes/Api/PluginLinkHandler.cs +++ b/Dalamud/Networking/Pipes/Api/PluginLinkHandler.cs @@ -1,5 +1,6 @@ using System.Linq; +using Dalamud.Console; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Networking.Pipes.Internal; @@ -43,7 +44,8 @@ public class PluginLinkHandler : IInternalDisposableService, IPluginLinkHandler private void HandleUri(DalamudUri uri) { var target = uri.Path.Split("/").FirstOrDefault(); - if (target == null || !string.Equals(target, this.localPlugin.InternalName, StringComparison.OrdinalIgnoreCase)) + var thisPlugin = ConsoleManagerPluginUtil.GetSanitizedNamespaceName(this.localPlugin.InternalName); + if (target == null || !string.Equals(target, thisPlugin, StringComparison.OrdinalIgnoreCase)) { return; } diff --git a/Dalamud/Networking/Pipes/Internal/LinkHandlerService.cs b/Dalamud/Networking/Pipes/Internal/LinkHandlerService.cs index 79bb1e017..3cc4af9f4 100644 --- a/Dalamud/Networking/Pipes/Internal/LinkHandlerService.cs +++ b/Dalamud/Networking/Pipes/Internal/LinkHandlerService.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using Dalamud.Logging.Internal; using Dalamud.Networking.Pipes.Rpc; +using Dalamud.Utility; namespace Dalamud.Networking.Pipes.Internal; @@ -65,10 +66,7 @@ internal class LinkHandlerService : IInternalDisposableService if (!this.handlers.TryGetValue(ns, out var list)) return; - lock (list) - { - list.RemoveAll(x => x == handler); - } + list.RemoveAll(x => x == handler); if (list.Count == 0) this.handlers.TryRemove(ns, out _); @@ -85,25 +83,12 @@ internal class LinkHandlerService : IInternalDisposableService this.log.Information("Received URI: {Uri}", uri.ToString()); var ns = uri.Namespace; - if (!this.handlers.TryGetValue(ns, out var list)) + if (!this.handlers.TryGetValue(ns, out var actions)) return; - Action[] snapshot; - lock (list) + foreach (var h in actions) { - snapshot = list.ToArray(); - } - - foreach (var h in snapshot) - { - try - { - h(uri); - } - catch (Exception e) - { - this.log.Warning(e, "Link handler threw for {UriPath}", uri.Path); - } + h.InvokeSafely(uri); } } @@ -116,14 +101,7 @@ internal class LinkHandlerService : IInternalDisposableService if (string.IsNullOrWhiteSpace(uri)) return; - try - { - var du = DalamudUri.FromUri(uri); - this.Dispatch(du); - } - catch (Exception) - { - // swallow parse errors; clients shouldn't crash the host - } + var du = DalamudUri.FromUri(uri); + this.Dispatch(du); } } diff --git a/Dalamud/Networking/Pipes/Rpc/PipeRpcHost.cs b/Dalamud/Networking/Pipes/Rpc/PipeRpcHost.cs index 07dc9d96a..ad1cc72cd 100644 --- a/Dalamud/Networking/Pipes/Rpc/PipeRpcHost.cs +++ b/Dalamud/Networking/Pipes/Rpc/PipeRpcHost.cs @@ -53,7 +53,7 @@ internal class PipeRpcHost : IDisposable public void Start() { if (this.acceptLoopTask != null) return; - this.acceptLoopTask = Task.Run(this.AcceptLoopAsync); + this.acceptLoopTask = Task.Factory.StartNew(this.AcceptLoopAsync, TaskCreationOptions.LongRunning); } /// Invoke an RPC request on a specific client expecting a result. diff --git a/Dalamud/Plugin/Services/IPluginLinkHandler.cs b/Dalamud/Plugin/Services/IPluginLinkHandler.cs index 57f772768..22139814d 100644 --- a/Dalamud/Plugin/Services/IPluginLinkHandler.cs +++ b/Dalamud/Plugin/Services/IPluginLinkHandler.cs @@ -1,4 +1,6 @@ -using Dalamud.Networking.Pipes; +using System.Diagnostics.CodeAnalysis; + +using Dalamud.Networking.Pipes; namespace Dalamud.Plugin.Services; @@ -6,6 +8,7 @@ namespace Dalamud.Plugin.Services; /// A service to allow plugins to subscribe to dalamud:// URIs targeting them. Plugins will receive any URI sent to the /// dalamud://plugin/{PLUGIN_INTERNAL_NAME}/... namespace. /// +[Experimental("DAL_RPC", Message = "This service will be finalized around 7.41 and may change before then.")] public interface IPluginLinkHandler { /// From 19a3926051ce6aa30ac907a5fb7201536b971452 Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Sun, 16 Nov 2025 21:35:33 -0800 Subject: [PATCH 22/27] Better hello message --- .../Networking/Pipes/Api/PluginLinkHandler.cs | 1 + .../Pipes/Internal/ClientHelloService.cs | 45 +++++++++++++++---- 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/Dalamud/Networking/Pipes/Api/PluginLinkHandler.cs b/Dalamud/Networking/Pipes/Api/PluginLinkHandler.cs index d8f43907c..78fbb0d82 100644 --- a/Dalamud/Networking/Pipes/Api/PluginLinkHandler.cs +++ b/Dalamud/Networking/Pipes/Api/PluginLinkHandler.cs @@ -6,6 +6,7 @@ using Dalamud.IoC.Internal; using Dalamud.Networking.Pipes.Internal; using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Services; +#pragma warning disable DAL_RPC namespace Dalamud.Networking.Pipes.Api; diff --git a/Dalamud/Networking/Pipes/Internal/ClientHelloService.cs b/Dalamud/Networking/Pipes/Internal/ClientHelloService.cs index cc06560bd..9c182561e 100644 --- a/Dalamud/Networking/Pipes/Internal/ClientHelloService.cs +++ b/Dalamud/Networking/Pipes/Internal/ClientHelloService.cs @@ -1,10 +1,13 @@ using System.Threading.Tasks; +using Dalamud.Data; using Dalamud.Game; using Dalamud.Game.ClientState; using Dalamud.Networking.Pipes.Rpc; using Dalamud.Utility; +using Lumina.Excel.Sheets; + namespace Dalamud.Networking.Pipes.Internal; /// @@ -30,25 +33,49 @@ internal sealed class ClientHelloService : IInternalDisposableService /// Respond with information. public async Task HandleHello(ClientHelloRequest request) { - var framework = await Service.GetAsync(); var dalamud = await Service.GetAsync(); - var clientState = await Service.GetAsync(); - var response = await framework.RunOnFrameworkThread(() => new ClientHelloResponse + return new ClientHelloResponse { ApiVersion = "1.0", DalamudVersion = Util.GetScmVersion(), GameVersion = dalamud.StartInfo.GameVersion?.ToString() ?? "Unknown", - PlayerName = clientState.IsLoggedIn ? clientState.LocalPlayer?.Name.ToString() ?? "Unknown" : null, - }); - - return response; + ClientIdentifier = await this.GetClientIdentifier(), + }; } /// public void DisposeService() { } + + private async Task GetClientIdentifier() + { + var framework = await Service.GetAsync(); + var clientState = await Service.GetAsync(); + var dataManager = await Service.GetAsync(); + + var clientIdentifier = $"FFXIV Process ${Environment.ProcessId}"; + + await framework.RunOnFrameworkThread(() => + { + if (clientState.IsLoggedIn) + { + var player = clientState.LocalPlayer; + if (player != null) + { + var world = dataManager.GetExcelSheet().GetRow(player.HomeWorld.RowId); + clientIdentifier = $"Logged in as {player.Name.TextValue} @ {world.Name.ExtractText()}"; + } + } + else + { + clientIdentifier = "On login screen"; + } + }); + + return clientIdentifier; + } } /// @@ -88,7 +115,7 @@ internal record ClientHelloResponse public string? GameVersion { get; init; } /// - /// Gets or sets the player name, or null if the player isn't logged in. + /// Gets an identifier for this client. /// - public string? PlayerName { get; set; } + public string? ClientIdentifier { get; init; } } From cc9191657453986cb6451f63f3238b3b29b7e26c Mon Sep 17 00:00:00 2001 From: goaaats Date: Tue, 18 Nov 2025 00:52:30 +0100 Subject: [PATCH 23/27] Fix bad merge --- Dalamud/Game/ClientState/Buddy/BuddyList.cs | 6 ++---- Dalamud/Game/ClientState/Party/PartyList.cs | 4 ++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/Dalamud/Game/ClientState/Buddy/BuddyList.cs b/Dalamud/Game/ClientState/Buddy/BuddyList.cs index a76b520af..4d5fc2aab 100644 --- a/Dalamud/Game/ClientState/Buddy/BuddyList.cs +++ b/Dalamud/Game/ClientState/Buddy/BuddyList.cs @@ -9,8 +9,6 @@ using Dalamud.Plugin.Services; using CSBuddy = FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy; using CSUIState = FFXIVClientStructs.FFXIV.Client.Game.UI.UIState; - -using CSBuddy = FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy; using CSBuddyMember = FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy.BuddyMember; namespace Dalamud.Game.ClientState.Buddy; @@ -74,7 +72,7 @@ internal sealed partial class BuddyList : IServiceType, IBuddyList } } - private unsafe CSBuddy* BuddyListStruct => &UIState.Instance()->Buddy; + private unsafe CSBuddy* BuddyListStruct => &CSUIState.Instance()->Buddy; /// public IBuddyMember? this[int index] @@ -113,7 +111,7 @@ internal sealed partial class BuddyList : IServiceType, IBuddyList if (address == 0) return null; - if (this.clientState.LocalContentId == 0) + if (this.playerState.ContentId == 0) return null; var buddy = new BuddyMember((CSBuddyMember*)address); diff --git a/Dalamud/Game/ClientState/Party/PartyList.cs b/Dalamud/Game/ClientState/Party/PartyList.cs index 1a5177b10..1dede1dd3 100644 --- a/Dalamud/Game/ClientState/Party/PartyList.cs +++ b/Dalamud/Game/ClientState/Party/PartyList.cs @@ -93,7 +93,7 @@ internal sealed unsafe partial class PartyList : IServiceType, IPartyList /// public IPartyMember? CreatePartyMemberReference(nint address) { - if (this.clientState.LocalContentId == 0) + if (this.playerState.ContentId == 0) return null; if (address == 0) @@ -114,7 +114,7 @@ internal sealed unsafe partial class PartyList : IServiceType, IPartyList /// public IPartyMember? CreateAllianceMemberReference(nint address) { - if (this.clientState.LocalContentId == 0) + if (this.playerState.ContentId == 0) return null; if (address == 0) From 6a69a6e197ac6761d6c3d39fe3a879c852151cf6 Mon Sep 17 00:00:00 2001 From: goaaats Date: Tue, 18 Nov 2025 00:58:08 +0100 Subject: [PATCH 24/27] Fix some warnings --- Dalamud/Game/ClientState/Buddy/BuddyList.cs | 2 +- Dalamud/GlobalSuppressions.cs | 1 + Dalamud/Networking/Pipes/DalamudUri.cs | 34 +++++++++---------- Dalamud/Plugin/Services/IPluginLinkHandler.cs | 5 +-- 4 files changed, 22 insertions(+), 20 deletions(-) diff --git a/Dalamud/Game/ClientState/Buddy/BuddyList.cs b/Dalamud/Game/ClientState/Buddy/BuddyList.cs index 4d5fc2aab..b8e4c0fcc 100644 --- a/Dalamud/Game/ClientState/Buddy/BuddyList.cs +++ b/Dalamud/Game/ClientState/Buddy/BuddyList.cs @@ -8,8 +8,8 @@ using Dalamud.IoC.Internal; using Dalamud.Plugin.Services; using CSBuddy = FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy; -using CSUIState = FFXIVClientStructs.FFXIV.Client.Game.UI.UIState; using CSBuddyMember = FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy.BuddyMember; +using CSUIState = FFXIVClientStructs.FFXIV.Client.Game.UI.UIState; namespace Dalamud.Game.ClientState.Buddy; diff --git a/Dalamud/GlobalSuppressions.cs b/Dalamud/GlobalSuppressions.cs index 8a9d31b12..35754eb04 100644 --- a/Dalamud/GlobalSuppressions.cs +++ b/Dalamud/GlobalSuppressions.cs @@ -21,6 +21,7 @@ using System.Diagnostics.CodeAnalysis; [assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1116:SplitParametersMustStartOnLineAfterDeclaration", Justification = "Reviewed.")] [assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:FileMayOnlyContainASingleType", Justification = "This would be nice, but a big refactor")] [assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649:FileNameMustMatchTypeName", Justification = "I don't like this one so much")] +[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1108:BlockStatementsMustNotContainEmbeddedComments", Justification = "I like having comments in blocks")] // ImRAII stuff [assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:ElementsMustBeDocumented", Justification = "Reviewed.", Scope = "namespaceanddescendants", Target = "Dalamud.Interface.Utility.Raii")] diff --git a/Dalamud/Networking/Pipes/DalamudUri.cs b/Dalamud/Networking/Pipes/DalamudUri.cs index 03ad15af1..7e639cbbe 100644 --- a/Dalamud/Networking/Pipes/DalamudUri.cs +++ b/Dalamud/Networking/Pipes/DalamudUri.cs @@ -61,6 +61,23 @@ public record DalamudUri /// public override string ToString() => this.rawUri.ToString(); + /// + /// Build a DalamudURI from a given URI. + /// + /// The URI to convert to a Dalamud URI. + /// Returns a DalamudUri. + public static DalamudUri FromUri(Uri uri) + { + return new DalamudUri(uri); + } + + /// + /// Build a DalamudURI from a URI in string format. + /// + /// The URI to convert to a Dalamud URI. + /// Returns a DalamudUri. + public static DalamudUri FromUri(string uri) => FromUri(new Uri(uri)); + private string[] GetDataSegments() { // reimplementation of the System.URI#Segments, under MIT license. @@ -82,21 +99,4 @@ public record DalamudUri return segments.ToArray(); } - - /// - /// Build a DalamudURI from a given URI. - /// - /// The URI to convert to a Dalamud URI. - /// Returns a DalamudUri. - public static DalamudUri FromUri(Uri uri) - { - return new DalamudUri(uri); - } - - /// - /// Build a DalamudURI from a URI in string format. - /// - /// The URI to convert to a Dalamud URI. - /// Returns a DalamudUri. - public static DalamudUri FromUri(string uri) => FromUri(new Uri(uri)); } diff --git a/Dalamud/Plugin/Services/IPluginLinkHandler.cs b/Dalamud/Plugin/Services/IPluginLinkHandler.cs index 22139814d..5d2d32728 100644 --- a/Dalamud/Plugin/Services/IPluginLinkHandler.cs +++ b/Dalamud/Plugin/Services/IPluginLinkHandler.cs @@ -6,7 +6,7 @@ namespace Dalamud.Plugin.Services; /// /// A service to allow plugins to subscribe to dalamud:// URIs targeting them. Plugins will receive any URI sent to the -/// dalamud://plugin/{PLUGIN_INTERNAL_NAME}/... namespace. +/// dalamud://plugin/{PLUGIN_INTERNAL_NAME}/... namespace. /// [Experimental("DAL_RPC", Message = "This service will be finalized around 7.41 and may change before then.")] public interface IPluginLinkHandler @@ -14,7 +14,8 @@ public interface IPluginLinkHandler /// /// A delegate containing the received URI. /// - delegate void PluginUriReceived(DalamudUri uri); + /// The URI opened by the user. + public delegate void PluginUriReceived(DalamudUri uri); /// /// The event fired when a URI targeting this plugin is received. From 8ab7b59ae47f5deaa59dc1001e92855c3b6e2a89 Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Tue, 25 Nov 2025 10:17:12 -0800 Subject: [PATCH 25/27] fix: Missing service types causing injection failures --- Dalamud/Plugin/SelfTest/ISelfTestRegistry.cs | 4 +++- Dalamud/Plugin/Services/IPluginLinkHandler.cs | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Dalamud/Plugin/SelfTest/ISelfTestRegistry.cs b/Dalamud/Plugin/SelfTest/ISelfTestRegistry.cs index af3b583c9..7e9faf3f9 100644 --- a/Dalamud/Plugin/SelfTest/ISelfTestRegistry.cs +++ b/Dalamud/Plugin/SelfTest/ISelfTestRegistry.cs @@ -1,5 +1,7 @@ using System.Collections.Generic; +using Dalamud.Plugin.Services; + namespace Dalamud.Plugin.SelfTest; /// @@ -44,7 +46,7 @@ namespace Dalamud.Plugin.SelfTest; /// } /// /// -public interface ISelfTestRegistry +public interface ISelfTestRegistry : IDalamudService { /// /// Registers the self-test steps for this plugin. diff --git a/Dalamud/Plugin/Services/IPluginLinkHandler.cs b/Dalamud/Plugin/Services/IPluginLinkHandler.cs index 5d2d32728..c05757ac7 100644 --- a/Dalamud/Plugin/Services/IPluginLinkHandler.cs +++ b/Dalamud/Plugin/Services/IPluginLinkHandler.cs @@ -9,7 +9,7 @@ namespace Dalamud.Plugin.Services; /// dalamud://plugin/{PLUGIN_INTERNAL_NAME}/... namespace. /// [Experimental("DAL_RPC", Message = "This service will be finalized around 7.41 and may change before then.")] -public interface IPluginLinkHandler +public interface IPluginLinkHandler : IDalamudService { /// /// A delegate containing the received URI. From 518b3a4fb351eb1a826d3712f1c7fc4b712d658f Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Wed, 3 Dec 2025 16:43:12 +0100 Subject: [PATCH 26/27] Fix NounProcessor BeastTribe column offset --- Dalamud/Game/Text/Noun/NounParams.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dalamud/Game/Text/Noun/NounParams.cs b/Dalamud/Game/Text/Noun/NounParams.cs index 3d5c424be..ab7a732d2 100644 --- a/Dalamud/Game/Text/Noun/NounParams.cs +++ b/Dalamud/Game/Text/Noun/NounParams.cs @@ -60,8 +60,8 @@ internal record struct NounParams() /// public readonly int ColumnOffset => this.SheetName switch { - // See "E8 ?? ?? ?? ?? 44 8B 6B 08" - nameof(LSheets.BeastTribe) => 10, + // See "E8 ?? ?? ?? ?? 44 8B 66 ?? 8B E8" + nameof(LSheets.BeastTribe) => 11, nameof(LSheets.DeepDungeonItem) => 1, nameof(LSheets.DeepDungeonEquipment) => 1, nameof(LSheets.DeepDungeonMagicStone) => 1, From f198ce46dc1d2100d95d4c090e446077e2af5729 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Wed, 3 Dec 2025 16:47:13 +0100 Subject: [PATCH 27/27] Add self tests for ColumnOffset --- .../Steps/NounProcessorSelfTestStep.cs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/NounProcessorSelfTestStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/NounProcessorSelfTestStep.cs index ccb23d395..ccccc691c 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/NounProcessorSelfTestStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/NounProcessorSelfTestStep.cs @@ -191,6 +191,29 @@ internal class NounProcessorSelfTestStep : ISelfTestStep new(nameof(LSheets.Item), 44348, ClientLanguage.French, 2, (int)FrenchArticleType.PossessiveFirstPerson, 1, "mes mémoquartz inhabituels fantasmagoriques"), new(nameof(LSheets.Item), 44348, ClientLanguage.French, 2, (int)FrenchArticleType.PossessiveSecondPerson, 1, "tes mémoquartz inhabituels fantasmagoriques"), new(nameof(LSheets.Item), 44348, ClientLanguage.French, 2, (int)FrenchArticleType.PossessiveThirdPerson, 1, "ses mémoquartz inhabituels fantasmagoriques"), + + // ColumnOffset tests + + new(nameof(LSheets.BeastTribe), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Indefinite, 1, "a Amalj'aa"), + new(nameof(LSheets.BeastTribe), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Definite, 1, "the Amalj'aa"), + + new(nameof(LSheets.DeepDungeonEquipment), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Indefinite, 1, "an aetherpool arm"), + new(nameof(LSheets.DeepDungeonEquipment), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Definite, 1, "the aetherpool arm"), + + new(nameof(LSheets.DeepDungeonItem), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Indefinite, 1, "a pomander of safety"), + new(nameof(LSheets.DeepDungeonItem), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Definite, 1, "the pomander of safety"), + + new(nameof(LSheets.DeepDungeonMagicStone), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Indefinite, 1, "a splinter of Inferno magicite"), + new(nameof(LSheets.DeepDungeonMagicStone), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Definite, 1, "the splinter of Inferno magicite"), + + new(nameof(LSheets.DeepDungeonDemiclone), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Indefinite, 1, "an Unei demiclone"), + new(nameof(LSheets.DeepDungeonDemiclone), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Definite, 1, "the Unei demiclone"), + + new(nameof(LSheets.Glasses), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Indefinite, 1, "a pair of oval spectacles"), + new(nameof(LSheets.Glasses), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Definite, 1, "the pair of oval spectacles"), + + new(nameof(LSheets.GlassesStyle), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Indefinite, 1, "a shaded spectacles"), + new(nameof(LSheets.GlassesStyle), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Definite, 1, "the shaded spectacles"), ]; private enum GermanCases