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.Boot/DalamudStartInfo.cpp b/Dalamud.Boot/DalamudStartInfo.cpp index 5be8f97d0..9c8fd9721 100644 --- a/Dalamud.Boot/DalamudStartInfo.cpp +++ b/Dalamud.Boot/DalamudStartInfo.cpp @@ -108,6 +108,11 @@ void from_json(const nlohmann::json& json, DalamudStartInfo& config) { config.LogName = json.value("LogName", config.LogName); config.PluginDirectory = json.value("PluginDirectory", config.PluginDirectory); config.AssetDirectory = json.value("AssetDirectory", config.AssetDirectory); + + if (json.contains("TempDirectory") && !json["TempDirectory"].is_null()) { + config.TempDirectory = json.value("TempDirectory", config.TempDirectory); + } + config.Language = json.value("Language", config.Language); config.Platform = json.value("Platform", config.Platform); config.GameVersion = json.value("GameVersion", config.GameVersion); diff --git a/Dalamud.Boot/DalamudStartInfo.h b/Dalamud.Boot/DalamudStartInfo.h index 0eeaddeed..308dcab7d 100644 --- a/Dalamud.Boot/DalamudStartInfo.h +++ b/Dalamud.Boot/DalamudStartInfo.h @@ -44,6 +44,7 @@ struct DalamudStartInfo { std::string ConfigurationPath; std::string LogPath; std::string LogName; + std::string TempDirectory; std::string PluginDirectory; std::string AssetDirectory; ClientLanguage Language = ClientLanguage::English; diff --git a/Dalamud.Boot/veh.cpp b/Dalamud.Boot/veh.cpp index b0ec1cefa..b75256af8 100644 --- a/Dalamud.Boot/veh.cpp +++ b/Dalamud.Boot/veh.cpp @@ -122,6 +122,7 @@ static DalamudExpected append_injector_launch_args(std::vector(g_startInfo.LogName) + L"\""); args.emplace_back(L"--dalamud-plugin-directory=\"" + unicode::convert(g_startInfo.PluginDirectory) + L"\""); args.emplace_back(L"--dalamud-asset-directory=\"" + unicode::convert(g_startInfo.AssetDirectory) + L"\""); + args.emplace_back(L"--dalamud-temp-directory=\"" + unicode::convert(g_startInfo.TempDirectory) + L"\""); args.emplace_back(std::format(L"--dalamud-client-language={}", static_cast(g_startInfo.Language))); args.emplace_back(std::format(L"--dalamud-delay-initialize={}", g_startInfo.DelayInitializeMs)); // NoLoadPlugins/NoLoadThirdPartyPlugins: supplied from DalamudCrashHandler @@ -268,7 +269,7 @@ LONG WINAPI vectored_exception_handler(EXCEPTION_POINTERS* ex) if (!is_ffxiv_address(L"ffxiv_dx11.exe", ex->ContextRecord->Rip) && !is_ffxiv_address(L"cimgui.dll", ex->ContextRecord->Rip)) - return EXCEPTION_CONTINUE_SEARCH; + return EXCEPTION_CONTINUE_SEARCH; } return exception_handler(ex); @@ -297,7 +298,7 @@ bool veh::add_handler(bool doFullDump, const std::string& workingDirectory) if (HANDLE hReadPipeRaw, hWritePipeRaw; CreatePipe(&hReadPipeRaw, &hWritePipeRaw, nullptr, 65536)) { hWritePipe.emplace(hWritePipeRaw, &CloseHandle); - + if (HANDLE hReadPipeInheritableRaw; DuplicateHandle(GetCurrentProcess(), hReadPipeRaw, GetCurrentProcess(), &hReadPipeInheritableRaw, 0, TRUE, DUPLICATE_SAME_ACCESS | DUPLICATE_CLOSE_SOURCE)) { hReadPipeInheritable.emplace(hReadPipeInheritableRaw, &CloseHandle); @@ -315,9 +316,9 @@ bool veh::add_handler(bool doFullDump, const std::string& workingDirectory) } // additional information - STARTUPINFOEXW siex{}; + STARTUPINFOEXW siex{}; PROCESS_INFORMATION pi{}; - + siex.StartupInfo.cb = sizeof siex; siex.StartupInfo.dwFlags = STARTF_USESHOWWINDOW; siex.StartupInfo.wShowWindow = g_startInfo.CrashHandlerShow ? SW_SHOW : SW_HIDE; @@ -385,7 +386,7 @@ bool veh::add_handler(bool doFullDump, const std::string& workingDirectory) argstr.push_back(L' '); } argstr.pop_back(); - + if (!handles.empty() && !UpdateProcThreadAttribute(siex.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST, &handles[0], std::span(handles).size_bytes(), nullptr, nullptr)) { logging::W("Failed to launch DalamudCrashHandler.exe: UpdateProcThreadAttribute error 0x{:x}", GetLastError()); @@ -400,7 +401,7 @@ bool veh::add_handler(bool doFullDump, const std::string& workingDirectory) TRUE, // Set handle inheritance to FALSE EXTENDED_STARTUPINFO_PRESENT, // lpStartupInfo actually points to a STARTUPINFOEX(W) nullptr, // Use parent's environment block - nullptr, // Use parent's starting directory + nullptr, // Use parent's starting directory &siex.StartupInfo, // Pointer to STARTUPINFO structure &pi // Pointer to PROCESS_INFORMATION structure (removed extra parentheses) )) @@ -416,7 +417,7 @@ bool veh::add_handler(bool doFullDump, const std::string& workingDirectory) } CloseHandle(pi.hThread); - + g_crashhandler_process = pi.hProcess; g_crashhandler_pipe_write = hWritePipe->release(); logging::I("Launched DalamudCrashHandler.exe: PID {}", pi.dwProcessId); diff --git a/Dalamud.Common/DalamudStartInfo.cs b/Dalamud.Common/DalamudStartInfo.cs index a0d7f8b0b..8c66a85ba 100644 --- a/Dalamud.Common/DalamudStartInfo.cs +++ b/Dalamud.Common/DalamudStartInfo.cs @@ -34,6 +34,12 @@ public record DalamudStartInfo /// public string? ConfigurationPath { get; set; } + /// + /// Gets or sets the directory for temporary files. This directory needs to exist and be writable to the user. + /// It should also be predictable and easy for launchers to find. + /// + public string? TempDirectory { get; set; } + /// /// Gets or sets the path of the log files. /// 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..13fcacef2 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 @@ -305,6 +291,7 @@ namespace Dalamud.Injector var configurationPath = startInfo.ConfigurationPath; var pluginDirectory = startInfo.PluginDirectory; var assetDirectory = startInfo.AssetDirectory; + var tempDirectory = startInfo.TempDirectory; var delayInitializeMs = startInfo.DelayInitializeMs; var logName = startInfo.LogName; var logPath = startInfo.LogPath; @@ -335,6 +322,10 @@ namespace Dalamud.Injector { assetDirectory = args[i][key.Length..]; } + else if (args[i].StartsWith(key = "--dalamud-temp-directory=")) + { + tempDirectory = args[i][key.Length..]; + } else if (args[i].StartsWith(key = "--dalamud-delay-initialize=")) { delayInitializeMs = int.Parse(args[i][key.Length..]); @@ -447,6 +438,7 @@ namespace Dalamud.Injector startInfo.ConfigurationPath = configurationPath; startInfo.PluginDirectory = pluginDirectory; startInfo.AssetDirectory = assetDirectory; + startInfo.TempDirectory = tempDirectory; startInfo.Language = clientLanguage; startInfo.Platform = platform; startInfo.DelayInitializeMs = delayInitializeMs; 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/Rpc/DalamudUriTests.cs b/Dalamud.Test/Rpc/DalamudUriTests.cs new file mode 100644 index 000000000..b371a5698 --- /dev/null +++ b/Dalamud.Test/Rpc/DalamudUriTests.cs @@ -0,0 +1,108 @@ +using System; +using System.Linq; + +using Dalamud.Networking.Rpc.Model; + +using Xunit; + +namespace Dalamud.Test.Rpc +{ + 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/Dalamud.csproj b/Dalamud/Dalamud.csproj index d1f730d5e..090301c10 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -81,6 +81,7 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Dalamud/Game/Addon/Events/AddonEventManagerAddressResolver.cs b/Dalamud/Game/Addon/Events/AddonEventManagerAddressResolver.cs index 415e1b169..ec1c51a12 100644 --- a/Dalamud/Game/Addon/Events/AddonEventManagerAddressResolver.cs +++ b/Dalamud/Game/Addon/Events/AddonEventManagerAddressResolver.cs @@ -1,4 +1,6 @@ -namespace Dalamud.Game.Addon.Events; +using Dalamud.Plugin.Services; + +namespace Dalamud.Game.Addon.Events; /// /// AddonEventManager memory address resolver. diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs index 854d666fd..bc9e4b639 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs @@ -1,4 +1,4 @@ -using FFXIVClientStructs.FFXIV.Component.GUI; +using Dalamud.Plugin.Services; namespace Dalamud.Game.Addon.Lifecycle; diff --git a/Dalamud/Game/BaseAddressResolver.cs b/Dalamud/Game/BaseAddressResolver.cs index 4133117d7..d41b1d9d8 100644 --- a/Dalamud/Game/BaseAddressResolver.cs +++ b/Dalamud/Game/BaseAddressResolver.cs @@ -2,6 +2,8 @@ using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; +using Dalamud.Plugin.Services; + namespace Dalamud.Game; /// 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/ClientStateAddressResolver.cs b/Dalamud/Game/ClientState/ClientStateAddressResolver.cs index 2fc859d09..53774121d 100644 --- a/Dalamud/Game/ClientState/ClientStateAddressResolver.cs +++ b/Dalamud/Game/ClientState/ClientStateAddressResolver.cs @@ -1,3 +1,5 @@ +using Dalamud.Plugin.Services; + namespace Dalamud.Game.ClientState; /// 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/Objects/TargetManager.cs b/Dalamud/Game/ClientState/Objects/TargetManager.cs index f81154693..a6432e242 100644 --- a/Dalamud/Game/ClientState/Objects/TargetManager.cs +++ b/Dalamud/Game/ClientState/Objects/TargetManager.cs @@ -1,6 +1,7 @@ using Dalamud.Game.ClientState.Objects.Types; using Dalamud.IoC; using Dalamud.IoC.Internal; +using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game.Control; 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/Game/Config/GameConfigAddressResolver.cs b/Dalamud/Game/Config/GameConfigAddressResolver.cs index 2491c4033..e03f4f40b 100644 --- a/Dalamud/Game/Config/GameConfigAddressResolver.cs +++ b/Dalamud/Game/Config/GameConfigAddressResolver.cs @@ -1,4 +1,6 @@ -namespace Dalamud.Game.Config; +using Dalamud.Plugin.Services; + +namespace Dalamud.Game.Config; /// /// Game config system address resolver. diff --git a/Dalamud/Game/DutyState/DutyStateAddressResolver.cs b/Dalamud/Game/DutyState/DutyStateAddressResolver.cs index 1bca93efb..480b699a0 100644 --- a/Dalamud/Game/DutyState/DutyStateAddressResolver.cs +++ b/Dalamud/Game/DutyState/DutyStateAddressResolver.cs @@ -1,3 +1,5 @@ +using Dalamud.Plugin.Services; + namespace Dalamud.Game.DutyState; /// diff --git a/Dalamud/Game/Gui/GameGuiAddressResolver.cs b/Dalamud/Game/Gui/GameGuiAddressResolver.cs index 92b89c5a9..1295e2047 100644 --- a/Dalamud/Game/Gui/GameGuiAddressResolver.cs +++ b/Dalamud/Game/Gui/GameGuiAddressResolver.cs @@ -1,3 +1,5 @@ +using Dalamud.Plugin.Services; + namespace Dalamud.Game.Gui; /// diff --git a/Dalamud/Game/Gui/NamePlate/NamePlateGuiAddressResolver.cs b/Dalamud/Game/Gui/NamePlate/NamePlateGuiAddressResolver.cs index 450e1fa9f..f97450c28 100644 --- a/Dalamud/Game/Gui/NamePlate/NamePlateGuiAddressResolver.cs +++ b/Dalamud/Game/Gui/NamePlate/NamePlateGuiAddressResolver.cs @@ -1,3 +1,5 @@ +using Dalamud.Plugin.Services; + namespace Dalamud.Game.Gui.NamePlate; /// diff --git a/Dalamud/Game/Network/GameNetworkAddressResolver.cs b/Dalamud/Game/Network/GameNetworkAddressResolver.cs index de92f7c10..48abc2d97 100644 --- a/Dalamud/Game/Network/GameNetworkAddressResolver.cs +++ b/Dalamud/Game/Network/GameNetworkAddressResolver.cs @@ -1,3 +1,5 @@ +using Dalamud.Plugin.Services; + namespace Dalamud.Game.Network; /// diff --git a/Dalamud/Game/Network/Internal/NetworkHandlersAddressResolver.cs b/Dalamud/Game/Network/Internal/NetworkHandlersAddressResolver.cs index 9cd46f798..34c071556 100644 --- a/Dalamud/Game/Network/Internal/NetworkHandlersAddressResolver.cs +++ b/Dalamud/Game/Network/Internal/NetworkHandlersAddressResolver.cs @@ -1,4 +1,6 @@ -namespace Dalamud.Game.Network.Internal; +using Dalamud.Plugin.Services; + +namespace Dalamud.Game.Network.Internal; /// /// Internal address resolver for the network handlers. diff --git a/Dalamud/Game/SigScanner.cs b/Dalamud/Game/SigScanner.cs index c8a371aee..262e98fa5 100644 --- a/Dalamud/Game/SigScanner.cs +++ b/Dalamud/Game/SigScanner.cs @@ -8,6 +8,8 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Threading; +using Dalamud.Plugin.Services; + using Iced.Intel; using Newtonsoft.Json; using Serilog; diff --git a/Dalamud/Game/TargetSigScanner.cs b/Dalamud/Game/TargetSigScanner.cs index f60c32d9a..540d0ea47 100644 --- a/Dalamud/Game/TargetSigScanner.cs +++ b/Dalamud/Game/TargetSigScanner.cs @@ -1,8 +1,9 @@ -using System.Diagnostics; +using System.Diagnostics; using System.IO; using Dalamud.IoC; using Dalamud.IoC.Internal; +using Dalamud.Plugin.Services; namespace Dalamud.Game; 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/ImGuiBackend/InputHandler/Win32InputHandler.StaticLookupFunctions.cs b/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.StaticLookupFunctions.cs index 5710a5991..a7b70ce35 100644 --- a/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.StaticLookupFunctions.cs +++ b/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.StaticLookupFunctions.cs @@ -299,11 +299,12 @@ internal sealed partial class Win32InputHandler private static void ViewportFlagsToWin32Styles(ImGuiViewportFlags flags, out int style, out int exStyle) { - style = (int)(flags.HasFlag(ImGuiViewportFlags.NoDecoration) ? WS.WS_POPUP : WS.WS_OVERLAPPEDWINDOW); - exStyle = - (int)(flags.HasFlag(ImGuiViewportFlags.NoTaskBarIcon) ? WS.WS_EX_TOOLWINDOW : (uint)WS.WS_EX_APPWINDOW); + style = (flags & ImGuiViewportFlags.NoDecoration) != 0 ? unchecked((int)WS.WS_POPUP) : WS.WS_OVERLAPPEDWINDOW; + exStyle = (flags & ImGuiViewportFlags.NoTaskBarIcon) != 0 ? WS.WS_EX_TOOLWINDOW : WS.WS_EX_APPWINDOW; exStyle |= WS.WS_EX_NOREDIRECTIONBITMAP; - if (flags.HasFlag(ImGuiViewportFlags.TopMost)) + if ((flags & ImGuiViewportFlags.TopMost) != 0) exStyle |= WS.WS_EX_TOPMOST; + if ((flags & ImGuiViewportFlags.NoInputs) != 0) + exStyle |= WS.WS_EX_TRANSPARENT | WS.WS_EX_LAYERED; } } diff --git a/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.cs b/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.cs index 596df4c67..0b2e27b57 100644 --- a/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.cs +++ b/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.cs @@ -8,6 +8,7 @@ using System.Text; using Dalamud.Bindings.ImGui; using Dalamud.Memory; +using Dalamud.Utility; using Serilog; @@ -34,11 +35,12 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler private readonly HCURSOR[] cursors; private readonly WndProcDelegate wndProcDelegate; - private readonly bool[] imguiMouseIsDown; private readonly nint platformNamePtr; private ViewportHandler viewportHandler; + private int mouseButtonsDown; + private bool mouseTracked; private long lastTime; private nint iniPathPtr; @@ -64,7 +66,8 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler io.BackendFlags |= ImGuiBackendFlags.HasMouseCursors | ImGuiBackendFlags.HasSetMousePos | ImGuiBackendFlags.RendererHasViewports | - ImGuiBackendFlags.PlatformHasViewports; + ImGuiBackendFlags.PlatformHasViewports | + ImGuiBackendFlags.HasMouseHoveredViewport; this.platformNamePtr = Marshal.StringToHGlobalAnsi("imgui_impl_win32_c#"); io.Handle->BackendPlatformName = (byte*)this.platformNamePtr; @@ -74,8 +77,6 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler if (io.ConfigFlags.HasFlag(ImGuiConfigFlags.ViewportsEnable)) this.viewportHandler = new(this); - this.imguiMouseIsDown = new bool[5]; - this.cursors = new HCURSOR[9]; this.cursors[(int)ImGuiMouseCursor.Arrow] = LoadCursorW(default, IDC.IDC_ARROW); this.cursors[(int)ImGuiMouseCursor.TextInput] = LoadCursorW(default, IDC.IDC_IBEAM); @@ -95,8 +96,6 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler private delegate LRESULT WndProcDelegate(HWND hWnd, uint uMsg, WPARAM wparam, LPARAM lparam); - private delegate BOOL MonitorEnumProcDelegate(HMONITOR monitor, HDC hdc, RECT* rect, LPARAM lparam); - /// public bool UpdateCursor { get; set; } = true; @@ -155,6 +154,7 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler public void NewFrame(int targetWidth, int targetHeight) { var io = ImGui.GetIO(); + var focusedWindow = GetForegroundWindow(); io.DisplaySize.X = targetWidth; io.DisplaySize.Y = targetHeight; @@ -168,9 +168,9 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler this.viewportHandler.UpdateMonitors(); - this.UpdateMousePos(); + this.UpdateMouseData(focusedWindow); - this.ProcessKeyEventsWorkarounds(); + this.ProcessKeyEventsWorkarounds(focusedWindow); // TODO: need to figure out some way to unify all this // The bottom case works(?) if the caller hooks SetCursor, but otherwise causes fps issues @@ -224,6 +224,40 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler switch (msg) { + case WM.WM_MOUSEMOVE: + { + if (!this.mouseTracked) + { + var tme = new TRACKMOUSEEVENT + { + cbSize = (uint)sizeof(TRACKMOUSEEVENT), + dwFlags = TME.TME_LEAVE, + hwndTrack = hWndCurrent, + }; + this.mouseTracked = TrackMouseEvent(&tme); + } + + var mousePos = new POINT(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)); + if ((io.ConfigFlags & ImGuiConfigFlags.ViewportsEnable) != 0) + ClientToScreen(hWndCurrent, &mousePos); + io.AddMousePosEvent(mousePos.x, mousePos.y); + break; + } + + case WM.WM_MOUSELEAVE: + { + this.mouseTracked = false; + var mouseScreenPos = new POINT(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)); + ClientToScreen(hWndCurrent, &mouseScreenPos); + if (this.ViewportFromPoint(mouseScreenPos).IsNull) + { + var fltMax = ImGuiNative.GETFLTMAX(); + io.AddMousePosEvent(-fltMax, -fltMax); + } + + break; + } + case WM.WM_LBUTTONDOWN: case WM.WM_LBUTTONDBLCLK: case WM.WM_RBUTTONDOWN: @@ -236,11 +270,10 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler var button = GetButton(msg, wParam); if (io.WantCaptureMouse) { - if (!ImGui.IsAnyMouseDown() && GetCapture() == nint.Zero) + if (this.mouseButtonsDown == 0 && GetCapture() == nint.Zero) SetCapture(hWndCurrent); - - io.MouseDown[button] = true; - this.imguiMouseIsDown[button] = true; + this.mouseButtonsDown |= 1 << button; + io.AddMouseButtonEvent(button, true); return default(LRESULT); } @@ -256,13 +289,12 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler case WM.WM_XBUTTONUP: { var button = GetButton(msg, wParam); - if (io.WantCaptureMouse && this.imguiMouseIsDown[button]) + if (io.WantCaptureMouse) { - if (!ImGui.IsAnyMouseDown() && GetCapture() == hWndCurrent) + this.mouseButtonsDown &= ~(1 << button); + if (this.mouseButtonsDown == 0 && GetCapture() == hWndCurrent) ReleaseCapture(); - - io.MouseDown[button] = false; - this.imguiMouseIsDown[button] = false; + io.AddMouseButtonEvent(button, false); return default(LRESULT); } @@ -272,7 +304,7 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler case WM.WM_MOUSEWHEEL: if (io.WantCaptureMouse) { - io.MouseWheel += GET_WHEEL_DELTA_WPARAM(wParam) / (float)WHEEL_DELTA; + io.AddMouseWheelEvent(0, GET_WHEEL_DELTA_WPARAM(wParam) / (float)WHEEL_DELTA); return default(LRESULT); } @@ -280,7 +312,7 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler case WM.WM_MOUSEHWHEEL: if (io.WantCaptureMouse) { - io.MouseWheelH += GET_WHEEL_DELTA_WPARAM(wParam) / (float)WHEEL_DELTA; + io.AddMouseWheelEvent(GET_WHEEL_DELTA_WPARAM(wParam) / (float)WHEEL_DELTA, 0); return default(LRESULT); } @@ -374,68 +406,86 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler this.viewportHandler.UpdateMonitors(); break; - case WM.WM_KILLFOCUS when hWndCurrent == this.hWnd: - if (!ImGui.IsAnyMouseDown() && GetCapture() == hWndCurrent) - ReleaseCapture(); + case WM.WM_SETFOCUS when hWndCurrent == this.hWnd: + io.AddFocusEvent(true); + break; - ImGui.GetIO().WantCaptureMouse = false; - ImGui.ClearWindowFocus(); + case WM.WM_KILLFOCUS when hWndCurrent == this.hWnd: + io.AddFocusEvent(false); + // if (!ImGui.IsAnyMouseDown() && GetCapture() == hWndCurrent) + // ReleaseCapture(); + // + // ImGui.GetIO().WantCaptureMouse = false; + // ImGui.ClearWindowFocus(); break; } return null; } - private void UpdateMousePos() + private void UpdateMouseData(HWND focusedWindow) { var io = ImGui.GetIO(); - var pt = default(POINT); - // Depending on if Viewports are enabled, we have to change how we process - // the cursor position. If viewports are enabled, we pass the absolute cursor - // position to ImGui. Otherwise, we use the old method of passing client-local - // mouse position to ImGui. - if (io.ConfigFlags.HasFlag(ImGuiConfigFlags.ViewportsEnable)) + var mouseScreenPos = default(POINT); + var hasMouseScreenPos = GetCursorPos(&mouseScreenPos) != 0; + + var isAppFocused = + focusedWindow != default + && (focusedWindow == this.hWnd + || IsChild(focusedWindow, this.hWnd) + || !ImGui.FindViewportByPlatformHandle(focusedWindow).IsNull); + + if (isAppFocused) { + // (Optional) Set OS mouse position from Dear ImGui if requested (rarely used, only when ImGuiConfigFlags_NavEnableSetMousePos is enabled by user) + // When multi-viewports are enabled, all Dear ImGui positions are same as OS positions. if (io.WantSetMousePos) { - SetCursorPos((int)io.MousePos.X, (int)io.MousePos.Y); + var pos = new POINT((int)io.MousePos.X, (int)io.MousePos.Y); + if ((io.ConfigFlags & ImGuiConfigFlags.ViewportsEnable) != 0) + ClientToScreen(this.hWnd, &pos); + SetCursorPos(pos.x, pos.y); } + } - if (GetCursorPos(&pt)) - { - io.MousePos.X = pt.x; - io.MousePos.Y = pt.y; - } - else - { - io.MousePos.X = float.MinValue; - io.MousePos.Y = float.MinValue; - } + // (Optional) Fallback to provide mouse position when focused (WM_MOUSEMOVE already provides this when hovered or captured) + if (!io.WantSetMousePos && !this.mouseTracked && hasMouseScreenPos) + { + // Single viewport mode: mouse position in client window coordinates (io.MousePos is (0,0) when the mouse is on the upper-left corner of the app window) + // (This is the position you can get with ::GetCursorPos() + ::ScreenToClient() or WM_MOUSEMOVE.) + // Multi-viewport mode: mouse position in OS absolute coordinates (io.MousePos is (0,0) when the mouse is on the upper-left of the primary monitor) + // (This is the position you can get with ::GetCursorPos() or WM_MOUSEMOVE + ::ClientToScreen(). In theory adding viewport->Pos to a client position would also be the same.) + var mousePos = mouseScreenPos; + if ((io.ConfigFlags & ImGuiConfigFlags.ViewportsEnable) == 0) + ClientToScreen(focusedWindow, &mousePos); + io.AddMousePosEvent(mousePos.x, mousePos.y); + } + + // (Optional) When using multiple viewports: call io.AddMouseViewportEvent() with the viewport the OS mouse cursor is hovering. + // If ImGuiBackendFlags_HasMouseHoveredViewport is not set by the backend, Dear imGui will ignore this field and infer the information using its flawed heuristic. + // - [X] Win32 backend correctly ignore viewports with the _NoInputs flag (here using ::WindowFromPoint with WM_NCHITTEST + HTTRANSPARENT in WndProc does that) + // Some backend are not able to handle that correctly. If a backend report an hovered viewport that has the _NoInputs flag (e.g. when dragging a window + // for docking, the viewport has the _NoInputs flag in order to allow us to find the viewport under), then Dear ImGui is forced to ignore the value reported + // by the backend, and use its flawed heuristic to guess the viewport behind. + // - [X] Win32 backend correctly reports this regardless of another viewport behind focused and dragged from (we need this to find a useful drag and drop target). + if (hasMouseScreenPos) + { + var viewport = this.ViewportFromPoint(mouseScreenPos); + io.AddMouseViewportEvent(!viewport.IsNull ? viewport.ID : 0u); } else { - if (io.WantSetMousePos) - { - pt.x = (int)io.MousePos.X; - pt.y = (int)io.MousePos.Y; - ClientToScreen(this.hWnd, &pt); - SetCursorPos(pt.x, pt.y); - } - - if (GetCursorPos(&pt) && ScreenToClient(this.hWnd, &pt)) - { - io.MousePos.X = pt.x; - io.MousePos.Y = pt.y; - } - else - { - io.MousePos.X = float.MinValue; - io.MousePos.Y = float.MinValue; - } + io.AddMouseViewportEvent(0); } } + private ImGuiViewportPtr ViewportFromPoint(POINT mouseScreenPos) + { + var hoveredHwnd = WindowFromPoint(mouseScreenPos); + return hoveredHwnd != default ? ImGui.FindViewportByPlatformHandle(hoveredHwnd) : default; + } + private bool UpdateMouseCursor() { var io = ImGui.GetIO(); @@ -451,7 +501,7 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler return true; } - private void ProcessKeyEventsWorkarounds() + private void ProcessKeyEventsWorkarounds(HWND focusedWindow) { // Left & right Shift keys: when both are pressed together, Windows tend to not generate the WM_KEYUP event for the first released one. if (ImGui.IsKeyDown(ImGuiKey.LeftShift) && !IsVkDown(VK.VK_LSHIFT)) @@ -480,7 +530,7 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler { // See: https://github.com/goatcorp/ImGuiScene/pull/13 // > GetForegroundWindow from winuser.h is a surprisingly expensive function. - var isForeground = GetForegroundWindow() == this.hWnd; + var isForeground = focusedWindow == this.hWnd; for (var i = (int)ImGuiKey.NamedKeyBegin; i < (int)ImGuiKey.NamedKeyEnd; i++) { // Skip raising modifier keys if the game is focused. @@ -646,14 +696,7 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler return; var pio = ImGui.GetPlatformIO(); - - if (ImGui.GetPlatformIO().Handle->Monitors.Data != null) - { - // We allocated the platform monitor data in OnUpdateMonitors ourselves, - // so we have to free it ourselves to ImGui doesn't try to, or else it will crash - Marshal.FreeHGlobal(new IntPtr(ImGui.GetPlatformIO().Handle->Monitors.Data)); - ImGui.GetPlatformIO().Handle->Monitors = default; - } + ImGui.GetPlatformIO().Handle->Monitors.Free(); fixed (char* windowClassNamePtr = WindowClassName) { @@ -693,59 +736,50 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler // Here we use a manual ImVector overload, free the existing monitor data, // and allocate our own, as we are responsible for telling ImGui about monitors var pio = ImGui.GetPlatformIO(); - var numMonitors = GetSystemMetrics(SM.SM_CMONITORS); - var data = Marshal.AllocHGlobal(Marshal.SizeOf() * numMonitors); - if (pio.Handle->Monitors.Data != null) - Marshal.FreeHGlobal(new IntPtr(pio.Handle->Monitors.Data)); - pio.Handle->Monitors = new(numMonitors, numMonitors, (ImGuiPlatformMonitor*)data.ToPointer()); + pio.Handle->Monitors.Resize(0); - // ImGuiPlatformIOPtr platformIO = ImGui.GetPlatformIO(); - // Marshal.FreeHGlobal(platformIO.Handle->Monitors.Data); - // int numMonitors = GetSystemMetrics(SystemMetric.SM_CMONITORS); - // nint data = Marshal.AllocHGlobal(Marshal.SizeOf() * numMonitors); - // platformIO.Handle->Monitors = new ImVector(numMonitors, numMonitors, data); - - var monitorIndex = -1; - var enumfn = new MonitorEnumProcDelegate( - (hMonitor, _, _, _) => - { - monitorIndex++; - var info = new MONITORINFO { cbSize = (uint)sizeof(MONITORINFO) }; - if (!GetMonitorInfoW(hMonitor, &info)) - return true; - - var monitorLt = new Vector2(info.rcMonitor.left, info.rcMonitor.top); - var monitorRb = new Vector2(info.rcMonitor.right, info.rcMonitor.bottom); - var workLt = new Vector2(info.rcWork.left, info.rcWork.top); - var workRb = new Vector2(info.rcWork.right, info.rcWork.bottom); - // Give ImGui the info for this display - - ref var imMonitor = ref ImGui.GetPlatformIO().Monitors.Ref(monitorIndex); - imMonitor.MainPos = monitorLt; - imMonitor.MainSize = monitorRb - monitorLt; - imMonitor.WorkPos = workLt; - imMonitor.WorkSize = workRb - workLt; - imMonitor.DpiScale = 1f; - return true; - }); - EnumDisplayMonitors( - default, - null, - (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(enumfn), - default); + EnumDisplayMonitors(default, null, &EnumDisplayMonitorsCallback, default); Log.Information("Monitors set up!"); - for (var i = 0; i < numMonitors; i++) + foreach (ref var monitor in pio.Handle->Monitors) { - var monitor = pio.Handle->Monitors[i]; Log.Information( - "Monitor {Index}: {MainPos} {MainSize} {WorkPos} {WorkSize}", - i, + "Monitor: {MainPos} {MainSize} {WorkPos} {WorkSize}", monitor.MainPos, monitor.MainSize, monitor.WorkPos, monitor.WorkSize); } + + return; + + [UnmanagedCallersOnly] + static BOOL EnumDisplayMonitorsCallback(HMONITOR hMonitor, HDC hdc, RECT* rect, LPARAM lParam) + { + var info = new MONITORINFO { cbSize = (uint)sizeof(MONITORINFO) }; + if (!GetMonitorInfoW(hMonitor, &info)) + return true; + + var monitorLt = new Vector2(info.rcMonitor.left, info.rcMonitor.top); + var monitorRb = new Vector2(info.rcMonitor.right, info.rcMonitor.bottom); + var workLt = new Vector2(info.rcWork.left, info.rcWork.top); + var workRb = new Vector2(info.rcWork.right, info.rcWork.bottom); + + // Give ImGui the info for this display + var imMonitor = new ImGuiPlatformMonitor + { + MainPos = monitorLt, + MainSize = monitorRb - monitorLt, + WorkPos = workLt, + WorkSize = workRb - workLt, + DpiScale = 1f, + }; + if ((info.dwFlags & MONITORINFOF_PRIMARY) != 0) + ImGui.GetPlatformIO().Monitors.PushFront(imMonitor); + else + ImGui.GetPlatformIO().Monitors.PushBack(imMonitor); + return true; + } } [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])] @@ -794,6 +828,9 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler null); } + if (data->Hwnd == 0) + Util.Fatal($"CreateWindowExW failed: {GetLastError()}", "ImGui Viewport error"); + data->HwndOwned = true; viewport.PlatformRequestResize = false; viewport.PlatformHandle = viewport.PlatformHandleRaw = data->Hwnd; diff --git a/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringColorStackSet.cs b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringColorStackSet.cs index ad60d405e..85ab2e441 100644 --- a/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringColorStackSet.cs +++ b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringColorStackSet.cs @@ -15,10 +15,6 @@ namespace Dalamud.Interface.ImGuiSeStringRenderer.Internal; /// Color stacks to use while evaluating a SeString. internal sealed class SeStringColorStackSet { - /// Parsed , containing colors to use with and - /// . - private readonly uint[,] colorTypes; - /// Foreground color stack while evaluating a SeString for rendering. /// Touched only from the main thread. private readonly List colorStack = []; @@ -39,30 +35,38 @@ internal sealed class SeStringColorStackSet foreach (var row in uiColor) maxId = (int)Math.Max(row.RowId, maxId); - this.colorTypes = new uint[maxId + 1, 4]; + this.ColorTypes = new uint[maxId + 1, 4]; foreach (var row in uiColor) { // Contains ABGR. - this.colorTypes[row.RowId, 0] = row.Dark; - this.colorTypes[row.RowId, 1] = row.Light; - this.colorTypes[row.RowId, 2] = row.ClassicFF; - this.colorTypes[row.RowId, 3] = row.ClearBlue; + this.ColorTypes[row.RowId, 0] = row.Dark; + this.ColorTypes[row.RowId, 1] = row.Light; + this.ColorTypes[row.RowId, 2] = row.ClassicFF; + this.ColorTypes[row.RowId, 3] = row.ClearBlue; } if (BitConverter.IsLittleEndian) { // ImGui wants RGBA in LE. - fixed (uint* p = this.colorTypes) + fixed (uint* p = this.ColorTypes) { - foreach (ref var r in new Span(p, this.colorTypes.GetLength(0) * this.colorTypes.GetLength(1))) + foreach (ref var r in new Span(p, this.ColorTypes.GetLength(0) * this.ColorTypes.GetLength(1))) r = BinaryPrimitives.ReverseEndianness(r); } } } + /// Initializes a new instance of the class. + /// Color types. + public SeStringColorStackSet(uint[,] colorTypes) => this.ColorTypes = colorTypes; + /// Gets a value indicating whether at least one color has been pushed to the edge color stack. public bool HasAdditionalEdgeColor { get; private set; } + /// Gets the parsed containing colors to use with + /// and . + public uint[,] ColorTypes { get; } + /// Resets the colors in the stack. /// Draw state. internal void Initialize(scoped ref SeStringDrawState drawState) @@ -191,9 +195,9 @@ internal sealed class SeStringColorStackSet } // Opacity component is ignored. - var color = themeIndex >= 0 && themeIndex < this.colorTypes.GetLength(1) && - colorTypeIndex < this.colorTypes.GetLength(0) - ? this.colorTypes[colorTypeIndex, themeIndex] + var color = themeIndex >= 0 && themeIndex < this.ColorTypes.GetLength(1) && + colorTypeIndex < this.ColorTypes.GetLength(0) + ? this.ColorTypes[colorTypeIndex, themeIndex] : 0u; rgbaStack.Add(color | 0xFF000000u); diff --git a/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs index d0c40cd9f..87df2da2c 100644 --- a/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs +++ b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Numerics; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; @@ -25,7 +26,7 @@ namespace Dalamud.Interface.ImGuiSeStringRenderer.Internal; /// Draws SeString. [ServiceManager.EarlyLoadedService] -internal unsafe class SeStringRenderer : IInternalDisposableService +internal class SeStringRenderer : IServiceType { private const int ImGuiContextCurrentWindowOffset = 0x3FF0; private const int ImGuiWindowDcOffset = 0x118; @@ -47,28 +48,19 @@ internal unsafe class SeStringRenderer : IInternalDisposableService /// Parsed text fragments from a SeString. /// Touched only from the main thread. - private readonly List fragments = []; + private readonly List fragmentsMainThread = []; /// Color stacks to use while evaluating a SeString for rendering. /// Touched only from the main thread. - private readonly SeStringColorStackSet colorStackSet; - - /// Splits a draw list so that different layers of a single glyph can be drawn out of order. - private ImDrawListSplitter* splitter = ImGui.ImDrawListSplitter(); + private readonly SeStringColorStackSet colorStackSetMainThread; [ServiceManager.ServiceConstructor] private SeStringRenderer(DataManager dm, TargetSigScanner sigScanner) { - this.colorStackSet = new(dm.Excel.GetSheet()); + this.colorStackSetMainThread = new(dm.Excel.GetSheet()); this.gfd = dm.GetFile("common/font/gfdata.gfd")!; } - /// Finalizes an instance of the class. - ~SeStringRenderer() => this.ReleaseUnmanagedResources(); - - /// - void IInternalDisposableService.DisposeService() => this.ReleaseUnmanagedResources(); - /// Compiles and caches a SeString from a text macro representation. /// SeString text macro representation. /// Newline characters will be normalized to newline payloads. @@ -80,6 +72,44 @@ internal unsafe class SeStringRenderer : IInternalDisposableService text.ReplaceLineEndings("
"), new() { ExceptionMode = MacroStringParseExceptionMode.EmbedError })); + /// Creates a draw data that will draw the given SeString onto it. + /// SeString to render. + /// Parameters for drawing. + /// A new self-contained draw data. + public unsafe BufferBackedImDrawData CreateDrawData( + ReadOnlySeStringSpan sss, + scoped in SeStringDrawParams drawParams = default) + { + if (drawParams.TargetDrawList is not null) + { + throw new ArgumentException( + $"{nameof(SeStringDrawParams.TargetDrawList)} may not be specified.", + nameof(drawParams)); + } + + var dd = BufferBackedImDrawData.Create(); + + try + { + var size = this.Draw(sss, drawParams with { TargetDrawList = dd.ListPtr }).Size; + + var offset = drawParams.ScreenOffset ?? Vector2.Zero; + foreach (var vtx in new Span(dd.ListPtr.VtxBuffer.Data, dd.ListPtr.VtxBuffer.Size)) + offset = Vector2.Min(offset, vtx.Pos); + + dd.Data.DisplayPos = offset; + dd.Data.DisplaySize = size - offset; + dd.Data.Valid = 1; + dd.UpdateDrawDataStatistics(); + return dd; + } + catch + { + dd.Dispose(); + throw; + } + } + /// Compiles and caches a SeString from a text macro representation, and then draws it. /// SeString text macro representation. /// Newline characters will be normalized to newline payloads. @@ -113,28 +143,42 @@ internal unsafe class SeStringRenderer : IInternalDisposableService /// ImGui ID, if link functionality is desired. /// Button flags to use on link interaction. /// Interaction result of the rendered text. - public SeStringDrawResult Draw( + public unsafe SeStringDrawResult Draw( ReadOnlySeStringSpan sss, scoped in SeStringDrawParams drawParams = default, ImGuiId imGuiId = default, ImGuiButtonFlags buttonFlags = ImGuiButtonFlags.MouseButtonDefault) { - // Drawing is only valid if done from the main thread anyway, especially with interactivity. - ThreadSafety.AssertMainThread(); + // Interactivity is supported only from the main thread. + if (!imGuiId.IsEmpty()) + ThreadSafety.AssertMainThread(); if (drawParams.TargetDrawList is not null && imGuiId) throw new ArgumentException("ImGuiId cannot be set if TargetDrawList is manually set.", nameof(imGuiId)); - // This also does argument validation for drawParams. Do it here. - var state = new SeStringDrawState(sss, drawParams, this.colorStackSet, this.splitter); + using var cleanup = new DisposeSafety.ScopedFinalizer(); - // Reset and initialize the state. - this.fragments.Clear(); - this.colorStackSet.Initialize(ref state); + ImFont* font = null; + if (drawParams.Font.HasValue) + font = drawParams.Font.Value; + if (ThreadSafety.IsMainThread && drawParams.TargetDrawList is null && font is null) + font = ImGui.GetFont(); + if (font is null) + throw new ArgumentException("Specified font is empty."); + + // This also does argument validation for drawParams. Do it here. + // `using var` makes a struct read-only, but we do want to modify it. + var stateStorage = new SeStringDrawState( + sss, + drawParams, + ThreadSafety.IsMainThread ? this.colorStackSetMainThread : new(this.colorStackSetMainThread.ColorTypes), + ThreadSafety.IsMainThread ? this.fragmentsMainThread : [], + font); + ref var state = ref Unsafe.AsRef(in stateStorage); // Analyze the provided SeString and break it up to text fragments. this.CreateTextFragments(ref state); - var fragmentSpan = CollectionsMarshal.AsSpan(this.fragments); + var fragmentSpan = CollectionsMarshal.AsSpan(state.Fragments); // Calculate size. var size = Vector2.Zero; @@ -147,24 +191,17 @@ internal unsafe class SeStringRenderer : IInternalDisposableService state.SplitDrawList(); - // Handle cases where ImGui.AlignTextToFramePadding has been called. - var context = ImGui.GetCurrentContext(); - var currLineTextBaseOffset = 0f; - if (!context.IsNull) - { - var currentWindow = context.CurrentWindow; - if (!currentWindow.IsNull) - { - currLineTextBaseOffset = currentWindow.DC.CurrLineTextBaseOffset; - } - } - var itemSize = size; - if (currLineTextBaseOffset != 0f) + if (drawParams.TargetDrawList is null) { - itemSize.Y += 2 * currLineTextBaseOffset; - foreach (ref var f in fragmentSpan) - f.Offset += new Vector2(0, currLineTextBaseOffset); + // Handle cases where ImGui.AlignTextToFramePadding has been called. + var currLineTextBaseOffset = ImGui.GetCurrentContext().CurrentWindow.DC.CurrLineTextBaseOffset; + if (currLineTextBaseOffset != 0f) + { + itemSize.Y += 2 * currLineTextBaseOffset; + foreach (ref var f in fragmentSpan) + f.Offset += new Vector2(0, currLineTextBaseOffset); + } } // Draw all text fragments. @@ -280,15 +317,6 @@ internal unsafe class SeStringRenderer : IInternalDisposableService return displayRune.Value != 0; } - private void ReleaseUnmanagedResources() - { - if (this.splitter is not null) - { - this.splitter->Destroy(); - this.splitter = null; - } - } - /// Creates text fragment, taking line and word breaking into account. /// Draw state. private void CreateTextFragments(ref SeStringDrawState state) @@ -391,7 +419,7 @@ internal unsafe class SeStringRenderer : IInternalDisposableService var overflows = Math.Max(w, xy.X + fragment.VisibleWidth) > state.WrapWidth; // Test if the fragment does not fit into the current line and the current line is not empty. - if (xy.X != 0 && this.fragments.Count > 0 && !this.fragments[^1].BreakAfter && overflows) + if (xy.X != 0 && state.Fragments.Count > 0 && !state.Fragments[^1].BreakAfter && overflows) { // Introduce break if this is the first time testing the current break unit or the current fragment // is an entity. @@ -401,7 +429,7 @@ internal unsafe class SeStringRenderer : IInternalDisposableService xy.X = 0; xy.Y += state.LineHeight; w = 0; - CollectionsMarshal.AsSpan(this.fragments)[^1].BreakAfter = true; + CollectionsMarshal.AsSpan(state.Fragments)[^1].BreakAfter = true; fragment.Offset = xy; // Now that the fragment is given its own line, test if it overflows again. @@ -419,16 +447,16 @@ internal unsafe class SeStringRenderer : IInternalDisposableService fragment = this.CreateFragment(state, prev, curr, true, xy, link, entity, remainingWidth); } } - else if (this.fragments.Count > 0 && xy.X != 0) + else if (state.Fragments.Count > 0 && xy.X != 0) { // New fragment fits into the current line, and it has a previous fragment in the same line. // If the previous fragment ends with a soft hyphen, adjust its width so that the width of its // trailing soft hyphens are not considered. - if (this.fragments[^1].EndsWithSoftHyphen) - xy.X += this.fragments[^1].AdvanceWidthWithoutSoftHyphen - this.fragments[^1].AdvanceWidth; + if (state.Fragments[^1].EndsWithSoftHyphen) + xy.X += state.Fragments[^1].AdvanceWidthWithoutSoftHyphen - state.Fragments[^1].AdvanceWidth; // Adjust this fragment's offset from kerning distance. - xy.X += state.CalculateScaledDistance(this.fragments[^1].LastRune, fragment.FirstRune); + xy.X += state.CalculateScaledDistance(state.Fragments[^1].LastRune, fragment.FirstRune); fragment.Offset = xy; } @@ -439,7 +467,7 @@ internal unsafe class SeStringRenderer : IInternalDisposableService w = Math.Max(w, xy.X + fragment.VisibleWidth); xy.X += fragment.AdvanceWidth; prev = fragment.To; - this.fragments.Add(fragment); + state.Fragments.Add(fragment); if (fragment.BreakAfter) { @@ -491,7 +519,7 @@ internal unsafe class SeStringRenderer : IInternalDisposableService if (gfdTextureSrv != 0) { state.Draw( - new ImTextureID(gfdTextureSrv), + new(gfdTextureSrv), offset + new Vector2(x, MathF.Round((state.LineHeight - size.Y) / 2)), size, useHq ? gfdEntry.HqUv0 : gfdEntry.Uv0, @@ -528,7 +556,7 @@ internal unsafe class SeStringRenderer : IInternalDisposableService return; - static nint GetGfdTextureSrv() + static unsafe nint GetGfdTextureSrv() { var uim = UIModule.Instance(); if (uim is null) @@ -553,7 +581,7 @@ internal unsafe class SeStringRenderer : IInternalDisposableService /// Determines a bitmap icon to display for the given SeString payload. /// Byte span that should include a SeString payload. /// Icon to display, or if it should not be displayed as an icon. - private BitmapFontIcon GetBitmapFontIconFor(ReadOnlySpan sss) + private unsafe BitmapFontIcon GetBitmapFontIconFor(ReadOnlySpan sss) { var e = new ReadOnlySeStringSpan(sss).GetEnumerator(); if (!e.MoveNext() || e.Current.MacroCode is not MacroCode.Icon and not MacroCode.Icon2) @@ -710,38 +738,4 @@ internal unsafe class SeStringRenderer : IInternalDisposableService firstDisplayRune ?? default, lastNonSoftHyphenRune); } - - /// Represents a text fragment in a SeString span. - /// Starting byte offset (inclusive) in a SeString. - /// Ending byte offset (exclusive) in a SeString. - /// Byte offset of the link that decorates this text fragment, or -1 if none. - /// Offset in pixels w.r.t. . - /// Replacement entity, if any. - /// Visible width of this text fragment. This is the width required to draw everything - /// without clipping. - /// Advance width of this text fragment. This is the width required to add to the cursor - /// to position the next fragment correctly. - /// Same with , but trimming all the - /// trailing soft hyphens. - /// Whether to insert a line break after this text fragment. - /// Whether this text fragment ends with one or more soft hyphens. - /// First rune in this text fragment. - /// Last rune in this text fragment, for the purpose of calculating kerning distance with - /// the following text fragment in the same line, if any. - private record struct TextFragment( - int From, - int To, - int Link, - Vector2 Offset, - SeStringReplacementEntity Entity, - float VisibleWidth, - float AdvanceWidth, - float AdvanceWidthWithoutSoftHyphen, - bool BreakAfter, - bool EndsWithSoftHyphen, - Rune FirstRune, - Rune LastRune) - { - public bool IsSoftHyphenVisible => this.EndsWithSoftHyphen && this.BreakAfter; - } } diff --git a/Dalamud/Interface/ImGuiSeStringRenderer/Internal/TextFragment.cs b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/TextFragment.cs new file mode 100644 index 000000000..a64c32109 --- /dev/null +++ b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/TextFragment.cs @@ -0,0 +1,39 @@ +using System.Numerics; +using System.Text; + +namespace Dalamud.Interface.ImGuiSeStringRenderer.Internal; + +/// Represents a text fragment in a SeString span. +/// Starting byte offset (inclusive) in a SeString. +/// Ending byte offset (exclusive) in a SeString. +/// Byte offset of the link that decorates this text fragment, or -1 if none. +/// Offset in pixels w.r.t. . +/// Replacement entity, if any. +/// Visible width of this text fragment. This is the width required to draw everything +/// without clipping. +/// Advance width of this text fragment. This is the width required to add to the cursor +/// to position the next fragment correctly. +/// Same with , but trimming all the +/// trailing soft hyphens. +/// Whether to insert a line break after this text fragment. +/// Whether this text fragment ends with one or more soft hyphens. +/// First rune in this text fragment. +/// Last rune in this text fragment, for the purpose of calculating kerning distance with +/// the following text fragment in the same line, if any. +internal record struct TextFragment( + int From, + int To, + int Link, + Vector2 Offset, + SeStringReplacementEntity Entity, + float VisibleWidth, + float AdvanceWidth, + float AdvanceWidthWithoutSoftHyphen, + bool BreakAfter, + bool EndsWithSoftHyphen, + Rune FirstRune, + Rune LastRune) +{ + /// Gets a value indicating whether the fragment ends with a visible soft hyphen. + public bool IsSoftHyphenVisible => this.EndsWithSoftHyphen && this.BreakAfter; +} diff --git a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs index 64a7f3db3..11c1120b4 100644 --- a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs +++ b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -6,6 +7,8 @@ using System.Text; using Dalamud.Bindings.ImGui; using Dalamud.Interface.ImGuiSeStringRenderer.Internal; using Dalamud.Interface.Utility; +using Dalamud.Utility; + using FFXIVClientStructs.FFXIV.Component.GUI; using Lumina.Text.Payloads; using Lumina.Text.ReadOnly; @@ -19,46 +22,75 @@ public unsafe ref struct SeStringDrawState private static readonly int ChannelCount = Enum.GetValues().Length; private readonly ImDrawList* drawList; - private readonly SeStringColorStackSet colorStackSet; - private readonly ImDrawListSplitter* splitter; + + private ImDrawListSplitter splitter; /// Initializes a new instance of the struct. /// Raw SeString byte span. /// Instance of to initialize from. /// Instance of to use. - /// Instance of ImGui Splitter to use. + /// Fragments. + /// Font to use. internal SeStringDrawState( ReadOnlySpan span, scoped in SeStringDrawParams ssdp, SeStringColorStackSet colorStackSet, - ImDrawListSplitter* splitter) + List fragments, + ImFont* font) { - this.colorStackSet = colorStackSet; - this.splitter = splitter; - this.drawList = ssdp.TargetDrawList ?? ImGui.GetWindowDrawList(); this.Span = span; + this.ColorStackSet = colorStackSet; + this.Fragments = fragments; + this.Font = font; + + if (ssdp.TargetDrawList is null) + { + if (!ThreadSafety.IsMainThread) + { + throw new ArgumentException( + $"{nameof(ssdp.TargetDrawList)} must be set to render outside the main thread."); + } + + this.drawList = ssdp.TargetDrawList ?? ImGui.GetWindowDrawList(); + this.ScreenOffset = ssdp.ScreenOffset ?? ImGui.GetCursorScreenPos(); + this.FontSize = ssdp.FontSize ?? ImGui.GetFontSize(); + this.WrapWidth = ssdp.WrapWidth ?? ImGui.GetContentRegionAvail().X; + this.Color = ssdp.Color ?? ImGui.GetColorU32(ImGuiCol.Text); + this.LinkHoverBackColor = ssdp.LinkHoverBackColor ?? ImGui.GetColorU32(ImGuiCol.ButtonHovered); + this.LinkActiveBackColor = ssdp.LinkActiveBackColor ?? ImGui.GetColorU32(ImGuiCol.ButtonActive); + this.ThemeIndex = ssdp.ThemeIndex ?? AtkStage.Instance()->AtkUIColorHolder->ActiveColorThemeType; + } + else + { + this.drawList = ssdp.TargetDrawList.Value; + this.ScreenOffset = Vector2.Zero; + this.FontSize = ssdp.FontSize ?? throw new ArgumentException( + $"{nameof(ssdp.FontSize)} must be set to render outside the main thread."); + this.WrapWidth = ssdp.WrapWidth ?? float.MaxValue; + this.Color = ssdp.Color ?? uint.MaxValue; + this.LinkHoverBackColor = 0; // Interactivity is unused outside the main thread. + this.LinkActiveBackColor = 0; // Interactivity is unused outside the main thread. + this.ThemeIndex = ssdp.ThemeIndex ?? 0; + } + + this.splitter = default; this.GetEntity = ssdp.GetEntity; - this.ScreenOffset = ssdp.ScreenOffset ?? ImGui.GetCursorScreenPos(); this.ScreenOffset = new(MathF.Round(this.ScreenOffset.X), MathF.Round(this.ScreenOffset.Y)); - this.Font = ssdp.EffectiveFont; - this.FontSize = ssdp.FontSize ?? ImGui.GetFontSize(); this.FontSizeScale = this.FontSize / this.Font->FontSize; this.LineHeight = MathF.Round(ssdp.EffectiveLineHeight); - this.WrapWidth = ssdp.WrapWidth ?? ImGui.GetContentRegionAvail().X; this.LinkUnderlineThickness = ssdp.LinkUnderlineThickness ?? 0f; this.Opacity = ssdp.EffectiveOpacity; this.EdgeOpacity = (ssdp.EdgeStrength ?? 0.25f) * ssdp.EffectiveOpacity; - this.Color = ssdp.Color ?? ImGui.GetColorU32(ImGuiCol.Text); this.EdgeColor = ssdp.EdgeColor ?? 0xFF000000; this.ShadowColor = ssdp.ShadowColor ?? 0xFF000000; - this.LinkHoverBackColor = ssdp.LinkHoverBackColor ?? ImGui.GetColorU32(ImGuiCol.ButtonHovered); - this.LinkActiveBackColor = ssdp.LinkActiveBackColor ?? ImGui.GetColorU32(ImGuiCol.ButtonActive); this.ForceEdgeColor = ssdp.ForceEdgeColor; - this.ThemeIndex = ssdp.ThemeIndex ?? AtkStage.Instance()->AtkUIColorHolder->ActiveColorThemeType; this.Bold = ssdp.Bold; this.Italic = ssdp.Italic; this.Edge = ssdp.Edge; this.Shadow = ssdp.Shadow; + + this.ColorStackSet.Initialize(ref this); + fragments.Clear(); } /// @@ -135,7 +167,7 @@ public unsafe ref struct SeStringDrawState /// Gets a value indicating whether the edge should be drawn. public readonly bool ShouldDrawEdge => - (this.Edge || this.colorStackSet.HasAdditionalEdgeColor) && this.EdgeColor >= 0x1000000; + (this.Edge || this.ColorStackSet.HasAdditionalEdgeColor) && this.EdgeColor >= 0x1000000; /// Gets a value indicating whether the edge should be drawn. public readonly bool ShouldDrawShadow => this is { Shadow: true, ShadowColor: >= 0x1000000 }; @@ -143,11 +175,17 @@ public unsafe ref struct SeStringDrawState /// Gets a value indicating whether the edge should be drawn. public readonly bool ShouldDrawForeground => this is { Color: >= 0x1000000 }; + /// Gets the color stacks. + internal SeStringColorStackSet ColorStackSet { get; } + + /// Gets the text fragments. + internal List Fragments { get; } + /// Sets the current channel in the ImGui draw list splitter. /// Channel to switch to. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly void SetCurrentChannel(SeStringDrawChannel channelIndex) => - this.splitter->SetCurrentChannel(this.drawList, (int)channelIndex); + public void SetCurrentChannel(SeStringDrawChannel channelIndex) => + this.splitter.SetCurrentChannel(this.drawList, (int)channelIndex); /// Draws a single texture. /// ImGui texture ID to draw from. @@ -216,7 +254,7 @@ public unsafe ref struct SeStringDrawState /// Draws a single glyph using current styling configurations. /// Glyph to draw. /// Offset of the glyph in pixels w.r.t. . - internal readonly void DrawGlyph(scoped in ImGuiHelpers.ImFontGlyphReal g, Vector2 offset) + internal void DrawGlyph(scoped in ImGuiHelpers.ImFontGlyphReal g, Vector2 offset) { var texId = this.Font->ContainerAtlas->Textures.Ref(g.TextureIndex).TexID; var xy0 = new Vector2( @@ -268,7 +306,7 @@ public unsafe ref struct SeStringDrawState /// Offset of the glyph in pixels w.r.t. /// . /// Advance width of the glyph. - internal readonly void DrawLinkUnderline(Vector2 offset, float advanceWidth) + internal void DrawLinkUnderline(Vector2 offset, float advanceWidth) { if (this.LinkUnderlineThickness < 1f) return; @@ -350,15 +388,15 @@ public unsafe ref struct SeStringDrawState switch (payload.MacroCode) { case MacroCode.Color: - this.colorStackSet.HandleColorPayload(ref this, payload); + this.ColorStackSet.HandleColorPayload(ref this, payload); return true; case MacroCode.EdgeColor: - this.colorStackSet.HandleEdgeColorPayload(ref this, payload); + this.ColorStackSet.HandleEdgeColorPayload(ref this, payload); return true; case MacroCode.ShadowColor: - this.colorStackSet.HandleShadowColorPayload(ref this, payload); + this.ColorStackSet.HandleShadowColorPayload(ref this, payload); return true; case MacroCode.Bold when payload.TryGetExpression(out var e) && e.TryGetUInt(out var u): @@ -379,11 +417,11 @@ public unsafe ref struct SeStringDrawState return true; case MacroCode.ColorType: - this.colorStackSet.HandleColorTypePayload(ref this, payload); + this.ColorStackSet.HandleColorTypePayload(ref this, payload); return true; case MacroCode.EdgeColorType: - this.colorStackSet.HandleEdgeColorTypePayload(ref this, payload); + this.ColorStackSet.HandleEdgeColorTypePayload(ref this, payload); return true; default: @@ -393,10 +431,9 @@ public unsafe ref struct SeStringDrawState /// Splits the draw list. [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal readonly void SplitDrawList() => - this.splitter->Split(this.drawList, ChannelCount); + internal void SplitDrawList() => this.splitter.Split(this.drawList, ChannelCount); /// Merges the draw list. [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal readonly void MergeDrawList() => this.splitter->Merge(this.drawList); + internal void MergeDrawList() => this.splitter.Merge(this.drawList); } diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index 64e1acaa4..af78c5b0c 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -533,6 +533,13 @@ internal class DalamudInterface : IInternalDisposableService this.creditsDarkeningAnimation.Restart(); } + /// + public T GetDataWindowWidget() where T : IDataWindowWidget => this.dataWindow.GetWidget(); + + /// Sets the data window current widget. + /// Widget to set current. + public void SetDataWindowWidget(IDataWindowWidget widget) => this.dataWindow.CurrentWidget = widget; + private void OnDraw() { this.FrameCount++; diff --git a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs index ae86958dd..eb0589d59 100644 --- a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs +++ b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs @@ -68,7 +68,7 @@ internal class DataWindow : Window, IDisposable private bool isExcept; private bool selectionCollapsed; - private IDataWindowWidget currentWidget; + private bool isLoaded; /// @@ -82,9 +82,12 @@ internal class DataWindow : Window, IDisposable this.RespectCloseHotkey = false; this.orderedModules = this.modules.OrderBy(module => module.DisplayName); - this.currentWidget = this.orderedModules.First(); + this.CurrentWidget = this.orderedModules.First(); } + /// Gets or sets the current widget. + public IDataWindowWidget CurrentWidget { get; set; } + /// public void Dispose() => this.modules.OfType().AggregateToDisposable().Dispose(); @@ -99,6 +102,20 @@ internal class DataWindow : Window, IDisposable { } + /// Gets the data window widget of the specified type. + /// Type of the data window widget to find. + /// Found widget. + public T GetWidget() where T : IDataWindowWidget + { + foreach (var m in this.modules) + { + if (m is T w) + return w; + } + + throw new ArgumentException($"No widget of type {typeof(T).FullName} found."); + } + /// /// Set the DataKind dropdown menu. /// @@ -110,7 +127,7 @@ internal class DataWindow : Window, IDisposable if (this.modules.FirstOrDefault(module => module.IsWidgetCommand(dataKind)) is { } targetModule) { - this.currentWidget = targetModule; + this.CurrentWidget = targetModule; } else { @@ -153,9 +170,9 @@ internal class DataWindow : Window, IDisposable { foreach (var widget in this.orderedModules) { - if (ImGui.Selectable(widget.DisplayName, this.currentWidget == widget)) + if (ImGui.Selectable(widget.DisplayName, this.CurrentWidget == widget)) { - this.currentWidget = widget; + this.CurrentWidget = widget; } } @@ -206,9 +223,9 @@ internal class DataWindow : Window, IDisposable try { - if (this.currentWidget is { Ready: true }) + if (this.CurrentWidget is { Ready: true }) { - this.currentWidget.Draw(); + this.CurrentWidget.Draw(); } else { diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/FontAwesomeTestWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/FontAwesomeTestWidget.cs index 91f1af98e..4f5540daf 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/FontAwesomeTestWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/FontAwesomeTestWidget.cs @@ -1,9 +1,15 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Numerics; +using System.Threading.Tasks; using Dalamud.Bindings.ImGui; +using Dalamud.Interface.Components; +using Dalamud.Interface.Textures.Internal; using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Internal; + +using Lumina.Text.ReadOnly; namespace Dalamud.Interface.Internal.Windows.Data.Widgets; @@ -87,12 +93,30 @@ internal class FontAwesomeTestWidget : IDataWindowWidget ImGuiHelpers.ScaledDummy(10f); for (var i = 0; i < this.icons?.Count; i++) { + if (this.icons[i] == FontAwesomeIcon.None) + continue; + + ImGui.AlignTextToFramePadding(); ImGui.Text($"0x{(int)this.icons[i].ToIconChar():X}"); ImGuiHelpers.ScaledRelativeSameLine(50f); ImGui.Text($"{this.iconNames?[i]}"); ImGuiHelpers.ScaledRelativeSameLine(280f); ImGui.PushFont(this.useFixedWidth ? InterfaceManager.IconFontFixedWidth : InterfaceManager.IconFont); ImGui.Text(this.icons[i].ToIconString()); + ImGuiHelpers.ScaledRelativeSameLine(320f); + if (this.useFixedWidth + ? ImGui.Button($"{(char)this.icons[i]}##FontAwesomeIconButton{i}") + : ImGuiComponents.IconButton($"##FontAwesomeIconButton{i}", this.icons[i])) + { + _ = Service.Get().ShowTextureSaveMenuAsync( + this.DisplayName, + this.icons[i].ToString(), + Task.FromResult( + Service.Get().CreateTextureFromSeString( + ReadOnlySeString.FromText(this.icons[i].ToIconString()), + new() { Font = ImGui.GetFont(), FontSize = ImGui.GetFontSize() }))); + } + ImGui.PopFont(); ImGuiHelpers.ScaledDummy(2f); } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringRendererTestWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringRendererTestWidget.cs index 7ff5a63be..0f51e0322 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringRendererTestWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringRendererTestWidget.cs @@ -1,5 +1,6 @@ using System.Numerics; using System.Text; +using System.Threading.Tasks; using Dalamud.Bindings.ImGui; using Dalamud.Data; @@ -9,11 +10,13 @@ using Dalamud.Interface.ImGuiSeStringRenderer; using Dalamud.Interface.ImGuiSeStringRenderer.Internal; using Dalamud.Interface.Textures.Internal; using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Internal; using Dalamud.Storage.Assets; using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Component.GUI; using Lumina.Excel.Sheets; using Lumina.Text; +using Lumina.Text.Parse; using Lumina.Text.Payloads; using Lumina.Text.ReadOnly; @@ -56,11 +59,11 @@ internal unsafe class SeStringRendererTestWidget : IDataWindowWidget /// public void Draw() { - var t2 = ImGui.ColorConvertU32ToFloat4(this.style.Color ?? ImGui.GetColorU32(ImGuiCol.Text)); + var t2 = ImGui.ColorConvertU32ToFloat4(this.style.Color ??= ImGui.GetColorU32(ImGuiCol.Text)); if (ImGui.ColorEdit4("Color", ref t2)) this.style.Color = ImGui.ColorConvertFloat4ToU32(t2); - t2 = ImGui.ColorConvertU32ToFloat4(this.style.EdgeColor ?? 0xFF000000u); + t2 = ImGui.ColorConvertU32ToFloat4(this.style.EdgeColor ??= 0xFF000000u); if (ImGui.ColorEdit4("Edge Color", ref t2)) this.style.EdgeColor = ImGui.ColorConvertFloat4ToU32(t2); @@ -69,27 +72,27 @@ internal unsafe class SeStringRendererTestWidget : IDataWindowWidget if (ImGui.Checkbox("Forced"u8, ref t)) this.style.ForceEdgeColor = t; - t2 = ImGui.ColorConvertU32ToFloat4(this.style.ShadowColor ?? 0xFF000000u); - if (ImGui.ColorEdit4("Shadow Color", ref t2)) + t2 = ImGui.ColorConvertU32ToFloat4(this.style.ShadowColor ??= 0xFF000000u); + if (ImGui.ColorEdit4("Shadow Color"u8, ref t2)) this.style.ShadowColor = ImGui.ColorConvertFloat4ToU32(t2); - t2 = ImGui.ColorConvertU32ToFloat4(this.style.LinkHoverBackColor ?? ImGui.GetColorU32(ImGuiCol.ButtonHovered)); - if (ImGui.ColorEdit4("Link Hover Color", ref t2)) + t2 = ImGui.ColorConvertU32ToFloat4(this.style.LinkHoverBackColor ??= ImGui.GetColorU32(ImGuiCol.ButtonHovered)); + if (ImGui.ColorEdit4("Link Hover Color"u8, ref t2)) this.style.LinkHoverBackColor = ImGui.ColorConvertFloat4ToU32(t2); - t2 = ImGui.ColorConvertU32ToFloat4(this.style.LinkActiveBackColor ?? ImGui.GetColorU32(ImGuiCol.ButtonActive)); - if (ImGui.ColorEdit4("Link Active Color", ref t2)) + t2 = ImGui.ColorConvertU32ToFloat4(this.style.LinkActiveBackColor ??= ImGui.GetColorU32(ImGuiCol.ButtonActive)); + if (ImGui.ColorEdit4("Link Active Color"u8, ref t2)) this.style.LinkActiveBackColor = ImGui.ColorConvertFloat4ToU32(t2); - var t3 = this.style.LineHeight ?? 1f; + var t3 = this.style.LineHeight ??= 1f; if (ImGui.DragFloat("Line Height"u8, ref t3, 0.01f, 0.4f, 3f, "%.02f")) this.style.LineHeight = t3; - t3 = this.style.Opacity ?? ImGui.GetStyle().Alpha; + t3 = this.style.Opacity ??= 1f; if (ImGui.DragFloat("Opacity"u8, ref t3, 0.005f, 0f, 1f, "%.02f")) this.style.Opacity = t3; - t3 = this.style.EdgeStrength ?? 0.25f; + t3 = this.style.EdgeStrength ??= 0.25f; if (ImGui.DragFloat("Edge Strength"u8, ref t3, 0.005f, 0f, 1f, "%.02f")) this.style.EdgeStrength = t3; @@ -240,6 +243,27 @@ internal unsafe class SeStringRendererTestWidget : IDataWindowWidget Service.Get().CompileAndCache(this.testString).Data.Span)); } + ImGui.SameLine(); + + if (ImGui.Button("Copy as Image")) + { + _ = Service.Get().ShowTextureSaveMenuAsync( + this.DisplayName, + $"From {nameof(SeStringRendererTestWidget)}", + Task.FromResult( + Service.Get().CreateTextureFromSeString( + ReadOnlySeString.FromMacroString( + this.testString, + new(ExceptionMode: MacroStringParseExceptionMode.EmbedError)), + this.style with + { + Font = ImGui.GetFont(), + FontSize = ImGui.GetFontSize(), + WrapWidth = ImGui.GetContentRegionAvail().X, + ThemeIndex = AtkStage.Instance()->AtkUIColorHolder->ActiveColorThemeType, + }))); + } + ImGuiHelpers.ScaledDummy(3); ImGuiHelpers.CompileSeStringWrapped( "Optional features implemented for the following test input:
" + diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs index 52fa0e822..3416a2506 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Numerics; @@ -306,12 +306,12 @@ internal class TexWidget : IDataWindowWidget pres->Release(); ImGui.Text($"RC: Resource({rcres})/View({rcsrv})"); - ImGui.Text(source.ToString()); + ImGui.Text($"{source.Width} x {source.Height} | {source}"); } else { - ImGui.Text("RC: -"u8); - ImGui.Text(" "u8); + ImGui.Text("RC: -"); + ImGui.Text(string.Empty); } } @@ -342,6 +342,10 @@ internal class TexWidget : IDataWindowWidget runLater?.Invoke(); } + /// Adds a texture wrap for debug display purposes. + /// Task returning a texture. + public void AddTexture(Task textureTask) => this.addedTextures.Add(new(Api10: textureTask)); + private unsafe void DrawBlame(List allBlames) { var im = Service.Get(); diff --git a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs index e3eb22a04..69cdc4d28 100644 --- a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs +++ b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs @@ -86,7 +86,8 @@ internal class TitleScreenMenuWindow : Window, IDisposable : base( "TitleScreenMenuOverlay", ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoScrollbar | - ImGuiWindowFlags.NoBackground | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoNavFocus) + ImGuiWindowFlags.NoBackground | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoNavFocus | + ImGuiWindowFlags.NoDocking) { this.showTsm = consoleManager.AddVariable("dalamud.show_tsm", "Show the Title Screen Menu", true); diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs index 2853aa4d2..be2f5a742 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System.Threading; +using System.Threading.Tasks; using Dalamud.Bindings.ImGui; @@ -33,10 +34,22 @@ public interface IFontHandle : IDisposable ///
/// /// Use directly if you want to keep the current ImGui font if the font is not ready.
- /// Alternatively, use to wait for this property to become true. + /// Alternatively, use to wait for this property to become true. ///
bool Available { get; } + /// + /// Attempts to lock the fully constructed instance of corresponding to the this + /// , for use in any thread.
+ /// Modification of the font will exhibit undefined behavior if some other thread also uses the font. + ///
+ /// The error message, if any. + /// + /// An instance of that must be disposed after use on success; + /// null with populated on failure. + /// + ILockedImFont? TryLock(out string? errorMessage); + /// /// Locks the fully constructed instance of corresponding to the this /// , for use in any thread.
@@ -92,4 +105,11 @@ public interface IFontHandle : IDisposable ///
/// A task containing this . Task WaitAsync(); + + /// + /// Waits for to become true. + /// + /// The cancellation token. + /// A task containing this . + Task WaitAsync(CancellationToken cancellationToken); } diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs index 1fdaf4596..98a823deb 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs @@ -238,12 +238,17 @@ internal abstract class FontHandle : IFontHandle } /// - public Task WaitAsync() + public Task WaitAsync() => this.WaitAsync(CancellationToken.None); + + /// + public Task WaitAsync(CancellationToken cancellationToken) { if (this.Available) return Task.FromResult(this); var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + cancellationToken.Register(() => tcs.TrySetCanceled()); + this.ImFontChanged += OnImFontChanged; this.Disposed += OnDisposed; if (this.Available) diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.FromSeString.cs b/Dalamud/Interface/Textures/Internal/TextureManager.FromSeString.cs new file mode 100644 index 000000000..3e90ae3ea --- /dev/null +++ b/Dalamud/Interface/Textures/Internal/TextureManager.FromSeString.cs @@ -0,0 +1,35 @@ +using Dalamud.Interface.ImGuiSeStringRenderer; +using Dalamud.Interface.ImGuiSeStringRenderer.Internal; +using Dalamud.Interface.Textures.TextureWraps; +using Dalamud.Utility; + +namespace Dalamud.Interface.Textures.Internal; + +/// Service responsible for loading and disposing ImGui texture wraps. +internal sealed partial class TextureManager +{ + [ServiceManager.ServiceDependency] + private readonly SeStringRenderer seStringRenderer = Service.Get(); + + /// + public IDalamudTextureWrap CreateTextureFromSeString( + ReadOnlySpan text, + scoped in SeStringDrawParams drawParams = default, + string? debugName = null) + { + ThreadSafety.AssertMainThread(); + using var dd = this.seStringRenderer.CreateDrawData(text, drawParams); + var texture = this.CreateDrawListTexture(debugName ?? nameof(this.CreateTextureFromSeString)); + try + { + texture.Size = dd.Data.DisplaySize; + texture.Draw(dd.DataPtr); + return texture; + } + catch + { + texture.Dispose(); + throw; + } + } +} diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.cs b/Dalamud/Interface/Textures/Internal/TextureManager.cs index 059c716ce..d0f0d8c07 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManager.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Dalamud.Configuration.Internal; using Dalamud.Data; using Dalamud.Game; +using Dalamud.Interface.ImGuiSeStringRenderer.Internal; using Dalamud.Interface.Internal; using Dalamud.Interface.Textures.Internal.SharedImmediateTextures; using Dalamud.Interface.Textures.TextureWraps; @@ -248,7 +249,7 @@ internal sealed partial class TextureManager usage = D3D11_USAGE.D3D11_USAGE_DYNAMIC; else usage = D3D11_USAGE.D3D11_USAGE_DEFAULT; - + using var texture = this.device.CreateTexture2D( new() { diff --git a/Dalamud/Interface/Textures/Internal/TextureManagerPluginScoped.cs b/Dalamud/Interface/Textures/Internal/TextureManagerPluginScoped.cs index 9b0fa0943..ac6de7dd7 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManagerPluginScoped.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManagerPluginScoped.cs @@ -6,6 +6,7 @@ using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; +using Dalamud.Interface.ImGuiSeStringRenderer; using Dalamud.Interface.Internal; using Dalamud.Interface.Textures.TextureWraps; using Dalamud.IoC; @@ -283,6 +284,18 @@ internal sealed class TextureManagerPluginScoped return textureWrap; } + /// + public IDalamudTextureWrap CreateTextureFromSeString( + ReadOnlySpan text, + scoped in SeStringDrawParams drawParams = default, + string? debugName = null) + { + var manager = this.ManagerOrThrow; + var textureWrap = manager.CreateTextureFromSeString(text, drawParams, debugName); + manager.Blame(textureWrap, this.plugin); + return textureWrap; + } + /// public IEnumerable GetSupportedImageDecoderInfos() => this.ManagerOrThrow.Wic.GetSupportedDecoderInfos(); diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index e38537018..1ea0d9f2f 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Diagnostics; +using System.Threading; using System.Threading.Tasks; using Dalamud.Bindings.ImGui; @@ -691,13 +692,14 @@ public sealed class UiBuilder : IDisposable, IUiBuilder FontAtlasAutoRebuildMode autoRebuildMode, bool isGlobalScaled = true, string? debugName = null) => - this.scopedFinalizer.Add(Service - .Get() - .CreateFontAtlas( - this.namespaceName + ":" + (debugName ?? "custom"), - autoRebuildMode, - isGlobalScaled, - this.plugin)); + this.scopedFinalizer.Add( + Service + .Get() + .CreateFontAtlas( + this.namespaceName + ":" + (debugName ?? "custom"), + autoRebuildMode, + isGlobalScaled, + this.plugin)); /// /// Unregister the UiBuilder. Do not call this in plugin code. @@ -868,6 +870,15 @@ public sealed class UiBuilder : IDisposable, IUiBuilder // Note: do not dispose w; we do not own it } + public ILockedImFont? TryLock(out string? errorMessage) + { + if (this.wrapped is { } w) + return w.TryLock(out errorMessage); + + errorMessage = nameof(ObjectDisposedException); + return null; + } + public ILockedImFont Lock() => this.wrapped?.Lock() ?? throw new ObjectDisposedException(nameof(FontHandleWrapper)); @@ -876,7 +887,13 @@ public sealed class UiBuilder : IDisposable, IUiBuilder public void Pop() => this.WrappedNotDisposed.Pop(); public Task WaitAsync() => - this.WrappedNotDisposed.WaitAsync().ContinueWith(_ => (IFontHandle)this); + this.wrapped?.WaitAsync().ContinueWith(_ => (IFontHandle)this) + ?? Task.FromException(new ObjectDisposedException(nameof(FontHandleWrapper))); + + public Task WaitAsync(CancellationToken cancellationToken) => + this.wrapped?.WaitAsync(cancellationToken) + .ContinueWith(_ => (IFontHandle)this, cancellationToken) + ?? Task.FromException(new ObjectDisposedException(nameof(FontHandleWrapper))); public override string ToString() => $"{nameof(FontHandleWrapper)}({this.wrapped?.ToString() ?? "disposed"})"; diff --git a/Dalamud/Interface/Utility/BufferBackedImDrawData.cs b/Dalamud/Interface/Utility/BufferBackedImDrawData.cs new file mode 100644 index 000000000..e6128992a --- /dev/null +++ b/Dalamud/Interface/Utility/BufferBackedImDrawData.cs @@ -0,0 +1,92 @@ +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +using Dalamud.Bindings.ImGui; + +namespace Dalamud.Interface.Utility; + +/// Wrapper aroundx containing one . +public unsafe struct BufferBackedImDrawData : IDisposable +{ + private nint buffer; + + /// Initializes a new instance of the struct. + /// Address of buffer to use. + private BufferBackedImDrawData(nint buffer) => this.buffer = buffer; + + /// Gets the stored in this buffer. + public readonly ref ImDrawData Data => ref ((DataStruct*)this.buffer)->Data; + + /// Gets the stored in this buffer. + public readonly ImDrawDataPtr DataPtr => new((ImDrawData*)Unsafe.AsPointer(ref this.Data)); + + /// Gets the stored in this buffer. + public readonly ref ImDrawList List => ref ((DataStruct*)this.buffer)->List; + + /// Gets the stored in this buffer. + public readonly ImDrawListPtr ListPtr => new((ImDrawList*)Unsafe.AsPointer(ref this.List)); + + /// Creates a new instance of . + /// A new instance of . + public static BufferBackedImDrawData Create() + { + if (ImGui.GetCurrentContext().IsNull || ImGui.GetIO().FontDefault.Handle is null) + throw new("ImGui is not ready"); + + var res = new BufferBackedImDrawData(Marshal.AllocHGlobal(sizeof(DataStruct))); + var ds = (DataStruct*)res.buffer; + *ds = default; + + var atlas = ImGui.GetIO().Fonts; + ds->SharedData = *ImGui.GetDrawListSharedData().Handle; + ds->SharedData.TexIdCommon = atlas.Textures[atlas.TextureIndexCommon].TexID; + ds->SharedData.TexUvWhitePixel = atlas.TexUvWhitePixel; + ds->SharedData.TexUvLines = (Vector4*)Unsafe.AsPointer(ref atlas.TexUvLines[0]); + ds->SharedData.Font = ImGui.GetIO().FontDefault; + ds->SharedData.FontSize = ds->SharedData.Font->FontSize; + ds->SharedData.ClipRectFullscreen = new( + float.NegativeInfinity, + float.NegativeInfinity, + float.PositiveInfinity, + float.PositiveInfinity); + + ds->List.Data = &ds->SharedData; + ds->ListPtr = &ds->List; + ds->Data.CmdLists = &ds->ListPtr; + ds->Data.CmdListsCount = 1; + ds->Data.FramebufferScale = Vector2.One; + + res.ListPtr._ResetForNewFrame(); + res.ListPtr.PushClipRectFullScreen(); + res.ListPtr.PushTextureID(new(atlas.TextureIndexCommon)); + return res; + } + + /// Updates the statistics information stored in from . + public readonly void UpdateDrawDataStatistics() + { + this.Data.TotalIdxCount = this.List.IdxBuffer.Size; + this.Data.TotalVtxCount = this.List.VtxBuffer.Size; + } + + /// + public void Dispose() + { + if (this.buffer != 0) + { + this.ListPtr._ClearFreeMemory(); + Marshal.FreeHGlobal(this.buffer); + this.buffer = 0; + } + } + + [StructLayout(LayoutKind.Sequential)] + private struct DataStruct + { + public ImDrawData Data; + public ImDrawList* ListPtr; + public ImDrawList List; + public ImDrawListSharedData SharedData; + } +} diff --git a/Dalamud/Interface/Utility/ImGuiHelpers.cs b/Dalamud/Interface/Utility/ImGuiHelpers.cs index 87f258250..ee2840d3d 100644 --- a/Dalamud/Interface/Utility/ImGuiHelpers.cs +++ b/Dalamud/Interface/Utility/ImGuiHelpers.cs @@ -575,6 +575,15 @@ public static partial class ImGuiHelpers public static unsafe ImFontPtr OrElse(this ImFontPtr self, ImFontPtr other) => self.IsNull ? other : self; + /// Creates a draw data that will draw the given SeString onto it. + /// SeString to render. + /// Initial rendering style. + /// A new self-contained draw data. + internal static BufferBackedImDrawData CreateDrawData( + ReadOnlySpan sss, + scoped in SeStringDrawParams style = default) => + Service.Get().CreateDrawData(sss, style); + /// /// Mark 4K page as used, after adding a codepoint to a font. /// diff --git a/Dalamud/Interface/Utility/Internal/DevTextureSaveMenu.cs b/Dalamud/Interface/Utility/Internal/DevTextureSaveMenu.cs index 86435e8c1..4a0137c88 100644 --- a/Dalamud/Interface/Utility/Internal/DevTextureSaveMenu.cs +++ b/Dalamud/Interface/Utility/Internal/DevTextureSaveMenu.cs @@ -6,10 +6,12 @@ using System.Text; using System.Threading.Tasks; using Dalamud.Bindings.ImGui; +using Dalamud.Game; using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.Internal; +using Dalamud.Interface.Internal.Windows.Data.Widgets; using Dalamud.Interface.Textures.Internal; using Dalamud.Interface.Textures.TextureWraps; using Serilog; @@ -33,6 +35,14 @@ internal sealed class DevTextureSaveMenu : IInternalDisposableService this.interfaceManager.Draw += this.InterfaceManagerOnDraw; } + private enum ContextMenuActionType + { + None, + SaveAsFile, + CopyToClipboard, + SendToTexWidget, + } + /// void IInternalDisposableService.DisposeService() => this.interfaceManager.Draw -= this.InterfaceManagerOnDraw; @@ -66,15 +76,16 @@ internal sealed class DevTextureSaveMenu : IInternalDisposableService var textureManager = await Service.GetAsync(); var popupName = $"{nameof(this.ShowTextureSaveMenuAsync)}_{textureWrap.Handle.Handle:X}"; + ContextMenuActionType action; BitmapCodecInfo? encoder; { var first = true; var encoders = textureManager.Wic.GetSupportedEncoderInfos().ToList(); - var tcs = new TaskCompletionSource( + var tcs = new TaskCompletionSource<(ContextMenuActionType Action, BitmapCodecInfo? Codec)>( TaskCreationOptions.RunContinuationsAsynchronously); Service.Get().Draw += DrawChoices; - encoder = await tcs.Task; + (action, encoder) = await tcs.Task; [SuppressMessage("ReSharper", "AccessToDisposedClosure", Justification = "This shall not escape")] void DrawChoices() @@ -98,13 +109,20 @@ internal sealed class DevTextureSaveMenu : IInternalDisposableService } if (ImGui.Selectable("Copy"u8)) - tcs.TrySetResult(null); + tcs.TrySetResult((ContextMenuActionType.CopyToClipboard, null)); + if (ImGui.Selectable("Send to TexWidget"u8)) + tcs.TrySetResult((ContextMenuActionType.SendToTexWidget, null)); + + ImGui.Separator(); + foreach (var encoder2 in encoders) { if (ImGui.Selectable(encoder2.Name)) - tcs.TrySetResult(encoder2); + tcs.TrySetResult((ContextMenuActionType.SaveAsFile, encoder2)); } + ImGui.Separator(); + const float previewImageWidth = 320; var size = textureWrap.Size; if (size.X > previewImageWidth) @@ -120,50 +138,68 @@ internal sealed class DevTextureSaveMenu : IInternalDisposableService } } - if (encoder is null) + switch (action) { - isCopy = true; - await textureManager.CopyToClipboardAsync(textureWrap, name, true); - } - else - { - var props = new Dictionary(); - if (encoder.ContainerGuid == GUID.GUID_ContainerFormatTiff) - props["CompressionQuality"] = 1.0f; - else if (encoder.ContainerGuid == GUID.GUID_ContainerFormatJpeg || - encoder.ContainerGuid == GUID.GUID_ContainerFormatHeif || - encoder.ContainerGuid == GUID.GUID_ContainerFormatWmp) - props["ImageQuality"] = 1.0f; + case ContextMenuActionType.CopyToClipboard: + isCopy = true; + await textureManager.CopyToClipboardAsync(textureWrap, name, true); + break; - var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - this.fileDialogManager.SaveFileDialog( - "Save texture...", - $"{encoder.Name.Replace(',', '.')}{{{string.Join(',', encoder.Extensions)}}}", - name + encoder.Extensions.First(), - encoder.Extensions.First(), - (ok, path2) => - { - if (!ok) - tcs.SetCanceled(); - else - tcs.SetResult(path2); - }); - var path = await tcs.Task.ConfigureAwait(false); - - await textureManager.SaveToFileAsync(textureWrap, encoder.ContainerGuid, path, props: props); - - var notif = Service.Get().AddNotification( - new() - { - Content = $"File saved to: {path}", - Title = initiatorName, - Type = NotificationType.Success, - }); - notif.Click += n => + case ContextMenuActionType.SendToTexWidget: { - Process.Start(new ProcessStartInfo(path) { UseShellExecute = true }); - n.Notification.DismissNow(); - }; + var framework = await Service.GetAsync(); + var dalamudInterface = await Service.GetAsync(); + await framework.RunOnFrameworkThread( + () => + { + var texWidget = dalamudInterface.GetDataWindowWidget(); + dalamudInterface.SetDataWindowWidget(texWidget); + texWidget.AddTexture(Task.FromResult(textureWrap.CreateWrapSharingLowLevelResource())); + }); + break; + } + + case ContextMenuActionType.SaveAsFile when encoder is not null: + { + var props = new Dictionary(); + if (encoder.ContainerGuid == GUID.GUID_ContainerFormatTiff) + props["CompressionQuality"] = 1.0f; + else if (encoder.ContainerGuid == GUID.GUID_ContainerFormatJpeg || + encoder.ContainerGuid == GUID.GUID_ContainerFormatHeif || + encoder.ContainerGuid == GUID.GUID_ContainerFormatWmp) + props["ImageQuality"] = 1.0f; + + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + this.fileDialogManager.SaveFileDialog( + "Save texture...", + $"{encoder.Name.Replace(',', '.')}{{{string.Join(',', encoder.Extensions)}}}", + name + encoder.Extensions.First(), + encoder.Extensions.First(), + (ok, path2) => + { + if (!ok) + tcs.SetCanceled(); + else + tcs.SetResult(path2); + }); + var path = await tcs.Task.ConfigureAwait(false); + + await textureManager.SaveToFileAsync(textureWrap, encoder.ContainerGuid, path, props: props); + + var notif = Service.Get().AddNotification( + new() + { + Content = $"File saved to: {path}", + Title = initiatorName, + Type = NotificationType.Success, + }); + notif.Click += n => + { + Process.Start(new ProcessStartInfo(path) { UseShellExecute = true }); + n.Notification.DismissNow(); + }; + break; + } } } catch (Exception e) diff --git a/Dalamud/Interface/Windowing/Window.cs b/Dalamud/Interface/Windowing/Window.cs index e90e38119..b0786fbb5 100644 --- a/Dalamud/Interface/Windowing/Window.cs +++ b/Dalamud/Interface/Windowing/Window.cs @@ -1,9 +1,6 @@ using System.Collections.Generic; using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Numerics; -using System.Runtime.InteropServices; using System.Threading.Tasks; using CheapLoc; @@ -19,10 +16,13 @@ using Dalamud.Interface.Utility.Internal; using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing.Persistence; using Dalamud.Logging.Internal; -using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.UI; +using TerraFX.Interop.Windows; + +using static TerraFX.Interop.Windows.Windows; + namespace Dalamud.Interface.Windowing; /// @@ -31,11 +31,15 @@ namespace Dalamud.Interface.Windowing; public abstract class Window { private const float FadeInOutTime = 0.072f; + private const string AdditionsPopupName = "WindowSystemContextActions"; private static readonly ModuleLog Log = new("WindowSystem"); private static bool wasEscPressedLastFrame = false; + private readonly TitleBarButton additionsButton; + private readonly List allButtons = []; + private bool internalLastIsOpen = false; private bool internalIsOpen = false; private bool internalIsPinned = false; @@ -72,6 +76,20 @@ public abstract class Window this.WindowName = name; this.Flags = flags; this.ForceMainWindow = forceMainWindow; + + this.additionsButton = new() + { + Icon = FontAwesomeIcon.Bars, + IconOffset = new Vector2(2.5f, 1), + Click = _ => + { + this.internalIsClickthrough = false; + this.presetDirty = false; + ImGui.OpenPopup(AdditionsPopupName); + }, + Priority = int.MinValue, + AvailableClickthrough = true, + }; } /// @@ -482,14 +500,12 @@ public abstract class Window ImGuiP.GetCurrentWindow().InheritNoInputs = this.internalIsClickthrough; } - // Not supported yet on non-main viewports - if ((this.internalIsPinned || this.internalIsClickthrough || this.internalAlpha.HasValue) && - ImGui.GetWindowViewport().ID != ImGui.GetMainViewport().ID) + if (ImGui.GetWindowViewport().ID != ImGui.GetMainViewport().ID) { - this.internalAlpha = null; - this.internalIsPinned = false; - this.internalIsClickthrough = false; - this.presetDirty = true; + if ((flags & ImGuiWindowFlags.NoInputs) == ImGuiWindowFlags.NoInputs) + ImGui.GetWindowViewport().Flags |= ImGuiViewportFlags.NoInputs; + else + ImGui.GetWindowViewport().Flags &= ~ImGuiViewportFlags.NoInputs; } if (this.hasError) @@ -513,7 +529,6 @@ public abstract class Window } } - const string additionsPopupName = "WindowSystemContextActions"; var flagsApplicableForTitleBarIcons = !flags.HasFlag(ImGuiWindowFlags.NoDecoration) && !flags.HasFlag(ImGuiWindowFlags.NoTitleBar); var showAdditions = (this.AllowPinning || this.AllowClickthrough) && @@ -524,13 +539,8 @@ public abstract class Window { ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 1f); - if (ImGui.BeginPopup(additionsPopupName, ImGuiWindowFlags.NoMove)) + if (ImGui.BeginPopup(AdditionsPopupName, ImGuiWindowFlags.NoMove)) { - var isAvailable = ImGuiHelpers.CheckIsWindowOnMainViewport(); - - if (!isAvailable) - ImGui.BeginDisabled(); - if (this.internalIsClickthrough) ImGui.BeginDisabled(); @@ -578,21 +588,11 @@ public abstract class Window this.presetDirty = true; } - if (isAvailable) - { - ImGui.TextColored(ImGuiColors.DalamudGrey, - Loc.Localize("WindowSystemContextActionClickthroughDisclaimer", - "Open this menu again by clicking the three dashes to disable clickthrough.")); - } - else - { - ImGui.TextColored(ImGuiColors.DalamudGrey, - Loc.Localize("WindowSystemContextActionViewportDisclaimer", - "These features are only available if this window is inside the game window.")); - } - - if (!isAvailable) - ImGui.EndDisabled(); + ImGui.TextColored( + ImGuiColors.DalamudGrey, + Loc.Localize( + "WindowSystemContextActionClickthroughDisclaimer", + "Open this menu again by clicking the three dashes to disable clickthrough.")); if (ImGui.Button(Loc.Localize("WindowSystemContextActionPrintWindow", "Print window"))) printWindow = true; @@ -603,34 +603,15 @@ public abstract class Window ImGui.PopStyleVar(); } - unsafe + if (flagsApplicableForTitleBarIcons) { - var window = ImGuiP.GetCurrentWindow(); - - ImRect outRect; - ImGuiP.TitleBarRect(&outRect, window); - - var additionsButton = new TitleBarButton - { - Icon = FontAwesomeIcon.Bars, - IconOffset = new Vector2(2.5f, 1), - Click = _ => - { - this.internalIsClickthrough = false; - this.presetDirty = false; - ImGui.OpenPopup(additionsPopupName); - }, - Priority = int.MinValue, - AvailableClickthrough = true, - }; - - if (flagsApplicableForTitleBarIcons) - { - this.DrawTitleBarButtons(window, flags, outRect, - showAdditions - ? this.TitleBarButtons.Append(additionsButton) - : this.TitleBarButtons); - } + this.allButtons.Clear(); + this.allButtons.EnsureCapacity(this.TitleBarButtons.Count + 1); + this.allButtons.AddRange(this.TitleBarButtons); + if (showAdditions) + this.allButtons.Add(this.additionsButton); + this.allButtons.Sort(static (a, b) => b.Priority - a.Priority); + this.DrawTitleBarButtons(); } if (wasFocused) @@ -797,8 +778,11 @@ public abstract class Window } } - private unsafe void DrawTitleBarButtons(ImGuiWindowPtr window, ImGuiWindowFlags flags, ImRect titleBarRect, IEnumerable buttons) + private unsafe void DrawTitleBarButtons() { + var window = ImGuiP.GetCurrentWindow(); + var flags = window.Flags; + var titleBarRect = window.TitleBarRect(); ImGui.PushClipRect(ImGui.GetWindowPos(), ImGui.GetWindowPos() + ImGui.GetWindowSize(), false); var style = ImGui.GetStyle(); @@ -833,26 +817,22 @@ public abstract class Window var max = pos + new Vector2(fontSize, fontSize); ImRect bb = new(pos, max); var isClipped = !ImGuiP.ItemAdd(bb, id, null, 0); - bool hovered, held; - var pressed = false; + bool hovered, held, pressed; if (this.internalIsClickthrough) { - hovered = false; - held = false; - // ButtonBehavior does not function if the window is clickthrough, so we have to do it ourselves - if (ImGui.IsMouseHoveringRect(pos, max)) - { - hovered = true; + var pad = ImGui.GetStyle().TouchExtraPadding; + var rect = new ImRect(pos - pad, max + pad); + hovered = rect.Contains(ImGui.GetMousePos()); - // We can't use ImGui native functions here, because they don't work with clickthrough - if ((Windows.Win32.PInvoke.GetKeyState((int)VirtualKey.LBUTTON) & 0x8000) != 0) - { - held = true; - pressed = true; - } - } + // Temporarily enable inputs + // This will be reset on next frame, and then enabled again if it is still being hovered + if (hovered && ImGui.GetWindowViewport().ID != ImGui.GetMainViewport().ID) + ImGui.GetWindowViewport().Flags &= ~ImGuiViewportFlags.NoInputs; + + // We can't use ImGui native functions here, because they don't work with clickthrough + pressed = held = hovered && (GetKeyState(VK.VK_LBUTTON) & 0x8000) != 0; } else { @@ -881,7 +861,7 @@ public abstract class Window return pressed; } - foreach (var button in buttons.OrderBy(x => x.Priority)) + foreach (var button in this.allButtons) { if (this.internalIsClickthrough && !button.AvailableClickthrough) return; @@ -1035,7 +1015,7 @@ public abstract class Window /// /// Gets or sets an action that is called when the button is clicked. /// - public Action Click { get; set; } + public Action? Click { get; set; } /// /// Gets or sets the priority the button shall be shown in. diff --git a/Dalamud/Networking/Rpc/Model/DalamudUri.cs b/Dalamud/Networking/Rpc/Model/DalamudUri.cs new file mode 100644 index 000000000..852478762 --- /dev/null +++ b/Dalamud/Networking/Rpc/Model/DalamudUri.cs @@ -0,0 +1,102 @@ +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Web; + +namespace Dalamud.Networking.Rpc.Model; + +/// +/// 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/Rpc/RpcConnection.cs b/Dalamud/Networking/Rpc/RpcConnection.cs new file mode 100644 index 000000000..5288948eb --- /dev/null +++ b/Dalamud/Networking/Rpc/RpcConnection.cs @@ -0,0 +1,95 @@ +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +using Dalamud.Networking.Rpc.Service; + +using Serilog; + +using StreamJsonRpc; + +namespace Dalamud.Networking.Rpc; + +/// +/// A single RPC client session connected via a stream (named pipe or Unix socket). +/// +internal class RpcConnection : IDisposable +{ + private readonly Stream stream; + private readonly RpcServiceRegistry registry; + private readonly CancellationTokenSource cts = new(); + + /// + /// Initializes a new instance of the class. + /// + /// The stream that this connection will handle. + /// A registry of RPC services. + public RpcConnection(Stream stream, RpcServiceRegistry registry) + { + this.Id = Guid.CreateVersion7(); + this.stream = stream; + this.registry = registry; + + var formatter = new JsonMessageFormatter(); + var handler = new HeaderDelimitedMessageHandler(stream, stream, 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.stream.Dispose(); + } + catch (Exception ex) + { + Log.Debug(ex, "Error disposing stream 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/Rpc/RpcHostService.cs b/Dalamud/Networking/Rpc/RpcHostService.cs new file mode 100644 index 000000000..bbe9dc8eb --- /dev/null +++ b/Dalamud/Networking/Rpc/RpcHostService.cs @@ -0,0 +1,91 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; + +using Dalamud.Logging.Internal; +using Dalamud.Networking.Rpc.Transport; + +namespace Dalamud.Networking.Rpc; + +/// +/// The Dalamud service repsonsible for hosting the RPC. +/// +[ServiceManager.EarlyLoadedService] +internal class RpcHostService : IServiceType, IInternalDisposableService +{ + private readonly ModuleLog log = new("RPC"); + private readonly RpcServiceRegistry registry = new(); + private readonly List transports = []; + + /// + /// Initializes a new instance of the class. + /// + [ServiceManager.ServiceConstructor] + public RpcHostService() + { + this.StartUnixTransport(); + + if (this.transports.Count == 0) + { + this.log.Warning("No RPC hosts could be started on this platform"); + } + } + + /// + /// Gets all active RPC transports. + /// + public IReadOnlyList Transports => this.transports; + + /// + /// Add a new service Object to the RPC host. + /// + /// The object to add. + public void AddService(object service) => this.registry.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.registry.AddMethod(name, handler); + + /// + public void DisposeService() + { + foreach (var host in this.transports) + { + host.Dispose(); + } + + this.transports.Clear(); + } + + /// + public async Task InvokeClientAsync(Guid clientId, string method, params object[] arguments) + { + var clients = this.transports.SelectMany(t => t.Connections).ToImmutableDictionary(); + + if (!clients.TryGetValue(clientId, out var session)) + throw new KeyNotFoundException($"No client {clientId}"); + + return await session.Rpc.InvokeAsync(method, arguments).ConfigureAwait(false); + } + + /// + public async Task BroadcastNotifyAsync(string method, params object[] arguments) + { + await foreach (var transport in this.transports.ToAsyncEnumerable().ConfigureAwait(false)) + { + await transport.BroadcastNotifyAsync(method, arguments).ConfigureAwait(false); + } + } + + private void StartUnixTransport() + { + var transport = new UnixRpcTransport(this.registry); + this.transports.Add(transport); + transport.Start(); + this.log.Information("RpcHostService listening to UNIX socket: {Socket}", transport.SocketPath); + } +} diff --git a/Dalamud/Networking/Rpc/RpcServiceRegistry.cs b/Dalamud/Networking/Rpc/RpcServiceRegistry.cs new file mode 100644 index 000000000..6daea14bf --- /dev/null +++ b/Dalamud/Networking/Rpc/RpcServiceRegistry.cs @@ -0,0 +1,85 @@ +using System.Collections.Generic; +using System.Threading; + +using StreamJsonRpc; + +namespace Dalamud.Networking.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/Networking/Rpc/Service/ClientHelloService.cs b/Dalamud/Networking/Rpc/Service/ClientHelloService.cs new file mode 100644 index 000000000..c5a4c851a --- /dev/null +++ b/Dalamud/Networking/Rpc/Service/ClientHelloService.cs @@ -0,0 +1,133 @@ +using System.Diagnostics; +using System.Threading.Tasks; + +using Dalamud.Data; +using Dalamud.Game; +using Dalamud.Game.ClientState; +using Dalamud.Utility; + +using Lumina.Excel.Sheets; + +namespace Dalamud.Networking.Rpc.Service; + +/// +/// 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", + ProcessId = Environment.ProcessId, + ProcessStartTime = new DateTimeOffset(Process.GetCurrentProcess().StartTime).ToUnixTimeSeconds(), + ClientState = 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 the process ID of this client. + /// + public int? ProcessId { get; init; } + + /// + /// Gets the time this process started. + /// + public long? ProcessStartTime { get; init; } + + /// + /// Gets a state for this client for user display. + /// + public string? ClientState { get; init; } +} diff --git a/Dalamud/Networking/Rpc/Service/LinkHandlerService.cs b/Dalamud/Networking/Rpc/Service/LinkHandlerService.cs new file mode 100644 index 000000000..9fa311ede --- /dev/null +++ b/Dalamud/Networking/Rpc/Service/LinkHandlerService.cs @@ -0,0 +1,107 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; + +using Dalamud.Logging.Internal; +using Dalamud.Networking.Rpc.Model; +using Dalamud.Utility; + +namespace Dalamud.Networking.Rpc.Service; + +/// +/// 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/Rpc/Service/Links/DebugLinkHandler.cs b/Dalamud/Networking/Rpc/Service/Links/DebugLinkHandler.cs new file mode 100644 index 000000000..269617fc0 --- /dev/null +++ b/Dalamud/Networking/Rpc/Service/Links/DebugLinkHandler.cs @@ -0,0 +1,67 @@ +using Dalamud.Game.Gui.Toast; +using Dalamud.Interface.ImGuiNotification; +using Dalamud.Interface.ImGuiNotification.Internal; +using Dalamud.Networking.Rpc.Model; + +namespace Dalamud.Networking.Rpc.Service.Links; + +#if DEBUG + +/// +/// A debug controller for link handling. +/// +[ServiceManager.EarlyLoadedService] +internal sealed class DebugLinkHandler : IInternalDisposableService +{ + private readonly LinkHandlerService linkHandlerService; + + /// + /// Initializes a new instance of the class. + /// + /// Injected LinkHandler. + [ServiceManager.ServiceConstructor] + public DebugLinkHandler(LinkHandlerService linkHandler) + { + this.linkHandlerService = linkHandler; + + this.linkHandlerService.Register("debug", this.HandleLink); + } + + /// + public void DisposeService() + { + this.linkHandlerService.Unregister("debug", this.HandleLink); + } + + private void HandleLink(DalamudUri uri) + { + var action = uri.Path.Split("/").GetValue(1)?.ToString(); + switch (action) + { + case "toast": + this.ShowToast(uri); + break; + case "notification": + this.ShowNotification(uri); + break; + } + } + + private void ShowToast(DalamudUri uri) + { + var message = uri.QueryParams.Get("message") ?? "Hello, world!"; + Service.Get().ShowNormal(message); + } + + private void ShowNotification(DalamudUri uri) + { + Service.Get().AddNotification( + new Notification + { + Title = uri.QueryParams.Get("title"), + Content = uri.QueryParams.Get("content") ?? "Hello, world!", + }); + } +} + +#endif diff --git a/Dalamud/Networking/Rpc/Service/Links/PluginLinkHandler.cs b/Dalamud/Networking/Rpc/Service/Links/PluginLinkHandler.cs new file mode 100644 index 000000000..3b7f18437 --- /dev/null +++ b/Dalamud/Networking/Rpc/Service/Links/PluginLinkHandler.cs @@ -0,0 +1,57 @@ +using System.Linq; + +using Dalamud.Console; +using Dalamud.IoC; +using Dalamud.IoC.Internal; +using Dalamud.Networking.Rpc.Model; +using Dalamud.Plugin.Internal.Types; +using Dalamud.Plugin.Services; + +#pragma warning disable DAL_RPC + +namespace Dalamud.Networking.Rpc.Service.Links; + +/// +[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("/").ElementAtOrDefault(1); + 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/Rpc/Transport/IRpcTransport.cs b/Dalamud/Networking/Rpc/Transport/IRpcTransport.cs new file mode 100644 index 000000000..ad7578eb4 --- /dev/null +++ b/Dalamud/Networking/Rpc/Transport/IRpcTransport.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Dalamud.Networking.Rpc.Transport; + +/// +/// Interface for RPC host implementations (named pipes or Unix sockets). +/// +internal interface IRpcTransport : IDisposable +{ + /// + /// Gets a list of active RPC connections. + /// + IReadOnlyDictionary Connections { get; } + + /// Starts accepting client connections. + void Start(); + + /// 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. + Task InvokeClientAsync(Guid clientId, string method, params object[] 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. + Task BroadcastNotifyAsync(string method, params object[] arguments); +} diff --git a/Dalamud/Networking/Rpc/Transport/UnixRpcTransport.cs b/Dalamud/Networking/Rpc/Transport/UnixRpcTransport.cs new file mode 100644 index 000000000..17da51444 --- /dev/null +++ b/Dalamud/Networking/Rpc/Transport/UnixRpcTransport.cs @@ -0,0 +1,207 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; + +using Dalamud.Logging.Internal; +using Dalamud.Utility; + +namespace Dalamud.Networking.Rpc.Transport; + +/// +/// Simple multi-client JSON-RPC Unix socket host using StreamJsonRpc. +/// +internal class UnixRpcTransport : IRpcTransport +{ + private readonly ModuleLog log = new("RPC/Transport/UnixSocket"); + + private readonly RpcServiceRegistry registry; + private readonly CancellationTokenSource cts = new(); + private readonly ConcurrentDictionary sessions = new(); + private readonly string? cleanupSocketDirectory; + + private Task? acceptLoopTask; + private Socket? listenSocket; + + /// + /// Initializes a new instance of the class. + /// + /// The RPC service registry to use. + /// The Unix socket directory to use. If null, defaults to Dalamud home directory. + /// The name of the socket to create. + public UnixRpcTransport(RpcServiceRegistry registry, string? socketDirectory = null, string? socketName = null) + { + this.registry = registry; + socketName ??= $"DalamudRPC.{Environment.ProcessId}.sock"; + + if (!socketDirectory.IsNullOrEmpty()) + { + this.SocketPath = Path.Combine(socketDirectory, socketName); + } + else + { + socketDirectory = Service.Get().StartInfo.TempDirectory; + + if (socketDirectory == null) + { + this.SocketPath = Path.Combine(Path.GetTempPath(), socketName); + this.log.Warning("Temp dir was not set in StartInfo; using system temp for unix socket."); + } + else + { + this.SocketPath = Path.Combine(socketDirectory, socketName); + this.cleanupSocketDirectory = socketDirectory; + } + } + } + + /// + /// Gets the path of the Unix socket this RPC host is using. + /// + public string SocketPath { get; } + + /// + public IReadOnlyDictionary Connections => this.sessions; + + /// Starts accepting client connections. + public void Start() + { + if (this.acceptLoopTask != null) return; + + // Make the directory for the socket if it doesn't exist + var socketDir = Path.GetDirectoryName(this.SocketPath); + if (!string.IsNullOrEmpty(socketDir) && !Directory.Exists(socketDir)) + { + this.log.Error("Directory for unix socket does not exist: {Path}", socketDir); + return; + } + + // Delete existing socket for this PID, if it exists. + if (File.Exists(this.SocketPath)) + { + try + { + File.Delete(this.SocketPath); + } + catch (Exception ex) + { + this.log.Warning(ex, "Failed to delete existing socket file: {Path}", this.SocketPath); + } + } + + 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); + } + + /// + public void Dispose() + { + this.cts.Cancel(); + this.acceptLoopTask?.Wait(1000); + + foreach (var kv in this.sessions) + { + kv.Value.Dispose(); + } + + this.sessions.Clear(); + + this.listenSocket?.Dispose(); + + if (File.Exists(this.SocketPath)) + { + try + { + File.Delete(this.SocketPath); + } + catch (Exception ex) + { + this.log.Warning(ex, "Failed to delete socket file on dispose: {Path}", this.SocketPath); + } + } + + this.cts.Dispose(); + this.log.Information("UnixRpcHost disposed ({Socket})", this.SocketPath); + GC.SuppressFinalize(this); + } + + private async Task AcceptLoopAsync() + { + var token = this.cts.Token; + + try + { + var endpoint = new UnixDomainSocketEndPoint(this.SocketPath); + this.listenSocket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified); + this.listenSocket.Bind(endpoint); + this.listenSocket.Listen(128); + + while (!token.IsCancellationRequested) + { + Socket? clientSocket = null; + try + { + clientSocket = await this.listenSocket.AcceptAsync(token).ConfigureAwait(false); + + var stream = new NetworkStream(clientSocket, ownsSocket: true); + var session = new RpcConnection(stream, 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) + { + clientSocket?.Dispose(); + break; + } + catch (Exception ex) + { + clientSocket?.Dispose(); + this.log.Error(ex, "Error in socket accept loop"); + await Task.Delay(500, token).ConfigureAwait(false); + } + } + } + catch (Exception ex) + { + this.log.Error(ex, "Fatal error in Unix socket accept loop"); + } + } +} diff --git a/Dalamud/Plugin/Services/IPluginLinkHandler.cs b/Dalamud/Plugin/Services/IPluginLinkHandler.cs new file mode 100644 index 000000000..37101222a --- /dev/null +++ b/Dalamud/Plugin/Services/IPluginLinkHandler.cs @@ -0,0 +1,24 @@ +using System.Diagnostics.CodeAnalysis; + +using Dalamud.Networking.Rpc.Model; + +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 : IDalamudService +{ + /// + /// A delegate containing the received URI. + /// + /// The URI opened by the user. + public delegate void PluginUriReceived(DalamudUri uri); + + /// + /// The event fired when a URI targeting this plugin is received. + /// + event PluginUriReceived OnUriReceived; +} diff --git a/Dalamud/Plugin/SelfTest/ISelfTestRegistry.cs b/Dalamud/Plugin/Services/ISelfTestRegistry.cs similarity index 92% rename from Dalamud/Plugin/SelfTest/ISelfTestRegistry.cs rename to Dalamud/Plugin/Services/ISelfTestRegistry.cs index af3b583c9..50d3d35ce 100644 --- a/Dalamud/Plugin/SelfTest/ISelfTestRegistry.cs +++ b/Dalamud/Plugin/Services/ISelfTestRegistry.cs @@ -1,6 +1,8 @@ using System.Collections.Generic; -namespace Dalamud.Plugin.SelfTest; +using Dalamud.Plugin.SelfTest; + +namespace Dalamud.Plugin.Services; /// /// Interface for registering and unregistering self-test steps from plugins. @@ -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/ISigScanner.cs b/Dalamud/Plugin/Services/ISigScanner.cs index fbbd8b05a..017c4fe9d 100644 --- a/Dalamud/Plugin/Services/ISigScanner.cs +++ b/Dalamud/Plugin/Services/ISigScanner.cs @@ -2,9 +2,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Threading; -using Dalamud.Plugin.Services; - -namespace Dalamud.Game; +namespace Dalamud.Plugin.Services; /// /// A SigScanner facilitates searching for memory signatures in a given ProcessModule. diff --git a/Dalamud/Plugin/Services/ITargetManager.cs b/Dalamud/Plugin/Services/ITargetManager.cs index 9c9fce550..0c14571c5 100644 --- a/Dalamud/Plugin/Services/ITargetManager.cs +++ b/Dalamud/Plugin/Services/ITargetManager.cs @@ -1,7 +1,7 @@ using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Plugin.Services; -namespace Dalamud.Game.ClientState.Objects; +namespace Dalamud.Plugin.Services; /// /// Get and set various kinds of targets for the player. @@ -37,13 +37,13 @@ public interface ITargetManager : IDalamudService /// Set to null to clear the target. /// public IGameObject? SoftTarget { get; set; } - + /// /// Gets or sets the gpose target. /// Set to null to clear the target. /// public IGameObject? GPoseTarget { get; set; } - + /// /// Gets or sets the mouseover nameplate target. /// Set to null to clear the target. diff --git a/Dalamud/Plugin/Services/ITextureProvider.cs b/Dalamud/Plugin/Services/ITextureProvider.cs index 7cd1b7c86..dc0522aa8 100644 --- a/Dalamud/Plugin/Services/ITextureProvider.cs +++ b/Dalamud/Plugin/Services/ITextureProvider.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Reflection; @@ -6,6 +6,7 @@ using System.Threading; using System.Threading.Tasks; using Dalamud.Bindings.ImGui; +using Dalamud.Interface.ImGuiSeStringRenderer; using Dalamud.Interface.Internal.Windows.Data.Widgets; using Dalamud.Interface.Textures; using Dalamud.Interface.Textures.TextureWraps; @@ -186,6 +187,17 @@ public interface ITextureProvider : IDalamudService string? debugName = null, CancellationToken cancellationToken = default); + /// Creates a texture by drawing a SeString onto it. + /// SeString to render. + /// Parameters for drawing. + /// Name for debug display purposes. + /// The new texture. + /// Can be only be used from the main thread. + public IDalamudTextureWrap CreateTextureFromSeString( + ReadOnlySpan text, + scoped in SeStringDrawParams drawParams = default, + string? debugName = null); + /// Gets the supported bitmap decoders. /// The supported bitmap decoders. /// diff --git a/Directory.Build.props b/Directory.Build.props index f9f061c17..eabb727e8 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/Directory.Packages.props b/Directory.Packages.props index a1cef517e..903a8ee88 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,65 +1,68 @@ - - true - false - + + true + false + - - - - - + + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - + + + + + - - - - - + + + + + - - - + + + - - + + - - - - - + + + + + - - - - - - - - - - - - + + + + + + + + + + + + + + + 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); }); diff --git a/build/build.csproj b/build/build.csproj index b4aaa959d..1e1416d92 100644 --- a/build/build.csproj +++ b/build/build.csproj @@ -12,6 +12,7 @@ - + + 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/Dalamud.Bindings.ImGui/ImVector.cs b/imgui/Dalamud.Bindings.ImGui/ImVector.cs index 9a10c1d6b..67e450193 100644 --- a/imgui/Dalamud.Bindings.ImGui/ImVector.cs +++ b/imgui/Dalamud.Bindings.ImGui/ImVector.cs @@ -1,7 +1,12 @@ -using System.Runtime.CompilerServices; +using System.Collections; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; namespace Dalamud.Bindings.ImGui; +/// +/// A structure representing a dynamic array for unmanaged types. +/// public unsafe struct ImVector { public readonly int Size; @@ -15,23 +20,23 @@ public unsafe struct ImVector Data = data; } - public ref T Ref(int index) - { - return ref Unsafe.AsRef((byte*)Data + index * Unsafe.SizeOf()); - } + public readonly ref T Ref(int index) => ref Unsafe.AsRef((byte*)this.Data + (index * Unsafe.SizeOf())); - public IntPtr Address(int index) - { - return (IntPtr)((byte*)Data + index * Unsafe.SizeOf()); - } + public readonly nint Address(int index) => (nint)((byte*)this.Data + (index * Unsafe.SizeOf())); } /// /// A structure representing a dynamic array for unmanaged types. /// /// The type of elements in the vector, must be unmanaged. -public unsafe struct ImVector where T : unmanaged +[StructLayout(LayoutKind.Sequential)] +public unsafe struct ImVector : IEnumerable + where T : unmanaged { + private int size; + private int capacity; + private T* data; + /// /// Initializes a new instance of the struct with the specified size, capacity, and data pointer. /// @@ -45,11 +50,6 @@ public unsafe struct ImVector where T : unmanaged this.data = data; } - private int size; - private int capacity; - private unsafe T* data; - - /// /// Gets or sets the element at the specified index. /// @@ -58,80 +58,72 @@ public unsafe struct ImVector where T : unmanaged /// Thrown when the index is out of range. public T this[int index] { - get + readonly get { - if (index < 0 || index >= size) - { + if (index < 0 || index >= this.size) throw new IndexOutOfRangeException(); - } - return data[index]; + return this.data[index]; } set { - if (index < 0 || index >= size) - { + if (index < 0 || index >= this.size) throw new IndexOutOfRangeException(); - } - data[index] = value; + this.data[index] = value; } } /// /// Gets a pointer to the first element of the vector. /// - public readonly T* Data => data; + public readonly T* Data => this.data; /// /// Gets a pointer to the first element of the vector. /// - public readonly T* Front => data; + public readonly T* Front => this.data; /// /// Gets a pointer to the last element of the vector. /// - public readonly T* Back => size > 0 ? data + size - 1 : null; + public readonly T* Back => this.size > 0 ? this.data + this.size - 1 : null; /// /// Gets or sets the capacity of the vector. /// public int Capacity { - readonly get => capacity; + readonly get => this.capacity; set { - if (capacity == value) - { + ArgumentOutOfRangeException.ThrowIfLessThan(value, this.size, nameof(Capacity)); + if (this.capacity == value) return; - } - if (data == null) + if (this.data == null) { - data = (T*)ImGui.MemAlloc((nuint)(value * sizeof(T))); + this.data = (T*)ImGui.MemAlloc((nuint)(value * sizeof(T))); } else { - int newSize = Math.Min(size, value); - T* newData = (T*)ImGui.MemAlloc((nuint)(value * sizeof(T))); - Buffer.MemoryCopy(data, newData, (nuint)(value * sizeof(T)), (nuint)(newSize * sizeof(T))); - ImGui.MemFree(data); - data = newData; - size = newSize; + var newSize = Math.Min(this.size, value); + var newData = (T*)ImGui.MemAlloc((nuint)(value * sizeof(T))); + Buffer.MemoryCopy(this.data, newData, (nuint)(value * sizeof(T)), (nuint)(newSize * sizeof(T))); + ImGui.MemFree(this.data); + this.data = newData; + this.size = newSize; } - capacity = value; + this.capacity = value; // Clear the rest of the data - for (int i = size; i < capacity; i++) - { - data[i] = default; - } + new Span(this.data + this.size, this.capacity - this.size).Clear(); } } /// /// Gets the number of elements in the vector. /// - public readonly int Size => size; + public readonly int Size => this.size; /// /// Grows the capacity of the vector to at least the specified value. @@ -139,10 +131,8 @@ public unsafe struct ImVector where T : unmanaged /// The new capacity. public void Grow(int newCapacity) { - if (newCapacity > capacity) - { - Capacity = newCapacity * 2; - } + var newCapacity2 = this.capacity > 0 ? this.capacity + (this.capacity / 2) : 8; + this.Capacity = newCapacity2 > newCapacity ? newCapacity2 : newCapacity; } /// @@ -151,10 +141,8 @@ public unsafe struct ImVector where T : unmanaged /// The minimum capacity required. public void EnsureCapacity(int size) { - if (size > capacity) - { + if (size > this.capacity) Grow(size); - } } /// @@ -164,25 +152,46 @@ public unsafe struct ImVector where T : unmanaged public void Resize(int newSize) { EnsureCapacity(newSize); - size = newSize; + this.size = newSize; } /// /// Clears all elements from the vector. /// - public void Clear() + public void Clear() => this.size = 0; + + /// + /// Adds an element to the end of the vector. + /// + /// The value to add. + [OverloadResolutionPriority(1)] + public void PushBack(T value) { - size = 0; + this.EnsureCapacity(this.size + 1); + this.data[this.size++] = value; } /// /// Adds an element to the end of the vector. /// /// The value to add. - public void PushBack(T value) + [OverloadResolutionPriority(2)] + public void PushBack(in T value) { - EnsureCapacity(size + 1); - data[size++] = value; + EnsureCapacity(this.size + 1); + this.data[this.size++] = value; + } + + /// + /// Adds an element to the front of the vector. + /// + /// The value to add. + public void PushFront(in T value) + { + if (this.size == 0) + this.PushBack(value); + else + this.Insert(0, value); } /// @@ -190,48 +199,126 @@ public unsafe struct ImVector where T : unmanaged /// public void PopBack() { - if (size > 0) + if (this.size > 0) { - size--; + this.size--; } } + public ref T Insert(int index, in T v) { + ArgumentOutOfRangeException.ThrowIfNegative(index, nameof(index)); + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, this.size, nameof(index)); + this.EnsureCapacity(this.size + 1); + if (index < this.size) + { + Buffer.MemoryCopy( + this.data + index, + this.data + index + 1, + (this.size - index) * sizeof(T), + (this.size - index) * sizeof(T)); + } + + this.data[index] = v; + this.size++; + return ref this.data[index]; + } + + public Span InsertRange(int index, ReadOnlySpan v) + { + ArgumentOutOfRangeException.ThrowIfNegative(index, nameof(index)); + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, this.size, nameof(index)); + this.EnsureCapacity(this.size + v.Length); + if (index < this.size) + { + Buffer.MemoryCopy( + this.data + index, + this.data + index + v.Length, + (this.size - index) * sizeof(T), + (this.size - index) * sizeof(T)); + } + + var dstSpan = new Span(this.data + index, v.Length); + v.CopyTo(new(this.data + index, v.Length)); + this.size += v.Length; + return dstSpan; + } + /// /// Frees the memory allocated for the vector. /// public void Free() { - if (data != null) + if (this.data != null) { - ImGui.MemFree(data); - data = null; - size = 0; - capacity = 0; + ImGui.MemFree(this.data); + this.data = null; + this.size = 0; + this.capacity = 0; } } - public ref T Ref(int index) + public readonly ref T Ref(int index) { - return ref Unsafe.AsRef((byte*)Data + index * Unsafe.SizeOf()); + return ref Unsafe.AsRef((byte*)Data + (index * Unsafe.SizeOf())); } - public ref TCast Ref(int index) + public readonly ref TCast Ref(int index) { - return ref Unsafe.AsRef((byte*)Data + index * Unsafe.SizeOf()); + return ref Unsafe.AsRef((byte*)Data + (index * Unsafe.SizeOf())); } - public void* Address(int index) + public readonly void* Address(int index) { - return (byte*)Data + index * Unsafe.SizeOf(); + return (byte*)Data + (index * Unsafe.SizeOf()); } - public void* Address(int index) + public readonly void* Address(int index) { - return (byte*)Data + index * Unsafe.SizeOf(); + return (byte*)Data + (index * Unsafe.SizeOf()); } - public ImVector* ToUntyped() + public readonly ImVector* ToUntyped() { - return (ImVector*)Unsafe.AsPointer(ref this); + return (ImVector*)Unsafe.AsPointer(ref Unsafe.AsRef(in this)); + } + + public readonly Span AsSpan() => new(this.data, this.size); + + public readonly Enumerator GetEnumerator() => new(this.data, this.data + this.size); + + readonly IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + + readonly IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + + public struct Enumerator(T* begin, T* end) : IEnumerator, IEnumerable + { + private T* current = null; + + public readonly ref T Current => ref *this.current; + + readonly T IEnumerator.Current => this.Current; + + readonly object IEnumerator.Current => this.Current; + + public bool MoveNext() + { + var next = this.current == null ? begin : this.current + 1; + if (next == end) + return false; + this.current = next; + return true; + } + + public void Reset() => this.current = null; + + public readonly Enumerator GetEnumerator() => new(begin, end); + + readonly void IDisposable.Dispose() + { + } + + readonly IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + + readonly IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); } } 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 e5f586630..e5dedba42 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit e5f586630ef06fa48d5dc0d8c0fa679323093c77 +Subproject commit e5dedba42a3fea8f050ea54ac583a5874bf51c6f diff --git a/lib/cimgui b/lib/cimgui index 27c8565f6..bc3272967 160000 --- a/lib/cimgui +++ b/lib/cimgui @@ -1 +1 @@ -Subproject commit 27c8565f631b004c3266373890e41ecc627f775b +Subproject commit bc327296758d57d3bdc963cb6ce71dd5b0c7e54c