From 4e87b4b0076460195f3be5e2fe76630e41ebec2e Mon Sep 17 00:00:00 2001 From: goaaats Date: Wed, 12 Nov 2025 20:15:12 +0100 Subject: [PATCH 01/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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 d7915c7020f04f15eaa4caa2c341b2356c51a08f Mon Sep 17 00:00:00 2001 From: goaaats Date: Fri, 28 Nov 2025 18:11:31 +0100 Subject: [PATCH 26/29] Show a sensible error message when Lumina fails to init --- Dalamud/Data/DataManager.cs | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/Dalamud/Data/DataManager.cs b/Dalamud/Data/DataManager.cs index d017bf85a..ed0aa6c4d 100644 --- a/Dalamud/Data/DataManager.cs +++ b/Dalamud/Data/DataManager.cs @@ -41,7 +41,7 @@ internal sealed class DataManager : IInternalDisposableService, IDataManager try { Log.Verbose("Starting data load..."); - + using (Timings.Start("Lumina Init")) { var luminaOptions = new LuminaOptions @@ -53,12 +53,25 @@ internal sealed class DataManager : IInternalDisposableService, IDataManager DefaultExcelLanguage = this.Language.ToLumina(), }; - this.GameData = new( - Path.Combine(Path.GetDirectoryName(Environment.ProcessPath)!, "sqpack"), - luminaOptions) + try { - StreamPool = new(), - }; + this.GameData = new( + Path.Combine(Path.GetDirectoryName(Environment.ProcessPath)!, "sqpack"), + luminaOptions) + { + StreamPool = new(), + }; + } + catch (Exception ex) + { + Log.Error(ex, "Lumina GameData init failed"); + Util.Fatal( + "Dalamud could not read required game data files. This likely means your game installation is corrupted or incomplete.\n\n" + + "Please repair your installation by right-clicking the login button in XIVLauncher and choosing \"Repair game files\".", + "Dalamud"); + + return; + } Log.Information("Lumina is ready: {0}", this.GameData.DataPath); @@ -71,7 +84,7 @@ internal sealed class DataManager : IInternalDisposableService, IDataManager dalamud.StartInfo.TroubleshootingPackData); this.HasModifiedGameDataFiles = tsInfo?.IndexIntegrity is LauncherTroubleshootingInfo.IndexIntegrityResult.Failed or LauncherTroubleshootingInfo.IndexIntegrityResult.Exception; - + if (this.HasModifiedGameDataFiles) Log.Verbose("Game data integrity check failed!\n{TsData}", dalamud.StartInfo.TroubleshootingPackData); } @@ -130,7 +143,7 @@ internal sealed class DataManager : IInternalDisposableService, IDataManager #region Lumina Wrappers /// - public ExcelSheet GetExcelSheet(ClientLanguage? language = null, string? name = null) where T : struct, IExcelRow + public ExcelSheet GetExcelSheet(ClientLanguage? language = null, string? name = null) where T : struct, IExcelRow => this.Excel.GetSheet(language?.ToLumina(), name); /// @@ -138,7 +151,7 @@ internal sealed class DataManager : IInternalDisposableService, IDataManager => this.Excel.GetSubrowSheet(language?.ToLumina(), name); /// - public FileResource? GetFile(string path) + public FileResource? GetFile(string path) => this.GetFile(path); /// @@ -161,7 +174,7 @@ internal sealed class DataManager : IInternalDisposableService, IDataManager : Task.FromException(new FileNotFoundException("The file could not be found.")); /// - public bool FileExists(string path) + public bool FileExists(string path) => this.GameData.FileExists(path); #endregion From 78ecb721cd3f48fd06d72b1d850a47ff68f39d1f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 29 Nov 2025 12:47:45 +0000 Subject: [PATCH 27/29] Update ClientStructs --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index 0769d1f18..e5f586630 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 0769d1f180f859688f47a7a99610e9ce10da946c +Subproject commit e5f586630ef06fa48d5dc0d8c0fa679323093c77 From d12a9ec7da4c2e91c22c0d3f848197eb743ba03d Mon Sep 17 00:00:00 2001 From: goaaats Date: Sat, 29 Nov 2025 19:15:37 +0100 Subject: [PATCH 28/29] Remove DalamudBetaKey, DalamudBetaKind from config Fix all code that depends on it to use Util.GetActiveTrack() instead --- .../Internal/DalamudConfiguration.cs | 10 ---------- Dalamud/Interface/Internal/DalamudInterface.cs | 2 +- .../Internal/Windows/BranchSwitcherWindow.cs | 18 ++++++++---------- .../PluginInstaller/PluginInstallerWindow.cs | 9 ++++----- Dalamud/Support/DalamudReleases.cs | 15 ++++++++++----- Dalamud/Support/Troubleshooting.cs | 2 +- 6 files changed, 24 insertions(+), 32 deletions(-) diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index da0d7c2c6..9404b5b10 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -108,11 +108,6 @@ internal sealed class DalamudConfiguration : IInternalDisposableService /// public bool DoPluginTest { get; set; } = false; - /// - /// Gets or sets a key to opt into Dalamud staging builds. - /// - public string? DalamudBetaKey { get; set; } = null; - /// /// Gets or sets a list of custom repos. /// @@ -278,11 +273,6 @@ internal sealed class DalamudConfiguration : IInternalDisposableService /// public bool IsResumeGameAfterPluginLoad { get; set; } = false; - /// - /// Gets or sets the kind of beta to download when matches the server value. - /// - public string? DalamudBetaKind { get; set; } - /// /// Gets or sets a value indicating whether any plugin should be loaded when the game is started. /// It is reset immediately when read. diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index 202334580..64e1acaa4 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -182,7 +182,7 @@ internal class DalamudInterface : IInternalDisposableService () => Service.GetNullable()?.ToggleDevMenu(), VirtualKey.SHIFT); - if (!configuration.DalamudBetaKind.IsNullOrEmpty()) + if (Util.GetActiveTrack() != "release") { titleScreenMenu.AddEntryCore( Loc.Localize("TSMDalamudDevMenu", "Developer Menu"), diff --git a/Dalamud/Interface/Internal/Windows/BranchSwitcherWindow.cs b/Dalamud/Interface/Internal/Windows/BranchSwitcherWindow.cs index 4e95b718e..9cc14ea14 100644 --- a/Dalamud/Interface/Internal/Windows/BranchSwitcherWindow.cs +++ b/Dalamud/Interface/Internal/Windows/BranchSwitcherWindow.cs @@ -6,7 +6,6 @@ using System.Net.Http.Json; using System.Threading.Tasks; using Dalamud.Bindings.ImGui; -using Dalamud.Configuration.Internal; using Dalamud.Interface.Colors; using Dalamud.Interface.Utility; using Dalamud.Interface.Windowing; @@ -15,6 +14,8 @@ using Dalamud.Utility; using Newtonsoft.Json; +using Serilog; + namespace Dalamud.Interface.Internal.Windows; /// @@ -47,7 +48,7 @@ public class BranchSwitcherWindow : Window Debug.Assert(this.branches != null, "this.branches != null"); var trackName = Util.GetActiveTrack(); - this.selectedBranchIndex = this.branches.IndexOf(x => x.Value.Track != trackName); + this.selectedBranchIndex = this.branches.IndexOf(x => x.Value.Track == trackName); if (this.selectedBranchIndex == -1) { this.selectedBranchIndex = 0; @@ -86,12 +87,9 @@ public class BranchSwitcherWindow : Window if (ImGui.Button("Pick & Restart"u8)) { - var config = Service.Get(); - config.DalamudBetaKind = pickedBranch.Key; - config.DalamudBetaKey = pickedBranch.Value.Key; - - // If we exit immediately, we need to write out the new config now - config.ForceSave(); + var newTrackName = pickedBranch.Key; + var newTrackKey = pickedBranch.Value.Key; + Log.Verbose("Switching to branch {Branch} with key {Key}", newTrackName, newTrackKey); var appData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); var xlPath = Path.Combine(appData, "XIVLauncher", "current", "XIVLauncher.exe"); @@ -104,8 +102,8 @@ public class BranchSwitcherWindow : Window UseShellExecute = false, }; - ps.ArgumentList.Add($"--dalamud-beta-kind={config.DalamudBetaKind}"); - ps.ArgumentList.Add($"--dalamud-beta-key={(config.DalamudBetaKey.IsNullOrEmpty() ? "invalid" : config.DalamudBetaKey)}"); + ps.ArgumentList.Add($"--dalamud-beta-kind={newTrackName}"); + ps.ArgumentList.Add($"--dalamud-beta-key={(newTrackKey.IsNullOrEmpty() ? "invalid" : newTrackKey)}"); Process.Start(ps); Environment.Exit(0); diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index b203b3894..ac092bd25 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -302,8 +302,7 @@ internal class PluginInstallerWindow : Window, IDisposable this.profileManagerWidget.Reset(); - var config = Service.Get(); - if (this.staleDalamudNewVersion == null && !config.DalamudBetaKind.IsNullOrEmpty()) + if (this.staleDalamudNewVersion == null && !Util.GetActiveTrack().IsNullOrEmpty()) { Service.Get().GetVersionForCurrentTrack().ContinueWith(t => { @@ -311,10 +310,10 @@ internal class PluginInstallerWindow : Window, IDisposable return; var versionInfo = t.Result; - if (versionInfo.AssemblyVersion != Util.GetScmVersion() && - versionInfo.Track != "release" && - string.Equals(versionInfo.Key, config.DalamudBetaKey, StringComparison.OrdinalIgnoreCase)) + if (versionInfo.AssemblyVersion != Util.GetScmVersion()) + { this.staleDalamudNewVersion = versionInfo.AssemblyVersion; + } }); } } diff --git a/Dalamud/Support/DalamudReleases.cs b/Dalamud/Support/DalamudReleases.cs index 15e851da2..603c77487 100644 --- a/Dalamud/Support/DalamudReleases.cs +++ b/Dalamud/Support/DalamudReleases.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using Dalamud.Configuration.Internal; using Dalamud.Networking.Http; +using Dalamud.Utility; using Newtonsoft.Json; @@ -15,7 +16,7 @@ namespace Dalamud.Support; internal class DalamudReleases : IServiceType { private const string VersionInfoUrl = "https://kamori.goats.dev/Dalamud/Release/VersionInfo?track={0}"; - + private readonly HappyHttpClient httpClient; private readonly DalamudConfiguration config; @@ -30,20 +31,24 @@ internal class DalamudReleases : IServiceType this.httpClient = httpClient; this.config = config; } - + /// /// Get the latest version info for the current track. /// /// The version info for the current track. - public async Task GetVersionForCurrentTrack() + public async Task GetVersionForCurrentTrack() { - var url = string.Format(VersionInfoUrl, [this.config.DalamudBetaKind]); + var currentTrack = Util.GetActiveTrack(); + if (currentTrack.IsNullOrEmpty()) + return null; + + var url = string.Format(VersionInfoUrl, [currentTrack]); var response = await this.httpClient.SharedHttpClient.GetAsync(url); response.EnsureSuccessStatusCode(); var content = await response.Content.ReadAsStringAsync(); return JsonConvert.DeserializeObject(content); } - + [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "laziness")] public class DalamudVersionInfo { diff --git a/Dalamud/Support/Troubleshooting.cs b/Dalamud/Support/Troubleshooting.cs index 4af8d5ffc..88048c462 100644 --- a/Dalamud/Support/Troubleshooting.cs +++ b/Dalamud/Support/Troubleshooting.cs @@ -73,7 +73,7 @@ public static class Troubleshooting DalamudGitHash = Util.GetGitHash() ?? "Unknown", GameVersion = startInfo.GameVersion?.ToString() ?? "Unknown", Language = startInfo.Language.ToString(), - BetaKey = configuration.DalamudBetaKey, + BetaKey = Util.GetActiveTrack(), DoPluginTest = configuration.DoPluginTest, LoadAllApiLevels = pluginManager?.LoadAllApiLevels == true, InterfaceLoaded = interfaceManager?.IsReady ?? false, From 7510c032cc0e706787259b44d4ab2415cc74c8a3 Mon Sep 17 00:00:00 2001 From: goaaats Date: Sat, 29 Nov 2025 19:22:28 +0100 Subject: [PATCH 29/29] Disable Intel CET support, causes CLR crashes on unpatched Windows --- Directory.Build.props | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Directory.Build.props b/Directory.Build.props index 4ed87c809..f9f061c17 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -6,6 +6,10 @@ x64 x64 13.0 + + + + false