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)
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/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.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.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}
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/Dalamud.csproj b/Dalamud/Dalamud.csproj
index ce140b8c9..b9b453f89 100644
--- a/Dalamud/Dalamud.csproj
+++ b/Dalamud/Dalamud.csproj
@@ -81,6 +81,7 @@
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
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
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);
diff --git a/Dalamud/Game/ClientState/Aetherytes/AetheryteList.cs b/Dalamud/Game/ClientState/Aetherytes/AetheryteList.cs
index 4a6d011e9..a24302947 100644
--- a/Dalamud/Game/ClientState/Aetherytes/AetheryteList.cs
+++ b/Dalamud/Game/ClientState/Aetherytes/AetheryteList.cs
@@ -88,10 +88,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);
}
///
@@ -99,4 +96,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()
+ {
+ }
+ }
}
diff --git a/Dalamud/Game/ClientState/Buddy/BuddyList.cs b/Dalamud/Game/ClientState/Buddy/BuddyList.cs
index dbac76518..b8e4c0fcc 100644
--- a/Dalamud/Game/ClientState/Buddy/BuddyList.cs
+++ b/Dalamud/Game/ClientState/Buddy/BuddyList.cs
@@ -8,6 +8,7 @@ using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services;
using CSBuddy = FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy;
+using CSBuddyMember = FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy.BuddyMember;
using CSUIState = FFXIVClientStructs.FFXIV.Client.Game.UI.UIState;
namespace Dalamud.Game.ClientState.Buddy;
@@ -23,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 PlayerState playerState = Service.Get();
@@ -84,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 == IntPtr.Zero)
+ if (address == 0)
return null;
- if (!this.playerState.IsLoaded)
+ if (this.playerState.ContentId == 0)
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;
@@ -132,12 +133,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()
+ {
+ }
+ }
}
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();
+ }
}
diff --git a/Dalamud/Game/ClientState/Fates/Fate.cs b/Dalamud/Game/ClientState/Fates/Fate.cs
index f82109fd0..4348c4025 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;
@@ -7,10 +8,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
{
@@ -112,129 +115,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)
- {
- if (fate == null)
- return false;
-
- var playerState = Service.Get();
- return playerState.IsLoaded == 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 30b0f4102..fa75c7e53 100644
--- a/Dalamud/Game/ClientState/Fates/FateTable.cs
+++ b/Dalamud/Game/ClientState/Fates/FateTable.cs
@@ -6,6 +6,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;
@@ -26,7 +27,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
@@ -69,29 +70,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)
{
- if (offset == IntPtr.Zero)
+ if (address == 0)
return null;
- var playerState = Service.Get();
- if (!playerState.IsLoaded)
+ var clientState = Service.Get();
+ if (clientState.LocalContentId == 0)
return null;
- return new Fate(offset);
+ return new Fate((CSFateContext*)address);
}
}
@@ -106,12 +107,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()
+ {
+ }
+ }
}
diff --git a/Dalamud/Game/ClientState/Objects/ObjectTable.cs b/Dalamud/Game/ClientState/Objects/ObjectTable.cs
index b66dd4775..6bbc43235 100644
--- a/Dalamud/Game/ClientState/Objects/ObjectTable.cs
+++ b/Dalamud/Game/ClientState/Objects/ObjectTable.cs
@@ -13,8 +13,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;
@@ -37,8 +35,6 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable
private readonly CachedEntry[] cachedObjectTable;
- private readonly Enumerator?[] frameworkThreadEnumerators = new Enumerator?[4];
-
[ServiceManager.ServiceConstructor]
private unsafe ObjectTable()
{
@@ -48,9 +44,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);
}
///
@@ -243,30 +236,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!;
@@ -278,7 +255,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)
@@ -295,17 +272,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;
}
}
}
diff --git a/Dalamud/Game/ClientState/Party/PartyList.cs b/Dalamud/Game/ClientState/Party/PartyList.cs
index 9618b679c..1dede1dd3 100644
--- a/Dalamud/Game/ClientState/Party/PartyList.cs
+++ b/Dalamud/Game/ClientState/Party/PartyList.cs
@@ -9,6 +9,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;
@@ -43,20 +44,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]
@@ -81,39 +82,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 (address == IntPtr.Zero || !this.playerState.IsLoaded)
+ if (this.playerState.ContentId == 0)
return null;
- return new PartyMember(address);
+ if (address == 0)
+ return null;
+
+ 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 (address == IntPtr.Zero || !this.playerState.IsLoaded)
+ if (this.playerState.ContentId == 0)
return null;
- return new PartyMember(address);
+ if (address == 0)
+ return null;
+
+ return new PartyMember((CSPartyMember*)address);
}
}
@@ -128,18 +135,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()
+ {
+ }
+ }
}
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();
+ }
}
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);
+ }
}
diff --git a/Dalamud/Game/ClientState/Statuses/StatusList.cs b/Dalamud/Game/ClientState/Statuses/StatusList.cs
index 410ae9d7c..81469ba93 100644
--- a/Dalamud/Game/ClientState/Statuses/StatusList.cs
+++ b/Dalamud/Game/ClientState/Statuses/StatusList.cs
@@ -3,7 +3,7 @@ using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
-using Dalamud.Game.Player;
+using CSStatus = FFXIVClientStructs.FFXIV.Client.Game.Status;
namespace Dalamud.Game.ClientState.Statuses;
@@ -16,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;
}
@@ -26,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.
@@ -49,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
{
@@ -66,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)
{
if (address == IntPtr.Zero)
return null;
@@ -74,8 +74,12 @@ public sealed unsafe partial class StatusList
// 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
// here or somewhere else.
- var playerState = Service.Get();
- if (!playerState.IsLoaded)
+ var clientState = Service.Get();
+
+ if (clientState.LocalContentId == 0)
+ return null;
+
+ if (address == 0)
return null;
return new StatusList(address);
@@ -86,16 +90,15 @@ 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)
{
if (address == IntPtr.Zero)
return null;
- var playerState = Service.Get();
- if (!playerState.IsLoaded)
+ if (address == 0)
return null;
- return new Status(address);
+ return new Status((CSStatus*)address);
}
///
@@ -103,22 +106,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 +133,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 +150,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()
+ {
+ }
+ }
}
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;
-}
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/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/Networking/Pipes/Api/PluginLinkHandler.cs b/Dalamud/Networking/Pipes/Api/PluginLinkHandler.cs
new file mode 100644
index 000000000..78fbb0d82
--- /dev/null
+++ b/Dalamud/Networking/Pipes/Api/PluginLinkHandler.cs
@@ -0,0 +1,56 @@
+using System.Linq;
+
+using Dalamud.Console;
+using Dalamud.IoC;
+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;
+
+///
+[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();
+ var thisPlugin = ConsoleManagerPluginUtil.GetSanitizedNamespaceName(this.localPlugin.InternalName);
+ if (target == null || !string.Equals(target, thisPlugin, 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..7e639cbbe
--- /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();
+
+ ///
+ /// 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.
+ 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();
+ }
+}
diff --git a/Dalamud/Networking/Pipes/Internal/ClientHelloService.cs b/Dalamud/Networking/Pipes/Internal/ClientHelloService.cs
new file mode 100644
index 000000000..9c182561e
--- /dev/null
+++ b/Dalamud/Networking/Pipes/Internal/ClientHelloService.cs
@@ -0,0 +1,121 @@
+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;
+
+///
+/// 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 dalamud = await Service.GetAsync();
+
+ return new ClientHelloResponse
+ {
+ ApiVersion = "1.0",
+ DalamudVersion = Util.GetScmVersion(),
+ GameVersion = dalamud.StartInfo.GameVersion?.ToString() ?? "Unknown",
+ 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;
+ }
+}
+
+///
+/// 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 an identifier for this client.
+ ///
+ public string? ClientIdentifier { get; init; }
+}
diff --git a/Dalamud/Networking/Pipes/Internal/LinkHandlerService.cs b/Dalamud/Networking/Pipes/Internal/LinkHandlerService.cs
new file mode 100644
index 000000000..3cc4af9f4
--- /dev/null
+++ b/Dalamud/Networking/Pipes/Internal/LinkHandlerService.cs
@@ -0,0 +1,107 @@
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+
+using Dalamud.Logging.Internal;
+using Dalamud.Networking.Pipes.Rpc;
+using Dalamud.Utility;
+
+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;
+
+ 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 actions))
+ return;
+
+ foreach (var h in actions)
+ {
+ h.InvokeSafely(uri);
+ }
+ }
+
+ ///
+ /// The RPC-invokable link handler.
+ ///
+ /// A plain-text URI to parse.
+ public void HandleLinkCall(string uri)
+ {
+ if (string.IsNullOrWhiteSpace(uri))
+ return;
+
+ var du = DalamudUri.FromUri(uri);
+ this.Dispatch(du);
+ }
+}
diff --git a/Dalamud/Networking/Pipes/Rpc/PipeRpcHost.cs b/Dalamud/Networking/Pipes/Rpc/PipeRpcHost.cs
new file mode 100644
index 000000000..ad1cc72cd
--- /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.Factory.StartNew(this.AcceptLoopAsync, TaskCreationOptions.LongRunning);
+ }
+
+ /// 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
-
+
+
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
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 @@
+
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