From 9d4bc95f7d648ec6d986a1eb7d6b620d421b434c Mon Sep 17 00:00:00 2001 From: goat Date: Thu, 14 Nov 2024 01:08:07 +0100 Subject: [PATCH 01/55] initial net9 port --- Dalamud.Common/Dalamud.Common.csproj | 1 - Dalamud.CorePlugin/Dalamud.CorePlugin.csproj | 2 -- Dalamud.Injector/Dalamud.Injector.csproj | 4 ---- Dalamud.Test/Dalamud.Test.csproj | 8 -------- Dalamud/Dalamud.csproj | 4 ---- Directory.Build.props | 10 +++++++++- build/build.csproj | 1 - global.json | 2 +- tools/Dalamud.LocExporter/Dalamud.LocExporter.csproj | 4 ---- 9 files changed, 10 insertions(+), 26 deletions(-) diff --git a/Dalamud.Common/Dalamud.Common.csproj b/Dalamud.Common/Dalamud.Common.csproj index 594e09021..54a182210 100644 --- a/Dalamud.Common/Dalamud.Common.csproj +++ b/Dalamud.Common/Dalamud.Common.csproj @@ -1,7 +1,6 @@ - net8.0 enable enable diff --git a/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj b/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj index 449d3db68..1cdfda74e 100644 --- a/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj +++ b/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj @@ -1,9 +1,7 @@ Dalamud.CorePlugin - net8.0-windows x64 - 10.0 true false false diff --git a/Dalamud.Injector/Dalamud.Injector.csproj b/Dalamud.Injector/Dalamud.Injector.csproj index 1ff29ea66..7b20c3d21 100644 --- a/Dalamud.Injector/Dalamud.Injector.csproj +++ b/Dalamud.Injector/Dalamud.Injector.csproj @@ -1,11 +1,7 @@ - net8.0 win-x64 - x64 - x64;AnyCPU - 10.0 diff --git a/Dalamud.Test/Dalamud.Test.csproj b/Dalamud.Test/Dalamud.Test.csproj index 28e326238..c6c8f8e8a 100644 --- a/Dalamud.Test/Dalamud.Test.csproj +++ b/Dalamud.Test/Dalamud.Test.csproj @@ -1,13 +1,5 @@ - - net8.0-windows - win-x64 - x64 - x64;AnyCPU - 11.0 - - Dalamud.Test Dalamud.Test diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 8e912fe8f..b844edd31 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -1,10 +1,6 @@ - net8.0-windows - x64 - x64;AnyCPU - 12.0 True diff --git a/Directory.Build.props b/Directory.Build.props index 0c5af2e37..4c039e532 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,5 +1,13 @@ - + + + + net9.0-windows + x64 + x64;AnyCPU + 13.0 + + diff --git a/build/build.csproj b/build/build.csproj index 219b668bd..9222d33ab 100644 --- a/build/build.csproj +++ b/build/build.csproj @@ -1,7 +1,6 @@  Exe - net8.0 disable IDE0002;IDE0051;IDE1006;CS0649;CS0169 diff --git a/global.json b/global.json index 7e8286fbe..ab1a4a2ec 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "8.0.0", + "version": "9.0.0", "rollForward": "latestMinor", "allowPrerelease": true } diff --git a/tools/Dalamud.LocExporter/Dalamud.LocExporter.csproj b/tools/Dalamud.LocExporter/Dalamud.LocExporter.csproj index 5701e706f..aa6cabedc 100644 --- a/tools/Dalamud.LocExporter/Dalamud.LocExporter.csproj +++ b/tools/Dalamud.LocExporter/Dalamud.LocExporter.csproj @@ -2,10 +2,6 @@ Exe - net8.0-windows - x64 - x64;AnyCPU - 12.0 enable enable From 6efa41d33db980e9e56b59c1407428768f676267 Mon Sep 17 00:00:00 2001 From: goat Date: Thu, 14 Nov 2024 01:08:39 +0100 Subject: [PATCH 02/55] use net9 SDK in CI --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 669d6255a..e1fbcac9b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -22,7 +22,7 @@ jobs: uses: microsoft/setup-msbuild@v1.0.2 - uses: actions/setup-dotnet@v3 with: - dotnet-version: '8.0.100' + dotnet-version: '9.0.100' - name: Define VERSION run: | $env:COMMIT = $env:GITHUB_SHA.Substring(0, 7) From a5622f30401b4bf174f5d78d8233717da6f40c68 Mon Sep 17 00:00:00 2001 From: goat Date: Thu, 14 Nov 2024 01:16:51 +0100 Subject: [PATCH 03/55] possibly work around nuke .NET9 issue --- build/build.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/build/build.csproj b/build/build.csproj index 9222d33ab..37a4d3252 100644 --- a/build/build.csproj +++ b/build/build.csproj @@ -11,5 +11,6 @@ + From 89dfd72e24e9e43bea0a6d32c76c0a86aa9e3c56 Mon Sep 17 00:00:00 2001 From: goat <16760685+goaaats@users.noreply.github.com> Date: Sat, 8 Mar 2025 16:13:54 +0100 Subject: [PATCH 04/55] ci: build with 9.0.200 SDK --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2e053e565..e46f6f693 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -22,7 +22,7 @@ jobs: uses: microsoft/setup-msbuild@v1.0.2 - uses: actions/setup-dotnet@v3 with: - dotnet-version: '9.0.100' + dotnet-version: '9.0.200' - name: Define VERSION run: | $env:COMMIT = $env:GITHUB_SHA.Substring(0, 7) From cf1d8a81c6a958de6ac2da21b1fc6d5d480ba567 Mon Sep 17 00:00:00 2001 From: goaaats Date: Sat, 8 Mar 2025 16:27:10 +0100 Subject: [PATCH 05/55] Remove obsolete ServicePointManager TLS version override --- Dalamud/EntryPoint.cs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/Dalamud/EntryPoint.cs b/Dalamud/EntryPoint.cs index 6d5c219dd..89e4e0e5e 100644 --- a/Dalamud/EntryPoint.cs +++ b/Dalamud/EntryPoint.cs @@ -178,20 +178,17 @@ public sealed class EntryPoint throw new Exception("Working directory was invalid"); Reloaded.Hooks.Tools.Utilities.FasmBasePath = new DirectoryInfo(info.WorkingDirectory); - + // Apply common fixes for culture issues CultureFixes.Apply(); - // This is due to GitHub not supporting TLS 1.0, so we enable all TLS versions globally - ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12 | SecurityProtocolType.Tls; - if (!Util.IsWine()) InitSymbolHandler(info); var dalamud = new Dalamud(info, fs, configuration, mainThreadContinueEvent); - Log.Information("This is Dalamud - Core: {GitHash}, CS: {CsGitHash} [{CsVersion}]", - Util.GetScmVersion(), - Util.GetGitHashClientStructs(), + Log.Information("This is Dalamud - Core: {GitHash}, CS: {CsGitHash} [{CsVersion}]", + Util.GetScmVersion(), + Util.GetGitHashClientStructs(), FFXIVClientStructs.ThisAssembly.Git.Commits); dalamud.WaitForUnload(); From b21a0c929839d917f54d0ffcde3438b47858ce27 Mon Sep 17 00:00:00 2001 From: goaaats Date: Sun, 9 Mar 2025 19:34:26 +0100 Subject: [PATCH 06/55] Add console argument/env var to disable legacy corrupted state exceptions policy --- Dalamud.Boot/DalamudStartInfo.cpp | 2 ++ Dalamud.Boot/DalamudStartInfo.h | 3 ++- Dalamud.Boot/dllmain.cpp | 23 ++++++++++++----------- Dalamud.Common/DalamudStartInfo.cs | 7 ++++++- Dalamud.Injector.Boot/main.cpp | 1 + Dalamud.Injector/EntryPoint.cs | 3 +++ lib/CoreCLR/boot.cpp | 10 ++++++++-- lib/CoreCLR/boot.h | 1 + 8 files changed, 35 insertions(+), 15 deletions(-) diff --git a/Dalamud.Boot/DalamudStartInfo.cpp b/Dalamud.Boot/DalamudStartInfo.cpp index 985332966..52f201fea 100644 --- a/Dalamud.Boot/DalamudStartInfo.cpp +++ b/Dalamud.Boot/DalamudStartInfo.cpp @@ -123,6 +123,7 @@ void from_json(const nlohmann::json& json, DalamudStartInfo& config) { config.BootVehEnabled = json.value("BootVehEnabled", config.BootVehEnabled); config.BootVehFull = json.value("BootVehFull", config.BootVehFull); config.BootEnableEtw = json.value("BootEnableEtw", config.BootEnableEtw); + config.BootDisableLegacyCorruptedStateExceptions = json.value("BootDisableLegacyCorruptedStateExceptions", config.BootDisableLegacyCorruptedStateExceptions); config.BootDotnetOpenProcessHookMode = json.value("BootDotnetOpenProcessHookMode", config.BootDotnetOpenProcessHookMode); if (const auto it = json.find("BootEnabledGameFixes"); it != json.end() && it->is_array()) { config.BootEnabledGameFixes.clear(); @@ -148,6 +149,7 @@ void DalamudStartInfo::from_envvars() { BootVehEnabled = utils::get_env(L"DALAMUD_IS_VEH"); BootVehFull = utils::get_env(L"DALAMUD_IS_VEH_FULL"); BootEnableEtw = utils::get_env(L"DALAMUD_ENABLE_ETW"); + BootDisableLegacyCorruptedStateExceptions = utils::get_env(L"DALAMUD_DISABLE_LEGACY_CORRUPTED_STATE_EXCEPTIONS"); BootDotnetOpenProcessHookMode = static_cast(utils::get_env(L"DALAMUD_DOTNET_OPENPROCESS_HOOKMODE")); for (const auto& item : utils::get_env_list(L"DALAMUD_GAMEFIX_LIST")) BootEnabledGameFixes.insert(unicode::convert(item, &unicode::lower)); diff --git a/Dalamud.Boot/DalamudStartInfo.h b/Dalamud.Boot/DalamudStartInfo.h index cc31ba2c5..d5a6a6aec 100644 --- a/Dalamud.Boot/DalamudStartInfo.h +++ b/Dalamud.Boot/DalamudStartInfo.h @@ -17,7 +17,7 @@ struct DalamudStartInfo { DirectHook = 1, }; friend void from_json(const nlohmann::json&, DotNetOpenProcessHookMode&); - + enum class ClientLanguage : int { Japanese, English, @@ -61,6 +61,7 @@ struct DalamudStartInfo { bool BootVehEnabled = false; bool BootVehFull = false; bool BootEnableEtw = false; + bool BootDisableLegacyCorruptedStateExceptions = false; DotNetOpenProcessHookMode BootDotnetOpenProcessHookMode = DotNetOpenProcessHookMode::ImportHooks; std::set BootEnabledGameFixes{}; std::set BootUnhookDlls{}; diff --git a/Dalamud.Boot/dllmain.cpp b/Dalamud.Boot/dllmain.cpp index 5c7c00b68..f608c626e 100644 --- a/Dalamud.Boot/dllmain.cpp +++ b/Dalamud.Boot/dllmain.cpp @@ -11,7 +11,7 @@ HINSTANCE g_hGameInstance = GetModuleHandleW(nullptr); HRESULT WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) { g_startInfo.from_envvars(); - + std::string jsonParseError; try { from_json(nlohmann::json::parse(std::string_view(static_cast(lpParam))), g_startInfo); @@ -21,7 +21,7 @@ HRESULT WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) { if (g_startInfo.BootShowConsole) ConsoleSetup(L"Dalamud Boot"); - + logging::update_dll_load_status(true); const auto logFilePath = unicode::convert(g_startInfo.BootLogPath); @@ -29,16 +29,16 @@ HRESULT WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) { auto attemptFallbackLog = false; if (logFilePath.empty()) { attemptFallbackLog = true; - + logging::I("No log file path given; not logging to file."); } else { try { logging::start_file_logging(logFilePath, !g_startInfo.BootShowConsole); logging::I("Logging to file: {}", logFilePath); - + } catch (const std::exception& e) { attemptFallbackLog = true; - + logging::E("Couldn't open log file: {}", logFilePath); logging::E("Error: {} / {}", errno, e.what()); } @@ -59,15 +59,15 @@ HRESULT WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) { SYSTEMTIME st; GetLocalTime(&st); logFilePath += std::format(L"Dalamud.Boot.{:04}{:02}{:02}.{:02}{:02}{:02}.{:03}.{}.log", st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, st.wMilliseconds, GetCurrentProcessId()); - + try { logging::start_file_logging(logFilePath, !g_startInfo.BootShowConsole); logging::I("Logging to fallback log file: {}", logFilePath); - + } catch (const std::exception& e) { if (!g_startInfo.BootShowConsole && !g_startInfo.BootDisableFallbackConsole) ConsoleSetup(L"Dalamud Boot - Fallback Console"); - + logging::E("Couldn't open fallback log file: {}", logFilePath); logging::E("Error: {} / {}", errno, e.what()); } @@ -83,7 +83,7 @@ HRESULT WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) { } else { logging::E("Failed to initialize MinHook (status={}({}))", MH_StatusToString(mhStatus), static_cast(mhStatus)); } - + logging::I("Dalamud.Boot Injectable, (c) 2021 XIVLauncher Contributors"); logging::I("Built at: " __DATE__ "@" __TIME__); @@ -117,6 +117,7 @@ HRESULT WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) { const auto result = InitializeClrAndGetEntryPoint( g_hModule, g_startInfo.BootEnableEtw, + !g_startInfo.BootDisableLegacyCorruptedStateExceptions, runtimeconfig_path, module_path, L"Dalamud.EntryPoint, Dalamud", @@ -174,11 +175,11 @@ BOOL APIENTRY DllMain(const HMODULE hModule, const DWORD dwReason, LPVOID lpRese case DLL_PROCESS_DETACH: // do not show debug message boxes on abort() here _set_abort_behavior(0, _WRITE_ABORT_MSG); - + // process is terminating; don't bother cleaning up if (lpReserved) return TRUE; - + logging::update_dll_load_status(false); xivfixes::apply_all(false); diff --git a/Dalamud.Common/DalamudStartInfo.cs b/Dalamud.Common/DalamudStartInfo.cs index c3cc33a12..ca81d1281 100644 --- a/Dalamud.Common/DalamudStartInfo.cs +++ b/Dalamud.Common/DalamudStartInfo.cs @@ -120,10 +120,15 @@ public record DalamudStartInfo public bool BootVehFull { get; set; } /// - /// Gets or sets a value indicating whether or not ETW should be enabled. + /// Gets or sets a value indicating whether ETW should be enabled. /// public bool BootEnableEtw { get; set; } + /// + /// Gets or sets a value indicating whether to enable legacy corrupted state exceptions. + /// + public bool BootDisableLegacyCorruptedStateExceptions { get; set; } + /// /// Gets or sets a value choosing the OpenProcess hookmode. /// diff --git a/Dalamud.Injector.Boot/main.cpp b/Dalamud.Injector.Boot/main.cpp index df4120009..50555a09a 100644 --- a/Dalamud.Injector.Boot/main.cpp +++ b/Dalamud.Injector.Boot/main.cpp @@ -27,6 +27,7 @@ int wmain(int argc, wchar_t** argv) const auto result = InitializeClrAndGetEntryPoint( GetModuleHandleW(nullptr), false, + false, runtimeconfig_path, module_path, L"Dalamud.Injector.EntryPoint, Dalamud.Injector", diff --git a/Dalamud.Injector/EntryPoint.cs b/Dalamud.Injector/EntryPoint.cs index 6507436c2..9e7033c4d 100644 --- a/Dalamud.Injector/EntryPoint.cs +++ b/Dalamud.Injector/EntryPoint.cs @@ -94,6 +94,7 @@ namespace Dalamud.Injector args.Remove("--msgbox2"); args.Remove("--msgbox3"); args.Remove("--etw"); + args.Remove("--no-legacy-corrupted-state-exceptions"); args.Remove("--veh"); args.Remove("--veh-full"); args.Remove("--no-plugin"); @@ -399,6 +400,7 @@ namespace Dalamud.Injector // Set boot defaults startInfo.BootShowConsole = args.Contains("--console"); startInfo.BootEnableEtw = args.Contains("--etw"); + startInfo.BootDisableLegacyCorruptedStateExceptions = args.Contains("--no-legacy-corrupted-state-exceptions"); startInfo.BootLogPath = GetLogPath(startInfo.LogPath, "dalamud.boot", startInfo.LogName); startInfo.BootEnabledGameFixes = new() { @@ -470,6 +472,7 @@ namespace Dalamud.Injector Console.WriteLine("Verbose logging:\t[-v]"); Console.WriteLine("Show Console:\t[--console] [--crash-handler-console]"); Console.WriteLine("Enable ETW:\t[--etw]"); + Console.WriteLine("Disable legacy corrupted state exceptions:\t[--no-legacy-corrupted-state-exceptions]"); Console.WriteLine("Enable VEH:\t[--veh], [--veh-full], [--unhandled-exception=default|stalldebug|none]"); Console.WriteLine("Show messagebox:\t[--msgbox1], [--msgbox2], [--msgbox3]"); Console.WriteLine("No plugins:\t[--no-plugin] [--no-3rd-plugin]"); diff --git a/lib/CoreCLR/boot.cpp b/lib/CoreCLR/boot.cpp index 54276aad1..723a1ed21 100644 --- a/lib/CoreCLR/boot.cpp +++ b/lib/CoreCLR/boot.cpp @@ -30,6 +30,7 @@ std::optional g_clr; HRESULT InitializeClrAndGetEntryPoint( void* calling_module, bool enable_etw, + bool enable_legacy_corrupted_state_exception_policy, std::wstring runtimeconfig_path, std::wstring module_path, std::wstring entrypoint_assembly_name, @@ -41,8 +42,13 @@ HRESULT InitializeClrAndGetEntryPoint( int result; SetEnvironmentVariable(L"DOTNET_MULTILEVEL_LOOKUP", L"0"); - SetEnvironmentVariable(L"COMPlus_legacyCorruptedStateExceptionsPolicy", L"1"); - SetEnvironmentVariable(L"DOTNET_legacyCorruptedStateExceptionsPolicy", L"1"); + + if (enable_legacy_corrupted_state_exception_policy) + { + SetEnvironmentVariable(L"COMPlus_legacyCorruptedStateExceptionsPolicy", L"1"); + SetEnvironmentVariable(L"DOTNET_legacyCorruptedStateExceptionsPolicy", L"1"); + } + SetEnvironmentVariable(L"COMPLUS_ForceENC", L"1"); SetEnvironmentVariable(L"DOTNET_ForceENC", L"1"); diff --git a/lib/CoreCLR/boot.h b/lib/CoreCLR/boot.h index 33bc58bbf..b227a3438 100644 --- a/lib/CoreCLR/boot.h +++ b/lib/CoreCLR/boot.h @@ -4,6 +4,7 @@ void ConsoleTeardown(); HRESULT InitializeClrAndGetEntryPoint( void* calling_module, bool enable_etw, + bool enable_legacy_corrupted_state_exception_policy, std::wstring runtimeconfig_path, std::wstring module_path, std::wstring entrypoint_assembly_name, From 97add3fc90cb2eb8763228899ebecd0c9de87af0 Mon Sep 17 00:00:00 2001 From: goaaats Date: Wed, 12 Mar 2025 23:23:31 +0100 Subject: [PATCH 07/55] Globally disable legacy corrupted state exceptions for now --- Dalamud.Boot/dllmain.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud.Boot/dllmain.cpp b/Dalamud.Boot/dllmain.cpp index f608c626e..68f04b84b 100644 --- a/Dalamud.Boot/dllmain.cpp +++ b/Dalamud.Boot/dllmain.cpp @@ -117,7 +117,7 @@ HRESULT WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) { const auto result = InitializeClrAndGetEntryPoint( g_hModule, g_startInfo.BootEnableEtw, - !g_startInfo.BootDisableLegacyCorruptedStateExceptions, + false, // !g_startInfo.BootDisableLegacyCorruptedStateExceptions, runtimeconfig_path, module_path, L"Dalamud.EntryPoint, Dalamud", From 2e2c73f707afd589aeea3e5de11835b9d4dbde53 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Wed, 12 Mar 2025 23:27:12 +0100 Subject: [PATCH 08/55] [net9] Rollup changes from master (#2193) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: handle UNC paths in FileDialog (#2191) * Remove command from assembly map when removed from command map (#2183) --------- Co-authored-by: Alex Vallière <6031413+AlexValliere@users.noreply.github.com> Co-authored-by: Blair Co-authored-by: github-actions[bot] --- Dalamud/Game/Command/CommandManager.cs | 6 ++++++ .../Interface/ImGuiFileDialog/FileDialog.Files.cs | 14 ++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/Dalamud/Game/Command/CommandManager.cs b/Dalamud/Game/Command/CommandManager.cs index 078ce8c50..fdaa5833b 100644 --- a/Dalamud/Game/Command/CommandManager.cs +++ b/Dalamud/Game/Command/CommandManager.cs @@ -10,6 +10,7 @@ using Dalamud.IoC.Internal; using Dalamud.Logging.Internal; using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Services; +using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.System.String; using FFXIVClientStructs.FFXIV.Client.UI; @@ -149,6 +150,11 @@ internal sealed unsafe class CommandManager : IInternalDisposableService, IComma /// public bool RemoveHandler(string command) { + if (this.commandAssemblyNameMap.FindFirst(c => c.Key.Item1 == command, out var assemblyKeyValuePair)) + { + this.commandAssemblyNameMap.TryRemove(assemblyKeyValuePair.Key, out _); + } + return this.commandMap.Remove(command, out _); } diff --git a/Dalamud/Interface/ImGuiFileDialog/FileDialog.Files.cs b/Dalamud/Interface/ImGuiFileDialog/FileDialog.Files.cs index 121ec8890..705c0f100 100644 --- a/Dalamud/Interface/ImGuiFileDialog/FileDialog.Files.cs +++ b/Dalamud/Interface/ImGuiFileDialog/FileDialog.Files.cs @@ -36,6 +36,20 @@ public partial class FileDialog private static string ComposeNewPath(List decomp) { + // Handle UNC paths (network paths) + if (decomp.Count >= 2 && string.IsNullOrEmpty(decomp[0]) && string.IsNullOrEmpty(decomp[1])) + { + var pathParts = new List(decomp); + pathParts.RemoveRange(0, 2); + // Can not access server level or UNC root + if (pathParts.Count <= 1) + { + return string.Empty; + } + + return $"\\\\{string.Join('\\', pathParts)}"; + } + if (decomp.Count == 1) { var drivePath = decomp[0]; From 203d80c602dd814082bced51d1116b983e3c6181 Mon Sep 17 00:00:00 2001 From: Nathan C <39100023+nathanctech@users.noreply.github.com> Date: Thu, 13 Mar 2025 17:01:39 -0400 Subject: [PATCH 09/55] Build adjustments for code annotation (#2155) * Drop special formatting, allowing annotations to work properly. * Suppress duplicate warnings, adding a prefix to prevent annotation * Tweak message, don't rebuild on test * Move testing into same job step * Only run PR build on newly opened PRs * flip build order, derp * Test suppressing summary for annotations... * Get the build order right, testing without conditionals... * Run tests after compile, suppress warnings from test * Reverted previous change to `main.yml`. * Drop special formatting, allowing annotations to work properly. * Suppress duplicate warnings, adding a prefix to prevent annotation * Tweak message, don't rebuild on test * Move testing into same job step * Only run PR build on newly opened PRs * flip build order, derp * Test suppressing summary for annotations... * Get the build order right, testing without conditionals... * Run tests after compile, suppress warnings from test * Reverted previous change to `main.yml`. * Drop special formatting, allowing annotations to work properly. * Suppress duplicate warnings, adding a prefix to prevent annotation * Tweak message, don't rebuild on test * Move testing into same job step * Only run PR build on newly opened PRs * flip build order, derp * Test suppressing summary for annotations... * Get the build order right, testing without conditionals... * Run tests after compile, suppress warnings from test * Reverted previous change to `main.yml`. * Add conditional for CI builds, add --skip-tests to make up for the combined build/test step. * Behavior change, now requires arg `ci` to be passed to trigger tests. Tests can also be manually triggered with `test`. --- .github/workflows/main.yml | 27 +++++++++++++------------ .nuke/build.schema.json | 4 ++++ build/DalamudBuild.cs | 40 +++++++++++++++++++++++++++++++------- 3 files changed, 50 insertions(+), 21 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e46f6f693..be44afacc 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,5 +1,6 @@ name: Build Dalamud on: [push, pull_request, workflow_dispatch] + concurrency: group: build_dalamud_${{ github.ref_name }} cancel-in-progress: true @@ -32,10 +33,8 @@ jobs: ($env:REPO_NAME) >> VERSION ($env:BRANCH) >> VERSION ($env:COMMIT) >> VERSION - - name: Build Dalamud - run: .\build.ps1 compile - - name: Test Dalamud - run: .\build.ps1 test + - name: Build and Test Dalamud + run: .\build.ps1 ci - name: Sign Dalamud if: ${{ github.repository_owner == 'goatcorp' && github.event_name == 'push' }} env: @@ -87,9 +86,9 @@ jobs: - name: "Verify Compatibility" run: | $FILES_TO_VALIDATE = "Dalamud.dll","FFXIVClientStructs.dll","Lumina.dll","Lumina.Excel.dll" - + $retcode = 0 - + foreach ($file in $FILES_TO_VALIDATE) { $testout = "" Write-Output "::group::=== API COMPATIBILITY CHECK: ${file} ===" @@ -100,7 +99,7 @@ jobs: $retcode = 1 } } - + exit $retcode deploy_stg: @@ -129,18 +128,18 @@ jobs: GH_BRANCH: ${{ steps.extract_branch.outputs.branch }} run: | Compress-Archive .\scratch\* .\canary.zip # Recreate the release zip - + $branchName = $env:GH_BRANCH - + if ($branchName -eq "master") { $branchName = "stg" } - + $newVersion = [System.IO.File]::ReadAllText("$(Get-Location)\scratch\TEMP_gitver.txt") $revision = [System.IO.File]::ReadAllText("$(Get-Location)\scratch\revision.txt") $commitHash = [System.IO.File]::ReadAllText("$(Get-Location)\scratch\commit_hash.txt") Remove-Item -Force -Recurse .\scratch - + if (Test-Path -Path $branchName) { $versionData = Get-Content ".\${branchName}\version" | ConvertFrom-Json $oldVersion = $versionData.AssemblyVersion @@ -159,7 +158,7 @@ jobs: Write-Host "Deployment folder doesn't exist. Not doing anything." Remove-Item .\canary.zip } - + - name: Commit changes shell: bash env: @@ -167,8 +166,8 @@ jobs: run: | git config --global user.name "Actions User" git config --global user.email "actions@github.com" - + git add . git commit -m "[CI] Update staging for ${DVER} on ${GH_BRANCH}" || true - + git push origin main || true diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json index e7e1a446a..8331affcc 100644 --- a/.nuke/build.schema.json +++ b/.nuke/build.schema.json @@ -76,6 +76,7 @@ "items": { "type": "string", "enum": [ + "CI", "Clean", "Compile", "CompileCImGui", @@ -88,6 +89,7 @@ "CompileInjector", "CompileInjectorBoot", "Restore", + "SetCILogging", "Test" ] } @@ -102,6 +104,7 @@ "items": { "type": "string", "enum": [ + "CI", "Clean", "Compile", "CompileCImGui", @@ -114,6 +117,7 @@ "CompileInjector", "CompileInjectorBoot", "Restore", + "SetCILogging", "Test" ] } diff --git a/build/DalamudBuild.cs b/build/DalamudBuild.cs index 67a812916..d374c79f8 100644 --- a/build/DalamudBuild.cs +++ b/build/DalamudBuild.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.IO; using Nuke.Common; @@ -5,6 +6,7 @@ using Nuke.Common.Execution; using Nuke.Common.Git; using Nuke.Common.IO; using Nuke.Common.ProjectModel; +using Nuke.Common.Tooling; using Nuke.Common.Tools.DotNet; using Nuke.Common.Tools.MSBuild; using Serilog; @@ -61,6 +63,9 @@ public class DalamudBuild : NukeBuild private static Dictionary EnvironmentVariables => new(EnvironmentInfo.Variables); + private static string ConsoleTemplate => "{Message:l}{NewLine}{Exception}"; + private static bool IsCIBuild => Environment.GetEnvironmentVariable("CI") == "true"; + Target Restore => _ => _ .Executes(() => { @@ -123,16 +128,20 @@ public class DalamudBuild : NukeBuild .SetProjectFile(DalamudProjectFile) .SetConfiguration(Configuration) .EnableNoRestore(); - + if (IsCIBuild) + { + s = s + .SetProcessArgumentConfigurator(a => a.Add("/clp:NoSummary")); // Disable MSBuild summary on CI builds + } // We need to emit compiler generated files for the docs build, since docfx can't run generators directly // TODO: This fails every build after this because of redefinitions... + // if (IsDocsBuild) // { // Log.Warning("Building for documentation, emitting compiler generated files. This can cause issues on Windows due to path-length limitations"); // s = s // .SetProperty("IsDocsBuild", "true"); // } - return s; }); }); @@ -171,12 +180,28 @@ public class DalamudBuild : NukeBuild .SetConfiguration(Configuration)); }); + Target SetCILogging => _ => _ + .DependentFor(Compile) + .OnlyWhenStatic(() => IsCIBuild) + .Executes(() => + { + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Debug() + .WriteTo.Console(outputTemplate: ConsoleTemplate) + .CreateLogger(); + }); + Target Compile => _ => _ - .DependsOn(CompileDalamud) - .DependsOn(CompileDalamudBoot) - .DependsOn(CompileDalamudCrashHandler) - .DependsOn(CompileInjector) - .DependsOn(CompileInjectorBoot); + .DependsOn(CompileDalamud) + .DependsOn(CompileDalamudBoot) + .DependsOn(CompileDalamudCrashHandler) + .DependsOn(CompileInjector) + .DependsOn(CompileInjectorBoot) + ; + + Target CI => _ => _ + .DependsOn(Compile) + .Triggers(Test); Target Test => _ => _ .DependsOn(Compile) @@ -185,6 +210,7 @@ public class DalamudBuild : NukeBuild DotNetTasks.DotNetTest(s => s .SetProjectFile(TestProjectFile) .SetConfiguration(Configuration) + .AddProperty("WarningLevel", "0") .EnableNoRestore()); }); From a2124bb73dde7f8eeb86f50c5c72d7413fc9e893 Mon Sep 17 00:00:00 2001 From: KazWolfe Date: Thu, 13 Mar 2025 14:16:28 -0700 Subject: [PATCH 10/55] fix: Attempt to better handle hook disposal (#1803) - Use a Weak Concurrent Collection to track scoped hooks - Make `Hook`s remove themselves from the Tracked Hook list. --- Dalamud/Hooking/AsmHook.cs | 8 +++- Dalamud/Hooking/Hook.cs | 5 +++ .../Internal/FunctionPointerVariableHook.cs | 4 +- .../GameInteropProviderPluginScoped.cs | 6 +-- Dalamud/Hooking/Internal/MinHookHook.cs | 4 +- Dalamud/Hooking/Internal/ReloadedHook.cs | 4 +- .../Internal/Windows/PluginStatWindow.cs | 13 ------ Dalamud/Utility/WeakConcurrentCollection.cs | 44 +++++++++++++++++++ 8 files changed, 67 insertions(+), 21 deletions(-) create mode 100644 Dalamud/Utility/WeakConcurrentCollection.cs diff --git a/Dalamud/Hooking/AsmHook.cs b/Dalamud/Hooking/AsmHook.cs index 4ee96bb15..f1ed7fd11 100644 --- a/Dalamud/Hooking/AsmHook.cs +++ b/Dalamud/Hooking/AsmHook.cs @@ -20,6 +20,8 @@ public sealed class AsmHook : IDisposable, IDalamudHook private bool isEnabled = false; private DynamicMethod statsMethod; + + private Guid hookId = Guid.NewGuid(); /// /// Initializes a new instance of the class. @@ -44,7 +46,7 @@ public sealed class AsmHook : IDisposable, IDalamudHook this.statsMethod.GetILGenerator().Emit(OpCodes.Ret); var dele = this.statsMethod.CreateDelegate(typeof(Action)); - HookManager.TrackedHooks.TryAdd(Guid.NewGuid(), new HookInfo(this, dele, Assembly.GetCallingAssembly())); + HookManager.TrackedHooks.TryAdd(this.hookId, new HookInfo(this, dele, Assembly.GetCallingAssembly())); } /// @@ -70,7 +72,7 @@ public sealed class AsmHook : IDisposable, IDalamudHook this.statsMethod.GetILGenerator().Emit(OpCodes.Ret); var dele = this.statsMethod.CreateDelegate(typeof(Action)); - HookManager.TrackedHooks.TryAdd(Guid.NewGuid(), new HookInfo(this, dele, Assembly.GetCallingAssembly())); + HookManager.TrackedHooks.TryAdd(this.hookId, new HookInfo(this, dele, Assembly.GetCallingAssembly())); } /// @@ -116,6 +118,8 @@ public sealed class AsmHook : IDisposable, IDalamudHook this.IsDisposed = true; + HookManager.TrackedHooks.TryRemove(this.hookId, out _); + if (this.isEnabled) { this.isEnabled = false; diff --git a/Dalamud/Hooking/Hook.cs b/Dalamud/Hooking/Hook.cs index da65fedc7..20796bbf0 100644 --- a/Dalamud/Hooking/Hook.cs +++ b/Dalamud/Hooking/Hook.cs @@ -71,6 +71,11 @@ public abstract class Hook : IDalamudHook where T : Delegate /// public virtual string BackendName => throw new NotImplementedException(); + /// + /// Gets the unique GUID for this hook. + /// + protected Guid HookId { get; } = Guid.NewGuid(); + /// /// Remove a hook from the current process. /// diff --git a/Dalamud/Hooking/Internal/FunctionPointerVariableHook.cs b/Dalamud/Hooking/Internal/FunctionPointerVariableHook.cs index 7a177206f..40a33fc1b 100644 --- a/Dalamud/Hooking/Internal/FunctionPointerVariableHook.cs +++ b/Dalamud/Hooking/Internal/FunctionPointerVariableHook.cs @@ -100,7 +100,7 @@ internal class FunctionPointerVariableHook : Hook unhooker.TrimAfterHook(); - HookManager.TrackedHooks.TryAdd(Guid.NewGuid(), new HookInfo(this, detour, callingAssembly)); + HookManager.TrackedHooks.TryAdd(this.HookId, new HookInfo(this, detour, callingAssembly)); } } @@ -137,6 +137,8 @@ internal class FunctionPointerVariableHook : Hook this.Disable(); + HookManager.TrackedHooks.TryRemove(this.HookId, out _); + var index = HookManager.MultiHookTracker[this.Address].IndexOf(this); HookManager.MultiHookTracker[this.Address][index] = null; diff --git a/Dalamud/Hooking/Internal/GameInteropProviderPluginScoped.cs b/Dalamud/Hooking/Internal/GameInteropProviderPluginScoped.cs index f7c4db00a..52d266335 100644 --- a/Dalamud/Hooking/Internal/GameInteropProviderPluginScoped.cs +++ b/Dalamud/Hooking/Internal/GameInteropProviderPluginScoped.cs @@ -1,5 +1,4 @@ -using System.Collections.Concurrent; -using System.Diagnostics; +using System.Diagnostics; using System.Linq; using Dalamud.Game; @@ -7,6 +6,7 @@ using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Services; +using Dalamud.Utility; using Dalamud.Utility.Signatures; using Serilog; @@ -25,7 +25,7 @@ internal class GameInteropProviderPluginScoped : IGameInteropProvider, IInternal private readonly LocalPlugin plugin; private readonly SigScanner scanner; - private readonly ConcurrentBag trackedHooks = new(); + private readonly WeakConcurrentCollection trackedHooks = new(); /// /// Initializes a new instance of the class. diff --git a/Dalamud/Hooking/Internal/MinHookHook.cs b/Dalamud/Hooking/Internal/MinHookHook.cs index 4cebca0d0..0305f3c84 100644 --- a/Dalamud/Hooking/Internal/MinHookHook.cs +++ b/Dalamud/Hooking/Internal/MinHookHook.cs @@ -35,7 +35,7 @@ internal class MinHookHook : Hook where T : Delegate unhooker.TrimAfterHook(); - HookManager.TrackedHooks.TryAdd(Guid.NewGuid(), new HookInfo(this, detour, callingAssembly)); + HookManager.TrackedHooks.TryAdd(this.HookId, new HookInfo(this, detour, callingAssembly)); } } @@ -76,6 +76,8 @@ internal class MinHookHook : Hook where T : Delegate HookManager.MultiHookTracker[this.Address][index] = null; } + HookManager.TrackedHooks.TryRemove(this.HookId, out _); + base.Dispose(); } diff --git a/Dalamud/Hooking/Internal/ReloadedHook.cs b/Dalamud/Hooking/Internal/ReloadedHook.cs index bf441a86d..2b0a4e9ce 100644 --- a/Dalamud/Hooking/Internal/ReloadedHook.cs +++ b/Dalamud/Hooking/Internal/ReloadedHook.cs @@ -30,7 +30,7 @@ internal class ReloadedHook : Hook where T : Delegate unhooker.TrimAfterHook(); - HookManager.TrackedHooks.TryAdd(Guid.NewGuid(), new HookInfo(this, detour, callingAssembly)); + HookManager.TrackedHooks.TryAdd(this.HookId, new HookInfo(this, detour, callingAssembly)); } } @@ -63,6 +63,8 @@ internal class ReloadedHook : Hook where T : Delegate if (this.IsDisposed) return; + HookManager.TrackedHooks.TryRemove(this.HookId, out _); + this.Disable(); base.Dispose(); diff --git a/Dalamud/Interface/Internal/Windows/PluginStatWindow.cs b/Dalamud/Interface/Internal/Windows/PluginStatWindow.cs index 314f023da..eeafa98e7 100644 --- a/Dalamud/Interface/Internal/Windows/PluginStatWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginStatWindow.cs @@ -258,8 +258,6 @@ internal class PluginStatWindow : Window ImGui.EndTabItem(); } - var toRemove = new List(); - if (ImGui.BeginTabItem("Hooks")) { ImGui.Checkbox("Show Dalamud Hooks", ref this.showDalamudHooks); @@ -291,9 +289,6 @@ internal class PluginStatWindow : Window { try { - if (trackedHook.Hook.IsDisposed) - toRemove.Add(guid); - if (!this.showDalamudHooks && trackedHook.Assembly == Assembly.GetExecutingAssembly()) continue; @@ -355,14 +350,6 @@ internal class PluginStatWindow : Window } } - if (ImGui.IsWindowAppearing()) - { - foreach (var guid in toRemove) - { - HookManager.TrackedHooks.TryRemove(guid, out _); - } - } - ImGui.EndTabBar(); } } diff --git a/Dalamud/Utility/WeakConcurrentCollection.cs b/Dalamud/Utility/WeakConcurrentCollection.cs new file mode 100644 index 000000000..a3bc04651 --- /dev/null +++ b/Dalamud/Utility/WeakConcurrentCollection.cs @@ -0,0 +1,44 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; + +namespace Dalamud.Utility; + +/// +/// An implementation of a weak concurrent set based on a . +/// +/// The type of object that we're tracking. +public class WeakConcurrentCollection : ICollection where T : class +{ + private readonly ConditionalWeakTable cwt = new(); + + /// + public int Count => this.cwt.Count(); + + /// + public bool IsReadOnly => false; + + private IEnumerable Keys => this.cwt.Select(pair => pair.Key); + + /// + public IEnumerator GetEnumerator() => this.cwt.Select(pair => pair.Key).GetEnumerator(); + + /// + IEnumerator IEnumerable.GetEnumerator() => this.cwt.Select(pair => pair.Key).GetEnumerator(); + + /// + public void Add(T item) => this.cwt.AddOrUpdate(item, null); + + /// + public void Clear() => this.cwt.Clear(); + + /// + public bool Contains(T item) => this.Keys.Contains(item); + + /// + public void CopyTo(T[] array, int arrayIndex) => this.Keys.ToArray().CopyTo(array, arrayIndex); + + /// + public bool Remove(T item) => this.cwt.Remove(item); +} From 82dd4629cb9deaac3ce2d1b81d828325a0035f5e Mon Sep 17 00:00:00 2001 From: Ethan Henderson Date: Sun, 23 Mar 2025 16:13:27 -0600 Subject: [PATCH 11/55] SMNGauge: Fix typo, update CS (#2197) * chore(SMNGauge): update gauge to latest CS struct * chore(SMNGauge): Fix `AttunementTimerRemaining` typo * refactor(SMNGauge): Cast `AttunementType` to a new enum instead of leaving it raw * fix(SMNGauge): Use `AttunementType` for `IsAttuned` --- .../JobGauge/Enums/SummonAttunement.cs | 27 ++++++++++++++++++ .../ClientState/JobGauge/Types/SMNGauge.cs | 28 +++++++++++++++---- 2 files changed, 50 insertions(+), 5 deletions(-) create mode 100644 Dalamud/Game/ClientState/JobGauge/Enums/SummonAttunement.cs diff --git a/Dalamud/Game/ClientState/JobGauge/Enums/SummonAttunement.cs b/Dalamud/Game/ClientState/JobGauge/Enums/SummonAttunement.cs new file mode 100644 index 000000000..e0b65966c --- /dev/null +++ b/Dalamud/Game/ClientState/JobGauge/Enums/SummonAttunement.cs @@ -0,0 +1,27 @@ +namespace Dalamud.Game.ClientState.JobGauge.Enums; + +public enum SummonAttunement +{ + /// + /// No attunement. + /// + NONE = 0, + + /// + /// Attuned to the summon Ifrit. + /// Same as . + /// + IFRIT = 1, + + /// + /// Attuned to the summon Titan. + /// Same as . + /// + TITAN = 2, + + /// + /// Attuned to the summon Garuda. + /// Same as . + /// + GARUDA = 3, +} diff --git a/Dalamud/Game/ClientState/JobGauge/Types/SMNGauge.cs b/Dalamud/Game/ClientState/JobGauge/Types/SMNGauge.cs index 81be0e762..4c27ae83a 100644 --- a/Dalamud/Game/ClientState/JobGauge/Types/SMNGauge.cs +++ b/Dalamud/Game/ClientState/JobGauge/Types/SMNGauge.cs @@ -22,10 +22,13 @@ public unsafe class SMNGauge : JobGaugeBase /// public ushort SummonTimerRemaining => this.Struct->SummonTimer; + [Obsolete("Typo fixed. Use AttunementTimerRemaining instead.", true)] + public ushort AttunmentTimerRemaining => this.AttunementTimerRemaining; + /// /// Gets the time remaining for the current attunement. /// - public ushort AttunmentTimerRemaining => this.Struct->AttunementTimer; + public ushort AttunementTimerRemaining => this.Struct->AttunementTimer; /// /// Gets the summon that will return after the current summon expires. @@ -40,10 +43,25 @@ public unsafe class SMNGauge : JobGaugeBase public PetGlam ReturnSummonGlam => (PetGlam)this.Struct->ReturnSummonGlam; /// - /// Gets the amount of aspected Attunment remaining. + /// Gets the amount of aspected Attunement remaining. /// + /// + /// As of 7.01, this should be treated as a bit field. + /// Use and instead. + /// public byte Attunement => this.Struct->Attunement; + /// + /// Gets the count of attunement cost resource available. + /// + public byte AttunementCount => this.Struct->AttunementCount; + + /// + /// Gets the type of attunement available. + /// Use the summon attuned accessors instead. + /// + public SummonAttunement AttunementType => (SummonAttunement)this.Struct->AttunementType; + /// /// Gets the current aether flags. /// Use the summon accessors instead. @@ -84,19 +102,19 @@ public unsafe class SMNGauge : JobGaugeBase /// Gets a value indicating whether if Ifrit is currently attuned. /// /// true or false. - public bool IsIfritAttuned => this.AetherFlags.HasFlag(AetherFlags.IfritAttuned) && !this.AetherFlags.HasFlag(AetherFlags.GarudaAttuned); + public bool IsIfritAttuned => this.AttunementType == SummonAttunement.IFRIT; /// /// Gets a value indicating whether if Titan is currently attuned. /// /// true or false. - public bool IsTitanAttuned => this.AetherFlags.HasFlag(AetherFlags.TitanAttuned) && !this.AetherFlags.HasFlag(AetherFlags.GarudaAttuned); + public bool IsTitanAttuned => this.AttunementType == SummonAttunement.TITAN; /// /// Gets a value indicating whether if Garuda is currently attuned. /// /// true or false. - public bool IsGarudaAttuned => this.AetherFlags.HasFlag(AetherFlags.GarudaAttuned); + public bool IsGarudaAttuned => this.AttunementType == SummonAttunement.GARUDA; /// /// Gets a value indicating whether there are any Aetherflow stacks available. From 23c87015b635ce7fa58a6b3608a8e9b99c3ca3c3 Mon Sep 17 00:00:00 2001 From: Ethan Henderson Date: Sun, 23 Mar 2025 16:14:16 -0600 Subject: [PATCH 12/55] add(DRKGauge): Add `DeliriumComboStep`, and its enum `DeliriumStep` (#2198) --- .../JobGauge/Enums/DeliriumStep.cs | 19 +++++++++++++++++++ .../ClientState/JobGauge/Types/DRKGauge.cs | 17 ++++++++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 Dalamud/Game/ClientState/JobGauge/Enums/DeliriumStep.cs diff --git a/Dalamud/Game/ClientState/JobGauge/Enums/DeliriumStep.cs b/Dalamud/Game/ClientState/JobGauge/Enums/DeliriumStep.cs new file mode 100644 index 000000000..cb35a83d2 --- /dev/null +++ b/Dalamud/Game/ClientState/JobGauge/Enums/DeliriumStep.cs @@ -0,0 +1,19 @@ +namespace Dalamud.Game.ClientState.JobGauge.Enums; + +public enum DeliriumStep +{ + /// + /// Scarlet Delirium can be used. + /// + SCARLET_DELIRIUM = 0, + + /// + /// Comeuppance can be used. + /// + COMEUPPANCE = 1, + + /// + /// Torcleaver can be used. + /// + TORCLEAVER = 2, +} diff --git a/Dalamud/Game/ClientState/JobGauge/Types/DRKGauge.cs b/Dalamud/Game/ClientState/JobGauge/Types/DRKGauge.cs index 834087040..c56d03db0 100644 --- a/Dalamud/Game/ClientState/JobGauge/Types/DRKGauge.cs +++ b/Dalamud/Game/ClientState/JobGauge/Types/DRKGauge.cs @@ -1,9 +1,12 @@ +using Dalamud.Game.ClientState.JobGauge.Enums; +using FFXIVClientStructs.FFXIV.Client.Game.Gauge; + namespace Dalamud.Game.ClientState.JobGauge.Types; /// /// In-memory DRK job gauge. /// -public unsafe class DRKGauge : JobGaugeBase +public unsafe class DRKGauge : JobGaugeBase { /// /// Initializes a new instance of the class. @@ -34,4 +37,16 @@ public unsafe class DRKGauge : JobGaugeBase /// true or false. public bool HasDarkArts => this.Struct->DarkArtsState > 0; + + /// + /// Gets the step of the Delirium Combo (Scarlet Delirium, Comeuppance, + /// Torcleaver) that the player is on.
+ /// Does not in any way consider whether the player is still under Delirium, or + /// if the player still has stacks of Delirium to use. + ///
+ /// + /// Value will persist until combo is finished OR + /// if the combo is not completed then the value will stay until about halfway into Delirium's cooldown. + /// + public DeliriumStep DeliriumComboStep => (DeliriumStep)this.Struct->DeliriumStep; } From 6a1cea731f5c33dd9e8789933eca43c5b2acd4eb Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Sun, 23 Mar 2025 18:11:03 -0700 Subject: [PATCH 13/55] chore: remove obsoleted AppendMacroString methods --- Dalamud/Utility/SeStringExtensions.cs | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/Dalamud/Utility/SeStringExtensions.cs b/Dalamud/Utility/SeStringExtensions.cs index 057759e1e..f50f19d8e 100644 --- a/Dalamud/Utility/SeStringExtensions.cs +++ b/Dalamud/Utility/SeStringExtensions.cs @@ -38,24 +38,6 @@ public static class SeStringExtensions /// The re-parsed Dalamud SeString. public static DSeString ToDalamudString(this ReadOnlySeStringSpan originalString) => DSeString.Parse(originalString.Data); - /// Compiles and appends a macro string. - /// Target SeString builder. - /// Macro string in UTF-8 to compile and append to . - /// this for method chaining. - [Obsolete($"Use {nameof(LSeStringBuilder)}.{nameof(LSeStringBuilder.AppendMacroString)} directly instead.", true)] - [Api12ToDo("Remove")] - public static LSeStringBuilder AppendMacroString(this LSeStringBuilder ssb, ReadOnlySpan macroString) => - ssb.AppendMacroString(macroString, new() { ExceptionMode = MacroStringParseExceptionMode.EmbedError }); - - /// Compiles and appends a macro string. - /// Target SeString builder. - /// Macro string in UTF-16 to compile and append to . - /// this for method chaining. - [Obsolete($"Use {nameof(LSeStringBuilder)}.{nameof(LSeStringBuilder.AppendMacroString)} directly instead.", true)] - [Api12ToDo("Remove")] - public static LSeStringBuilder AppendMacroString(this LSeStringBuilder ssb, ReadOnlySpan macroString) => - ssb.AppendMacroString(macroString, new() { ExceptionMode = MacroStringParseExceptionMode.EmbedError }); - /// Compiles and appends a macro string. /// Target SeString builder. /// Macro string in UTF-8 to compile and append to . From c3df199e29866ffd7cb99061338ad12611c75e3a Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Sun, 23 Mar 2025 18:12:41 -0700 Subject: [PATCH 14/55] chore: Obsolete accessing ObjectTable off main thread --- .../Game/ClientState/Objects/ObjectTable.cs | 42 ++++--------------- 1 file changed, 7 insertions(+), 35 deletions(-) diff --git a/Dalamud/Game/ClientState/Objects/ObjectTable.cs b/Dalamud/Game/ClientState/Objects/ObjectTable.cs index 6aa7fd8ad..6b7f5f193 100644 --- a/Dalamud/Game/ClientState/Objects/ObjectTable.cs +++ b/Dalamud/Game/ClientState/Objects/ObjectTable.cs @@ -66,7 +66,7 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable { get { - _ = this.WarnMultithreadedUsage(); + ThreadSafety.AssertMainThread(); return (nint)(&CSGameObjectManager.Instance()->Objects); } @@ -80,7 +80,7 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable { get { - _ = this.WarnMultithreadedUsage(); + ThreadSafety.AssertMainThread(); return (index >= objectTableLength || index < 0) ? null : this.cachedObjectTable[index].Update(); } @@ -89,7 +89,7 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable /// public IGameObject? SearchById(ulong gameObjectId) { - _ = this.WarnMultithreadedUsage(); + ThreadSafety.AssertMainThread(); if (gameObjectId is 0) return null; @@ -106,7 +106,7 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable /// public IGameObject? SearchByEntityId(uint entityId) { - _ = this.WarnMultithreadedUsage(); + ThreadSafety.AssertMainThread(); if (entityId is 0 or 0xE0000000) return null; @@ -123,7 +123,7 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable /// public unsafe nint GetObjectAddress(int index) { - _ = this.WarnMultithreadedUsage(); + ThreadSafety.AssertMainThread(); return (index >= objectTableLength || index < 0) ? nint.Zero : (nint)this.cachedObjectTable[index].Address; } @@ -131,7 +131,7 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable /// public unsafe IGameObject? CreateObjectReference(nint address) { - _ = this.WarnMultithreadedUsage(); + ThreadSafety.AssertMainThread(); if (this.clientState.LocalContentId == 0) return null; @@ -155,27 +155,6 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable }; } - [Api12ToDo("Use ThreadSafety.AssertMainThread() instead of this.")] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool WarnMultithreadedUsage() - { - if (ThreadSafety.IsMainThread) - return false; - - var n = Environment.TickCount64; - if (this.nextMultithreadedUsageWarnTime < n) - { - this.nextMultithreadedUsageWarnTime = n + 30000; - - Log.Warning( - "{plugin} is accessing {objectTable} outside the main thread. This is deprecated.", - Service.Get().FindCallingPlugin()?.Name ?? "", - nameof(ObjectTable)); - } - - return true; - } - /// Stores an object table entry, with preallocated concrete types. /// Initializes a new instance of the struct. /// A pointer to the object table entry this entry should be pointing to. @@ -228,14 +207,7 @@ internal sealed partial class ObjectTable /// public IEnumerator GetEnumerator() { - // If something's trying to enumerate outside the framework thread, we use the ObjectPool. - if (this.WarnMultithreadedUsage()) - { - // let's not - var e = this.multiThreadedEnumerators.Get(); - e.InitializeForPooledObjects(this); - return e; - } + 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()) From 1676522ea8f0a7f3447c3e5ff2a026260d8034d4 Mon Sep 17 00:00:00 2001 From: goat Date: Sun, 29 Dec 2024 16:53:16 +0100 Subject: [PATCH 15/55] clamp value of all easings by default (cherry picked from commit 1aada983931d9e45a250eebbc17c8b782d07701b) --- Dalamud/Interface/Animation/Easing.cs | 13 ++++++++----- .../Interface/Animation/EasingFunctions/InCirc.cs | 2 +- .../Interface/Animation/EasingFunctions/InCubic.cs | 2 +- .../Animation/EasingFunctions/InElastic.cs | 10 +++++----- .../Animation/EasingFunctions/InOutCirc.cs | 6 +++--- .../Animation/EasingFunctions/InOutCubic.cs | 2 +- .../Animation/EasingFunctions/InOutElastic.cs | 14 +++++++------- .../Animation/EasingFunctions/InOutQuint.cs | 2 +- .../Animation/EasingFunctions/InOutSine.cs | 2 +- .../Interface/Animation/EasingFunctions/InQuint.cs | 2 +- .../Interface/Animation/EasingFunctions/InSine.cs | 2 +- .../Interface/Animation/EasingFunctions/OutCirc.cs | 2 +- .../Animation/EasingFunctions/OutCubic.cs | 2 +- .../Animation/EasingFunctions/OutElastic.cs | 10 +++++----- .../Animation/EasingFunctions/OutQuint.cs | 2 +- .../Interface/Animation/EasingFunctions/OutSine.cs | 2 +- .../Internal/Windows/TitleScreenMenuWindow.cs | 4 ++-- 17 files changed, 41 insertions(+), 38 deletions(-) diff --git a/Dalamud/Interface/Animation/Easing.cs b/Dalamud/Interface/Animation/Easing.cs index c6d6149af..a48300a22 100644 --- a/Dalamud/Interface/Animation/Easing.cs +++ b/Dalamud/Interface/Animation/Easing.cs @@ -1,14 +1,11 @@ using System.Diagnostics; using System.Numerics; -using Dalamud.Utility; - namespace Dalamud.Interface.Animation; /// /// Base class facilitating the implementation of easing functions. /// -[Api12ToDo("Re-apply https://github.com/goatcorp/Dalamud/commit/1aada983931d9e45a250eebbc17c8b782d07701b")] public abstract class Easing { // TODO: Use game delta time here instead @@ -46,9 +43,15 @@ public abstract class Easing public bool IsInverse { get; set; } /// - /// Gets or sets the current value of the animation, from 0 to 1. + /// Gets the current value of the animation, from 0 to 1. /// - public double Value + public double Value => Math.Clamp(this.ValueUnclamped, 0, 1); + + /// + /// Gets or sets the current value of the animation, not limited to a range of 0 to 1. + /// Will return numbers outside of this range if accessed beyond animation time. + /// + public double ValueUnclamped { get { diff --git a/Dalamud/Interface/Animation/EasingFunctions/InCirc.cs b/Dalamud/Interface/Animation/EasingFunctions/InCirc.cs index c467104c5..d94e9fc9f 100644 --- a/Dalamud/Interface/Animation/EasingFunctions/InCirc.cs +++ b/Dalamud/Interface/Animation/EasingFunctions/InCirc.cs @@ -19,6 +19,6 @@ public class InCirc : Easing public override void Update() { var p = this.Progress; - this.Value = 1 - Math.Sqrt(1 - Math.Pow(p, 2)); + this.ValueUnclamped = 1 - Math.Sqrt(1 - Math.Pow(p, 2)); } } diff --git a/Dalamud/Interface/Animation/EasingFunctions/InCubic.cs b/Dalamud/Interface/Animation/EasingFunctions/InCubic.cs index 78f6774ac..64ebc5ba3 100644 --- a/Dalamud/Interface/Animation/EasingFunctions/InCubic.cs +++ b/Dalamud/Interface/Animation/EasingFunctions/InCubic.cs @@ -19,6 +19,6 @@ public class InCubic : Easing public override void Update() { var p = this.Progress; - this.Value = p * p * p; + this.ValueUnclamped = p * p * p; } } diff --git a/Dalamud/Interface/Animation/EasingFunctions/InElastic.cs b/Dalamud/Interface/Animation/EasingFunctions/InElastic.cs index c53c3d587..2e834e41c 100644 --- a/Dalamud/Interface/Animation/EasingFunctions/InElastic.cs +++ b/Dalamud/Interface/Animation/EasingFunctions/InElastic.cs @@ -21,10 +21,10 @@ public class InElastic : Easing public override void Update() { var p = this.Progress; - this.Value = p == 0 - ? 0 - : p == 1 - ? 1 - : -Math.Pow(2, (10 * p) - 10) * Math.Sin(((p * 10) - 10.75) * Constant); + this.ValueUnclamped = p == 0 + ? 0 + : p == 1 + ? 1 + : -Math.Pow(2, (10 * p) - 10) * Math.Sin(((p * 10) - 10.75) * Constant); } } diff --git a/Dalamud/Interface/Animation/EasingFunctions/InOutCirc.cs b/Dalamud/Interface/Animation/EasingFunctions/InOutCirc.cs index 71a598dfb..a63ab648a 100644 --- a/Dalamud/Interface/Animation/EasingFunctions/InOutCirc.cs +++ b/Dalamud/Interface/Animation/EasingFunctions/InOutCirc.cs @@ -19,8 +19,8 @@ public class InOutCirc : Easing public override void Update() { var p = this.Progress; - this.Value = p < 0.5 - ? (1 - Math.Sqrt(1 - Math.Pow(2 * p, 2))) / 2 - : (Math.Sqrt(1 - Math.Pow((-2 * p) + 2, 2)) + 1) / 2; + this.ValueUnclamped = p < 0.5 + ? (1 - Math.Sqrt(1 - Math.Pow(2 * p, 2))) / 2 + : (Math.Sqrt(1 - Math.Pow((-2 * p) + 2, 2)) + 1) / 2; } } diff --git a/Dalamud/Interface/Animation/EasingFunctions/InOutCubic.cs b/Dalamud/Interface/Animation/EasingFunctions/InOutCubic.cs index 07bcfa28d..4083265b7 100644 --- a/Dalamud/Interface/Animation/EasingFunctions/InOutCubic.cs +++ b/Dalamud/Interface/Animation/EasingFunctions/InOutCubic.cs @@ -19,6 +19,6 @@ public class InOutCubic : Easing public override void Update() { var p = this.Progress; - this.Value = p < 0.5 ? 4 * p * p * p : 1 - (Math.Pow((-2 * p) + 2, 3) / 2); + this.ValueUnclamped = p < 0.5 ? 4 * p * p * p : 1 - (Math.Pow((-2 * p) + 2, 3) / 2); } } diff --git a/Dalamud/Interface/Animation/EasingFunctions/InOutElastic.cs b/Dalamud/Interface/Animation/EasingFunctions/InOutElastic.cs index f78f9f336..f27726038 100644 --- a/Dalamud/Interface/Animation/EasingFunctions/InOutElastic.cs +++ b/Dalamud/Interface/Animation/EasingFunctions/InOutElastic.cs @@ -21,12 +21,12 @@ public class InOutElastic : Easing public override void Update() { var p = this.Progress; - this.Value = p == 0 - ? 0 - : p == 1 - ? 1 - : p < 0.5 - ? -(Math.Pow(2, (20 * p) - 10) * Math.Sin(((20 * p) - 11.125) * Constant)) / 2 - : (Math.Pow(2, (-20 * p) + 10) * Math.Sin(((20 * p) - 11.125) * Constant) / 2) + 1; + this.ValueUnclamped = p == 0 + ? 0 + : p == 1 + ? 1 + : p < 0.5 + ? -(Math.Pow(2, (20 * p) - 10) * Math.Sin(((20 * p) - 11.125) * Constant)) / 2 + : (Math.Pow(2, (-20 * p) + 10) * Math.Sin(((20 * p) - 11.125) * Constant) / 2) + 1; } } diff --git a/Dalamud/Interface/Animation/EasingFunctions/InOutQuint.cs b/Dalamud/Interface/Animation/EasingFunctions/InOutQuint.cs index 64ab98b16..e08129b25 100644 --- a/Dalamud/Interface/Animation/EasingFunctions/InOutQuint.cs +++ b/Dalamud/Interface/Animation/EasingFunctions/InOutQuint.cs @@ -19,6 +19,6 @@ public class InOutQuint : Easing public override void Update() { var p = this.Progress; - this.Value = p < 0.5 ? 16 * p * p * p * p * p : 1 - (Math.Pow((-2 * p) + 2, 5) / 2); + this.ValueUnclamped = p < 0.5 ? 16 * p * p * p * p * p : 1 - (Math.Pow((-2 * p) + 2, 5) / 2); } } diff --git a/Dalamud/Interface/Animation/EasingFunctions/InOutSine.cs b/Dalamud/Interface/Animation/EasingFunctions/InOutSine.cs index 2f347ff80..cb940d87d 100644 --- a/Dalamud/Interface/Animation/EasingFunctions/InOutSine.cs +++ b/Dalamud/Interface/Animation/EasingFunctions/InOutSine.cs @@ -19,6 +19,6 @@ public class InOutSine : Easing public override void Update() { var p = this.Progress; - this.Value = -(Math.Cos(Math.PI * p) - 1) / 2; + this.ValueUnclamped = -(Math.Cos(Math.PI * p) - 1) / 2; } } diff --git a/Dalamud/Interface/Animation/EasingFunctions/InQuint.cs b/Dalamud/Interface/Animation/EasingFunctions/InQuint.cs index a5ab5a22c..827e0e21b 100644 --- a/Dalamud/Interface/Animation/EasingFunctions/InQuint.cs +++ b/Dalamud/Interface/Animation/EasingFunctions/InQuint.cs @@ -19,6 +19,6 @@ public class InQuint : Easing public override void Update() { var p = this.Progress; - this.Value = p * p * p * p * p; + this.ValueUnclamped = p * p * p * p * p; } } diff --git a/Dalamud/Interface/Animation/EasingFunctions/InSine.cs b/Dalamud/Interface/Animation/EasingFunctions/InSine.cs index fa079baad..61affa10a 100644 --- a/Dalamud/Interface/Animation/EasingFunctions/InSine.cs +++ b/Dalamud/Interface/Animation/EasingFunctions/InSine.cs @@ -19,6 +19,6 @@ public class InSine : Easing public override void Update() { var p = this.Progress; - this.Value = 1 - Math.Cos((p * Math.PI) / 2); + this.ValueUnclamped = 1 - Math.Cos((p * Math.PI) / 2); } } diff --git a/Dalamud/Interface/Animation/EasingFunctions/OutCirc.cs b/Dalamud/Interface/Animation/EasingFunctions/OutCirc.cs index b0d3b895a..980e29a81 100644 --- a/Dalamud/Interface/Animation/EasingFunctions/OutCirc.cs +++ b/Dalamud/Interface/Animation/EasingFunctions/OutCirc.cs @@ -19,6 +19,6 @@ public class OutCirc : Easing public override void Update() { var p = this.Progress; - this.Value = Math.Sqrt(1 - Math.Pow(p - 1, 2)); + this.ValueUnclamped = Math.Sqrt(1 - Math.Pow(p - 1, 2)); } } diff --git a/Dalamud/Interface/Animation/EasingFunctions/OutCubic.cs b/Dalamud/Interface/Animation/EasingFunctions/OutCubic.cs index 9c1bb57dc..e1a79c35b 100644 --- a/Dalamud/Interface/Animation/EasingFunctions/OutCubic.cs +++ b/Dalamud/Interface/Animation/EasingFunctions/OutCubic.cs @@ -19,6 +19,6 @@ public class OutCubic : Easing public override void Update() { var p = this.Progress; - this.Value = 1 - Math.Pow(1 - p, 3); + this.ValueUnclamped = 1 - Math.Pow(1 - p, 3); } } diff --git a/Dalamud/Interface/Animation/EasingFunctions/OutElastic.cs b/Dalamud/Interface/Animation/EasingFunctions/OutElastic.cs index 6a4fcd6dc..1f525b404 100644 --- a/Dalamud/Interface/Animation/EasingFunctions/OutElastic.cs +++ b/Dalamud/Interface/Animation/EasingFunctions/OutElastic.cs @@ -21,10 +21,10 @@ public class OutElastic : Easing public override void Update() { var p = this.Progress; - this.Value = p == 0 - ? 0 - : p == 1 - ? 1 - : (Math.Pow(2, -10 * p) * Math.Sin(((p * 10) - 0.75) * Constant)) + 1; + this.ValueUnclamped = p == 0 + ? 0 + : p == 1 + ? 1 + : (Math.Pow(2, -10 * p) * Math.Sin(((p * 10) - 0.75) * Constant)) + 1; } } diff --git a/Dalamud/Interface/Animation/EasingFunctions/OutQuint.cs b/Dalamud/Interface/Animation/EasingFunctions/OutQuint.cs index a3174e762..24a2255d3 100644 --- a/Dalamud/Interface/Animation/EasingFunctions/OutQuint.cs +++ b/Dalamud/Interface/Animation/EasingFunctions/OutQuint.cs @@ -19,6 +19,6 @@ public class OutQuint : Easing public override void Update() { var p = this.Progress; - this.Value = 1 - Math.Pow(1 - p, 5); + this.ValueUnclamped = 1 - Math.Pow(1 - p, 5); } } diff --git a/Dalamud/Interface/Animation/EasingFunctions/OutSine.cs b/Dalamud/Interface/Animation/EasingFunctions/OutSine.cs index ba82232b3..a376d7f57 100644 --- a/Dalamud/Interface/Animation/EasingFunctions/OutSine.cs +++ b/Dalamud/Interface/Animation/EasingFunctions/OutSine.cs @@ -19,6 +19,6 @@ public class OutSine : Easing public override void Update() { var p = this.Progress; - this.Value = Math.Sin((p * Math.PI) / 2); + this.ValueUnclamped = Math.Sin((p * Math.PI) / 2); } } diff --git a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs index ce8c192a4..c1f0b2a67 100644 --- a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs +++ b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs @@ -251,7 +251,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable this.fadeOutEasing.Update(); - using (ImRaii.PushStyle(ImGuiStyleVar.Alpha, (float)Math.Max(this.fadeOutEasing.Value, 0))) + using (ImRaii.PushStyle(ImGuiStyleVar.Alpha, (float)this.fadeOutEasing.Value)) { var i = 0; foreach (var entry in entries) @@ -392,7 +392,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable if (overrideAlpha) { - ImGui.PushStyleVar(ImGuiStyleVar.Alpha, showText ? (float)Math.Min(logoEasing.Value, 1) : 0f); + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, showText ? (float)logoEasing.Value : 0f); } // Drop shadow From 3074115b345e51ad354674adec30cbaa2de58efe Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Sun, 23 Mar 2025 18:27:48 -0700 Subject: [PATCH 16/55] chore: Add api 13 tracker --- Dalamud/Utility/Api13ToDoAttribute.cs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 Dalamud/Utility/Api13ToDoAttribute.cs diff --git a/Dalamud/Utility/Api13ToDoAttribute.cs b/Dalamud/Utility/Api13ToDoAttribute.cs new file mode 100644 index 000000000..576401cda --- /dev/null +++ b/Dalamud/Utility/Api13ToDoAttribute.cs @@ -0,0 +1,24 @@ +namespace Dalamud.Utility; + +/// +/// Utility class for marking something to be changed for API 13, for ease of lookup. +/// +[AttributeUsage(AttributeTargets.All, Inherited = false)] +internal sealed class Api13ToDoAttribute : Attribute +{ + /// + /// Marks that this should be made internal. + /// + public const string MakeInternal = "Make internal."; + + /// + /// Initializes a new instance of the class. + /// + /// The explanation. + /// The explanation 2. + public Api13ToDoAttribute(string what, string what2 = "") + { + _ = what; + _ = what2; + } +} From e84654005e18e80ccf654df9372a96817c0aebae Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Sun, 23 Mar 2025 18:28:07 -0700 Subject: [PATCH 17/55] fix: Force devs to choose between ValueClamped and ValueUnclamped --- Dalamud/Interface/Animation/Easing.cs | 11 ++++++++++- .../Internal/ActiveNotification.ImGui.cs | 12 ++++++------ .../ImGuiNotification/Internal/ActiveNotification.cs | 2 +- .../Internal/Windows/ComponentDemoWindow.cs | 2 +- .../Internal/Windows/TitleScreenMenuWindow.cs | 10 +++++----- 5 files changed, 23 insertions(+), 14 deletions(-) diff --git a/Dalamud/Interface/Animation/Easing.cs b/Dalamud/Interface/Animation/Easing.cs index a48300a22..8191487f4 100644 --- a/Dalamud/Interface/Animation/Easing.cs +++ b/Dalamud/Interface/Animation/Easing.cs @@ -1,6 +1,8 @@ using System.Diagnostics; using System.Numerics; +using Dalamud.Utility; + namespace Dalamud.Interface.Animation; /// @@ -42,10 +44,17 @@ public abstract class Easing /// public bool IsInverse { get; set; } + /// + /// Gets the current value of the animation, following unclamped logic. + /// + [Obsolete($"This field has been deprecated. Use either {nameof(ValueClamped)} or {nameof(ValueUnclamped)} instead.", true)] + [Api13ToDo("Map this field to ValueClamped, probably.")] + public double Value => this.ValueUnclamped; + /// /// Gets the current value of the animation, from 0 to 1. /// - public double Value => Math.Clamp(this.ValueUnclamped, 0, 1); + public double ValueClamped => Math.Clamp(this.ValueUnclamped, 0, 1); /// /// Gets or sets the current value of the animation, not limited to a range of 0 to 1. diff --git a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs index 16d58bea5..ab41c5521 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs @@ -21,8 +21,8 @@ internal sealed partial class ActiveNotification var opacity = Math.Clamp( (float)(this.hideEasing.IsRunning - ? (this.hideEasing.IsDone || ReducedMotions ? 0 : 1f - this.hideEasing.Value) - : (this.showEasing.IsDone || ReducedMotions ? 1 : this.showEasing.Value)), + ? (this.hideEasing.IsDone || ReducedMotions ? 0 : 1f - this.hideEasing.ValueClamped) + : (this.showEasing.IsDone || ReducedMotions ? 1 : this.showEasing.ValueClamped)), 0f, 1f); if (opacity <= 0) @@ -106,7 +106,7 @@ internal sealed partial class ActiveNotification } else if (this.expandoEasing.IsRunning) { - var easedValue = ReducedMotions ? 1f : (float)this.expandoEasing.Value; + var easedValue = ReducedMotions ? 1f : (float)this.expandoEasing.ValueClamped; if (this.underlyingNotification.Minimized) ImGui.PushStyleVar(ImGuiStyleVar.Alpha, opacity * (1f - easedValue)); else @@ -295,8 +295,8 @@ internal sealed partial class ActiveNotification { relativeOpacity = this.underlyingNotification.Minimized - ? 1f - (float)this.expandoEasing.Value - : (float)this.expandoEasing.Value; + ? 1f - (float)this.expandoEasing.ValueClamped + : (float)this.expandoEasing.ValueClamped; } else { @@ -543,7 +543,7 @@ internal sealed partial class ActiveNotification float barL, barR; if (this.DismissReason is not null) { - var v = this.hideEasing.IsDone || ReducedMotions ? 0f : 1f - (float)this.hideEasing.Value; + var v = this.hideEasing.IsDone || ReducedMotions ? 0f : 1f - (float)this.hideEasing.ValueClamped; var midpoint = (this.prevProgressL + this.prevProgressR) / 2f; var length = (this.prevProgressR - this.prevProgressL) / 2f; barL = midpoint - (length * v); diff --git a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs index 607c7c49d..89a175a9e 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs @@ -226,7 +226,7 @@ internal sealed partial class ActiveNotification : IActiveNotification if (Math.Abs(underlyingProgress - this.progressBefore) < 0.000001f || this.progressEasing.IsDone || ReducedMotions) return underlyingProgress; - var state = ReducedMotions ? 1f : Math.Clamp((float)this.progressEasing.Value, 0f, 1f); + var state = ReducedMotions ? 1f : Math.Clamp((float)this.progressEasing.ValueClamped, 0f, 1f); return this.progressBefore + (state * (underlyingProgress - this.progressBefore)); } } diff --git a/Dalamud/Interface/Internal/Windows/ComponentDemoWindow.cs b/Dalamud/Interface/Internal/Windows/ComponentDemoWindow.cs index 0b704990b..c0d2e4c61 100644 --- a/Dalamud/Interface/Internal/Windows/ComponentDemoWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ComponentDemoWindow.cs @@ -147,7 +147,7 @@ internal sealed class ComponentDemoWindow : Window ImGui.Bullet(); ImGui.SetCursorPos(cursor + new Vector2(0, 10)); - ImGui.Text($"{easing.GetType().Name} ({easing.Value})"); + ImGui.Text($"{easing.GetType().Name} ({easing.ValueClamped})"); ImGuiHelpers.ScaledDummy(5); } } diff --git a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs index c1f0b2a67..3b6140b8c 100644 --- a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs +++ b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs @@ -204,7 +204,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable moveEasing.Update(); var finalPos = (i + 1) * this.shadeTexture.Value.Height * scale; - var pos = moveEasing.Value * finalPos; + var pos = moveEasing.ValueClamped * finalPos; // FIXME(goat): Sometimes, easings can overshoot and bring things out of alignment. if (moveEasing.IsDone) @@ -251,7 +251,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable this.fadeOutEasing.Update(); - using (ImRaii.PushStyle(ImGuiStyleVar.Alpha, (float)this.fadeOutEasing.Value)) + using (ImRaii.PushStyle(ImGuiStyleVar.Alpha, (float)this.fadeOutEasing.ValueClamped)) { var i = 0; foreach (var entry in entries) @@ -317,7 +317,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable var initialCursor = ImGui.GetCursorPos(); - using (ImRaii.PushStyle(ImGuiStyleVar.Alpha, (float)shadeEasing.Value)) + using (ImRaii.PushStyle(ImGuiStyleVar.Alpha, (float)shadeEasing.ValueClamped)) { var texture = this.shadeTexture.Value; ImGui.Image(texture.ImGuiHandle, new Vector2(texture.Width, texture.Height) * scale); @@ -367,7 +367,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable if (overrideAlpha) { - ImGui.PushStyleVar(ImGuiStyleVar.Alpha, isFirst ? 1f : (float)logoEasing.Value); + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, isFirst ? 1f : (float)logoEasing.ValueClamped); } else if (isFirst) { @@ -392,7 +392,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable if (overrideAlpha) { - ImGui.PushStyleVar(ImGuiStyleVar.Alpha, showText ? (float)logoEasing.Value : 0f); + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, showText ? (float)logoEasing.ValueClamped : 0f); } // Drop shadow From aaa3e33a8b14c6f43602dfad73759a1623c1a12b Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Sun, 23 Mar 2025 18:37:44 -0700 Subject: [PATCH 18/55] chore: PluginLoadReason is flags --- Dalamud/Plugin/PluginLoadReason.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Dalamud/Plugin/PluginLoadReason.cs b/Dalamud/Plugin/PluginLoadReason.cs index d4c1a3b26..de84e3368 100644 --- a/Dalamud/Plugin/PluginLoadReason.cs +++ b/Dalamud/Plugin/PluginLoadReason.cs @@ -3,6 +3,7 @@ namespace Dalamud.Plugin; /// /// This enum reflects reasons for loading a plugin. /// +[Flags] public enum PluginLoadReason { /// @@ -30,5 +31,3 @@ public enum PluginLoadReason /// Boot, } - -// TODO(api9): This should be a mask, so that we can combine Installer | ProfileLoaded From 8939cac80fc72349e26d555d2417483021c2cb83 Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Sun, 23 Mar 2025 18:45:15 -0700 Subject: [PATCH 19/55] fix: actually add values --- Dalamud/Plugin/PluginLoadReason.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Dalamud/Plugin/PluginLoadReason.cs b/Dalamud/Plugin/PluginLoadReason.cs index de84e3368..2b494c549 100644 --- a/Dalamud/Plugin/PluginLoadReason.cs +++ b/Dalamud/Plugin/PluginLoadReason.cs @@ -9,25 +9,25 @@ public enum PluginLoadReason /// /// We don't know why this plugin was loaded. /// - Unknown, + Unknown = 1 << 0, /// /// This plugin was loaded because it was installed with the plugin installer. /// - Installer, + Installer = 1 << 1, /// /// This plugin was loaded because it was just updated. /// - Update, + Update = 1 << 2, /// /// This plugin was loaded because it was told to reload. /// - Reload, + Reload = 1 << 3, /// /// This plugin was loaded because the game was started or Dalamud was reinjected. /// - Boot, + Boot = 1 << 4, } From 9815cf1d88de1c6eca39b6ef5683e6564b13eb68 Mon Sep 17 00:00:00 2001 From: Vanillaaaa Date: Mon, 24 Mar 2025 12:38:47 +0800 Subject: [PATCH 20/55] perf: show whether plugin toggle is enable or disable (#2189) --- .../Profiles/PluginManagementCommandHandler.cs | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/Dalamud/Plugin/Internal/Profiles/PluginManagementCommandHandler.cs b/Dalamud/Plugin/Internal/Profiles/PluginManagementCommandHandler.cs index ad5aad286..09cceebcb 100644 --- a/Dalamud/Plugin/Internal/Profiles/PluginManagementCommandHandler.cs +++ b/Dalamud/Plugin/Internal/Profiles/PluginManagementCommandHandler.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -216,7 +216,12 @@ internal class PluginManagementCommandHandler : IInternalDisposableService this.chat.Print(onSuccess); } - + + if (operation == PluginCommandOperation.Toggle) + { + operation = plugin.State == PluginState.Loaded ? PluginCommandOperation.Disable : PluginCommandOperation.Enable; + } + switch (operation) { case PluginCommandOperation.Enable: @@ -235,14 +240,6 @@ internal class PluginManagementCommandHandler : IInternalDisposableService Loc.Localize("PluginCommandsDisableFailed", "Failed to disable plugin \"{0}\". Please check the console for errors.").Format(plugin.Name))) .ConfigureAwait(false); break; - case PluginCommandOperation.Toggle: - this.chat.Print(Loc.Localize("PluginCommandsToggling", "Toggling plugin \"{0}\"...").Format(plugin.Name)); - Task.Run(() => plugin.State == PluginState.Loaded ? plugin.UnloadAsync() : plugin.LoadAsync(PluginLoadReason.Installer)) - .ContinueWith(t => Continuation(t, - Loc.Localize("PluginCommandsToggleSuccess", "Plugin \"{0}\" toggled.").Format(plugin.Name), - Loc.Localize("PluginCommandsToggleFailed", "Failed to toggle plugin \"{0}\". Please check the console for errors.").Format(plugin.Name))) - .ConfigureAwait(false); - break; default: throw new ArgumentOutOfRangeException(nameof(operation), operation, null); } From 7cac19ce814e3e2b7ccec9b3c4eb3fb93babf1bf Mon Sep 17 00:00:00 2001 From: marzent Date: Mon, 24 Mar 2025 16:42:24 +0100 Subject: [PATCH 21/55] Implement dalamud-platform launch argument (#1452) * implement dalamud platform launch arg * implement cross-platform gamePath fallback * refactor platform detection heuristic * add cross platform dalamud runtime detection --- Dalamud.Boot/DalamudStartInfo.cpp | 1 + Dalamud.Boot/DalamudStartInfo.h | 1 + Dalamud.Boot/utils.cpp | 5 + Dalamud.Common/DalamudStartInfo.cs | 9 +- Dalamud.Common/OSPlatformConverter.cs | 78 ++++++++++++ Dalamud.Injector/EntryPoint.cs | 113 ++++++++++++++---- Dalamud.Injector/NativeFunctions.cs | 42 +++++++ .../Internal/EnvironmentConfiguration.cs | 5 - Dalamud/EntryPoint.cs | 4 + Dalamud/Utility/Util.cs | 50 +------- lib/CoreCLR/boot.cpp | 92 +++++++++----- 11 files changed, 299 insertions(+), 101 deletions(-) create mode 100644 Dalamud.Common/OSPlatformConverter.cs diff --git a/Dalamud.Boot/DalamudStartInfo.cpp b/Dalamud.Boot/DalamudStartInfo.cpp index 52f201fea..4aa7d46dd 100644 --- a/Dalamud.Boot/DalamudStartInfo.cpp +++ b/Dalamud.Boot/DalamudStartInfo.cpp @@ -109,6 +109,7 @@ void from_json(const nlohmann::json& json, DalamudStartInfo& config) { config.PluginDirectory = json.value("PluginDirectory", config.PluginDirectory); config.AssetDirectory = json.value("AssetDirectory", config.AssetDirectory); config.Language = json.value("Language", config.Language); + config.Platform = json.value("Platform", config.Platform); config.GameVersion = json.value("GameVersion", config.GameVersion); config.TroubleshootingPackData = json.value("TroubleshootingPackData", std::string{}); config.DelayInitializeMs = json.value("DelayInitializeMs", config.DelayInitializeMs); diff --git a/Dalamud.Boot/DalamudStartInfo.h b/Dalamud.Boot/DalamudStartInfo.h index d5a6a6aec..64450e290 100644 --- a/Dalamud.Boot/DalamudStartInfo.h +++ b/Dalamud.Boot/DalamudStartInfo.h @@ -47,6 +47,7 @@ struct DalamudStartInfo { std::string PluginDirectory; std::string AssetDirectory; ClientLanguage Language = ClientLanguage::English; + std::string Platform; std::string GameVersion; std::string TroubleshootingPackData; int DelayInitializeMs = 0; diff --git a/Dalamud.Boot/utils.cpp b/Dalamud.Boot/utils.cpp index bbe47db82..dbfcf39ee 100644 --- a/Dalamud.Boot/utils.cpp +++ b/Dalamud.Boot/utils.cpp @@ -1,4 +1,5 @@ #include "pch.h" +#include "DalamudStartInfo.h" #include "utils.h" @@ -584,6 +585,10 @@ std::vector utils::get_env_list(const wchar_t* pcszName) { return res; } +bool utils::is_running_on_wine() { + return g_startInfo.Platform != "WINDOWS"; +} + std::filesystem::path utils::get_module_path(HMODULE hModule) { std::wstring buf(MAX_PATH, L'\0'); while (true) { diff --git a/Dalamud.Common/DalamudStartInfo.cs b/Dalamud.Common/DalamudStartInfo.cs index ca81d1281..eb2410cfd 100644 --- a/Dalamud.Common/DalamudStartInfo.cs +++ b/Dalamud.Common/DalamudStartInfo.cs @@ -1,3 +1,4 @@ +using System.Runtime.InteropServices; using Dalamud.Common.Game; using Newtonsoft.Json; @@ -15,7 +16,7 @@ public record DalamudStartInfo /// public DalamudStartInfo() { - // ignored + this.Platform = OSPlatform.Create("UNKNOWN"); } /// @@ -58,6 +59,12 @@ public record DalamudStartInfo /// public ClientLanguage Language { get; set; } = ClientLanguage.English; + /// + /// Gets or sets the underlying platform�Dalamud runs on. + /// + [JsonConverter(typeof(OSPlatformConverter))] + public OSPlatform Platform { get; set; } + /// /// Gets or sets the current game version code. /// diff --git a/Dalamud.Common/OSPlatformConverter.cs b/Dalamud.Common/OSPlatformConverter.cs new file mode 100644 index 000000000..62d2996d4 --- /dev/null +++ b/Dalamud.Common/OSPlatformConverter.cs @@ -0,0 +1,78 @@ +using System.Runtime.InteropServices; +using Newtonsoft.Json; + +namespace Dalamud.Common; + +/// +/// Converts a to and from a string (e.g. "FreeBSD"). +/// +public sealed class OSPlatformConverter : JsonConverter +{ + /// + /// Writes the JSON representation of the object. + /// + /// The to write to. + /// The value. + /// The calling serializer. + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) + { + if (value == null) + { + writer.WriteNull(); + } + else if (value is OSPlatform) + { + writer.WriteValue(value.ToString()); + } + else + { + throw new JsonSerializationException("Expected OSPlatform object value"); + } + } + + /// + /// Reads the JSON representation of the object. + /// + /// The to read from. + /// Type of the object. + /// The existing property value of the JSON that is being converted. + /// The calling serializer. + /// The object value. + public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null) + { + return null; + } + else + { + if (reader.TokenType == JsonToken.String) + { + try + { + return OSPlatform.Create((string)reader.Value!); + } + catch (Exception ex) + { + throw new JsonSerializationException($"Error parsing OSPlatform string: {reader.Value}", ex); + } + } + else + { + throw new JsonSerializationException($"Unexpected token or value when parsing OSPlatform. Token: {reader.TokenType}, Value: {reader.Value}"); + } + } + } + + /// + /// Determines whether this instance can convert the specified object type. + /// + /// Type of the object. + /// + /// true if this instance can convert the specified object type; otherwise, false. + /// + public override bool CanConvert(Type objectType) + { + return objectType == typeof(OSPlatform); + } +} diff --git a/Dalamud.Injector/EntryPoint.cs b/Dalamud.Injector/EntryPoint.cs index 9e7033c4d..0ee2e5507 100644 --- a/Dalamud.Injector/EntryPoint.cs +++ b/Dalamud.Injector/EntryPoint.cs @@ -263,6 +263,35 @@ namespace Dalamud.Injector } } + private static OSPlatform DetectPlatformHeuristic() + { + var ntdll = NativeFunctions.GetModuleHandleW("ntdll.dll"); + var wineServerCallPtr = NativeFunctions.GetProcAddress(ntdll, "wine_server_call"); + var wineGetHostVersionPtr = NativeFunctions.GetProcAddress(ntdll, "wine_get_host_version"); + var winePlatform = GetWinePlatform(wineGetHostVersionPtr); + var isWine = wineServerCallPtr != nint.Zero; + + static unsafe string? GetWinePlatform(nint wineGetHostVersionPtr) + { + if (wineGetHostVersionPtr == nint.Zero) return null; + + var methodDelegate = (delegate* unmanaged[Fastcall])wineGetHostVersionPtr; + methodDelegate(out var platformPtr, out var _); + + if (platformPtr == null) return null; + + return Marshal.PtrToStringAnsi((nint)platformPtr); + } + + if (!isWine) + return OSPlatform.Windows; + + if (winePlatform == "Darwin") + return OSPlatform.OSX; + + return OSPlatform.Linux; + } + private static DalamudStartInfo ExtractAndInitializeStartInfoFromArguments(DalamudStartInfo? startInfo, List args) { int len; @@ -278,6 +307,7 @@ namespace Dalamud.Injector var logName = startInfo.LogName; var logPath = startInfo.LogPath; var languageStr = startInfo.Language.ToString().ToLowerInvariant(); + var platformStr = startInfo.Platform.ToString().ToLowerInvariant(); var unhandledExceptionStr = startInfo.UnhandledException.ToString().ToLowerInvariant(); var troubleshootingData = "{\"empty\": true, \"description\": \"No troubleshooting data supplied.\"}"; @@ -307,6 +337,10 @@ namespace Dalamud.Injector { languageStr = args[i][key.Length..].ToLowerInvariant(); } + else if (args[i].StartsWith(key = "--dalamud-platform=")) + { + platformStr = args[i][key.Length..].ToLowerInvariant(); + } else if (args[i].StartsWith(key = "--dalamud-tspack-b64=")) { troubleshootingData = Encoding.UTF8.GetString(Convert.FromBase64String(args[i][key.Length..])); @@ -378,11 +412,35 @@ namespace Dalamud.Injector throw new CommandLineException($"\"{languageStr}\" is not a valid supported language."); } + OSPlatform platform; + if (platformStr[0..(len = Math.Min(platformStr.Length, (key = "win").Length))] == key[0..len]) // covers both win32 and Windows + { + platform = OSPlatform.Windows; + } + else if (platformStr[0..(len = Math.Min(platformStr.Length, (key = "linux").Length))] == key[0..len]) + { + platform = OSPlatform.Linux; + } + else if (platformStr[0..(len = Math.Min(platformStr.Length, (key = "macos").Length))] == key[0..len]) + { + platform = OSPlatform.OSX; + } + else if (platformStr[0..(len = Math.Min(platformStr.Length, (key = "osx").Length))] == key[0..len]) + { + platform = OSPlatform.OSX; + } + else + { + platform = DetectPlatformHeuristic(); + Log.Warning("Heuristically determined host system platform as {platform}", platform); + } + startInfo.WorkingDirectory = workingDirectory; startInfo.ConfigurationPath = configurationPath; startInfo.PluginDirectory = pluginDirectory; startInfo.AssetDirectory = assetDirectory; startInfo.Language = clientLanguage; + startInfo.Platform = platform; startInfo.DelayInitializeMs = delayInitializeMs; startInfo.GameVersion = null; startInfo.TroubleshootingPackData = troubleshootingData; @@ -465,7 +523,7 @@ namespace Dalamud.Injector } Console.WriteLine("Specifying dalamud start info: [--dalamud-working-directory=path] [--dalamud-configuration-path=path]"); - Console.WriteLine(" [--dalamud-plugin-directory=path]"); + Console.WriteLine(" [--dalamud-plugin-directory=path] [--dalamud-platform=win32|linux|macOS]"); Console.WriteLine(" [--dalamud-asset-directory=path] [--dalamud-delay-initialize=0(ms)]"); Console.WriteLine(" [--dalamud-client-language=0-3|j(apanese)|e(nglish)|d|g(erman)|f(rench)]"); @@ -732,15 +790,42 @@ namespace Dalamud.Injector { try { - var appDataDir = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); - var xivlauncherDir = Path.Combine(appDataDir, "XIVLauncher"); - var launcherConfigPath = Path.Combine(xivlauncherDir, "launcherConfigV3.json"); - gamePath = Path.Combine(JsonSerializer.CreateDefault().Deserialize>(new JsonTextReader(new StringReader(File.ReadAllText(launcherConfigPath))))["GamePath"], "game", "ffxiv_dx11.exe"); - Log.Information("Using game installation path configuration from from XIVLauncher: {0}", gamePath); + if (dalamudStartInfo.Platform == OSPlatform.Windows) + { + var appDataDir = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + var xivlauncherDir = Path.Combine(appDataDir, "XIVLauncher"); + var launcherConfigPath = Path.Combine(xivlauncherDir, "launcherConfigV3.json"); + gamePath = Path.Combine( + JsonSerializer.CreateDefault() + .Deserialize>( + new JsonTextReader(new StringReader(File.ReadAllText(launcherConfigPath))))["GamePath"], + "game", + "ffxiv_dx11.exe"); + Log.Information("Using game installation path configuration from from XIVLauncher: {0}", gamePath); + } + else if (dalamudStartInfo.Platform == OSPlatform.Linux) + { + var homeDir = $"Z:\\home\\{Environment.UserName}"; + var xivlauncherDir = Path.Combine(homeDir, ".xlcore"); + var launcherConfigPath = Path.Combine(xivlauncherDir, "launcher.ini"); + var config = File.ReadAllLines(launcherConfigPath) + .Where(line => line.Contains('=')) + .ToDictionary(line => line.Split('=')[0], line => line.Split('=')[1]); + gamePath = Path.Combine("Z:" + config["GamePath"].Replace('/', '\\'), "game", "ffxiv_dx11.exe"); + Log.Information("Using game installation path configuration from from XIVLauncher Core: {0}", gamePath); + } + else + { + var homeDir = $"Z:\\Users\\{Environment.UserName}"; + var xomlauncherDir = Path.Combine(homeDir, "Library", "Application Support", "XIV on Mac"); + // we could try to parse the binary plist file here if we really wanted to... + gamePath = Path.Combine(xomlauncherDir, "ffxiv", "game", "ffxiv_dx11.exe"); + Log.Information("Using default game installation path from XOM: {0}", gamePath); + } } catch (Exception) { - Log.Error("Failed to read launcherConfigV3.json to get the set-up game path, please specify one using -g"); + Log.Error("Failed to read launcher config to get the set-up game path, please specify one using -g"); return -1; } @@ -795,20 +880,6 @@ namespace Dalamud.Injector if (encryptArguments) { var rawTickCount = (uint)Environment.TickCount; - - if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - { - [System.Runtime.InteropServices.DllImport("c")] -#pragma warning disable SA1300 - static extern ulong clock_gettime_nsec_np(int clockId); -#pragma warning restore SA1300 - - const int CLOCK_MONOTONIC_RAW = 4; - var rawTickCountFixed = clock_gettime_nsec_np(CLOCK_MONOTONIC_RAW) / 1000000; - Log.Information("ArgumentBuilder::DeriveKey() fixing up rawTickCount from {0} to {1} on macOS", rawTickCount, rawTickCountFixed); - rawTickCount = (uint)rawTickCountFixed; - } - var ticks = rawTickCount & 0xFFFF_FFFFu; var key = ticks & 0xFFFF_0000u; gameArguments.Insert(0, $"T={ticks}"); diff --git a/Dalamud.Injector/NativeFunctions.cs b/Dalamud.Injector/NativeFunctions.cs index 2a4654aaf..06add3acc 100644 --- a/Dalamud.Injector/NativeFunctions.cs +++ b/Dalamud.Injector/NativeFunctions.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; namespace Dalamud.Injector @@ -910,5 +911,46 @@ namespace Dalamud.Injector uint dwDesiredAccess, [MarshalAs(UnmanagedType.Bool)] bool bInheritHandle, DuplicateOptions dwOptions); + + /// + /// See https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-getmodulehandlew. + /// Retrieves a module handle for the specified module. The module must have been loaded by the calling process. To + /// avoid the race conditions described in the Remarks section, use the GetModuleHandleEx function. + /// + /// + /// The name of the loaded module (either a .dll or .exe file). If the file name extension is omitted, the default + /// library extension .dll is appended. The file name string can include a trailing point character (.) to indicate + /// that the module name has no extension. The string does not have to specify a path. When specifying a path, be sure + /// to use backslashes (\), not forward slashes (/). The name is compared (case independently) to the names of modules + /// currently mapped into the address space of the calling process. If this parameter is NULL, GetModuleHandle returns + /// a handle to the file used to create the calling process (.exe file). The GetModuleHandle function does not retrieve + /// handles for modules that were loaded using the LOAD_LIBRARY_AS_DATAFILE flag.For more information, see LoadLibraryEx. + /// + /// + /// If the function succeeds, the return value is a handle to the specified module. If the function fails, the return + /// value is NULL.To get extended error information, call GetLastError. + /// + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)] + public static extern IntPtr GetModuleHandleW(string lpModuleName); + + /// + /// Retrieves the address of an exported function or variable from the specified dynamic-link library (DLL). + /// + /// + /// A handle to the DLL module that contains the function or variable. The LoadLibrary, LoadLibraryEx, LoadPackagedLibrary, + /// or GetModuleHandle function returns this handle. The GetProcAddress function does not retrieve addresses from modules + /// that were loaded using the LOAD_LIBRARY_AS_DATAFILE flag.For more information, see LoadLibraryEx. + /// + /// + /// The function or variable name, or the function's ordinal value. If this parameter is an ordinal value, it must be + /// in the low-order word; the high-order word must be zero. + /// + /// + /// If the function succeeds, the return value is the address of the exported function or variable. If the function + /// fails, the return value is NULL.To get extended error information, call GetLastError. + /// + [DllImport("kernel32", CharSet = CharSet.Ansi, ExactSpelling = true, SetLastError = true)] + [SuppressMessage("Globalization", "CA2101:Specify marshaling for P/Invoke string arguments", Justification = "Ansi only")] + public static extern IntPtr GetProcAddress(IntPtr hModule, string procName); } } diff --git a/Dalamud/Configuration/Internal/EnvironmentConfiguration.cs b/Dalamud/Configuration/Internal/EnvironmentConfiguration.cs index 2df9ec5fe..11a8d3567 100644 --- a/Dalamud/Configuration/Internal/EnvironmentConfiguration.cs +++ b/Dalamud/Configuration/Internal/EnvironmentConfiguration.cs @@ -5,11 +5,6 @@ namespace Dalamud.Configuration.Internal; ///
internal class EnvironmentConfiguration { - /// - /// Gets a value indicating whether the XL_WINEONLINUX setting has been enabled. - /// - public static bool XlWineOnLinux { get; } = GetEnvironmentVariable("XL_WINEONLINUX"); - /// /// Gets a value indicating whether the DALAMUD_NOT_HAVE_PLUGINS setting has been enabled. /// diff --git a/Dalamud/EntryPoint.cs b/Dalamud/EntryPoint.cs index 89e4e0e5e..ebfc975ff 100644 --- a/Dalamud/EntryPoint.cs +++ b/Dalamud/EntryPoint.cs @@ -181,7 +181,11 @@ public sealed class EntryPoint // Apply common fixes for culture issues CultureFixes.Apply(); + + // This is due to GitHub not supporting TLS 1.0, so we enable all TLS versions globally + ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12 | SecurityProtocolType.Tls; + // Currently VEH is not fully functional on WINE if (!Util.IsWine()) InitSymbolHandler(info); diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs index 7724c68e0..2fcc4806a 100644 --- a/Dalamud/Utility/Util.cs +++ b/Dalamud/Utility/Util.cs @@ -19,6 +19,7 @@ using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Interface.Colors; using Dalamud.Interface.Utility; +using Dalamud.Logging.Internal; using Dalamud.Interface.Utility.Raii; using Dalamud.Support; using ImGuiNET; @@ -500,55 +501,14 @@ public static class Util /// Determine if Dalamud is currently running within a Wine context (e.g. either on macOS or Linux). This method /// will not return information about the host operating system. ///
- /// Returns true if Wine is detected, false otherwise. - public static bool IsWine() - { - if (EnvironmentConfiguration.XlWineOnLinux) return true; - if (Environment.GetEnvironmentVariable("XL_PLATFORM") is not null and not "Windows") return true; - - var ntdll = NativeFunctions.GetModuleHandleW("ntdll.dll"); - - // Test to see if any Wine specific exports exist. If they do, then we are running on Wine. - // The exports "wine_get_version", "wine_get_build_id", and "wine_get_host_version" will tend to be hidden - // by most Linux users (else FFXIV will want a macOS license), so we will additionally check some lesser-known - // exports as well. - return AnyProcExists( - ntdll, - "wine_get_version", - "wine_get_build_id", - "wine_get_host_version", - "wine_server_call", - "wine_unix_to_nt_file_name"); - - bool AnyProcExists(nint handle, params string[] procs) => - procs.Any(p => NativeFunctions.GetProcAddress(handle, p) != nint.Zero); - } + /// Returns true if running on Wine, false otherwise. + public static bool IsWine() => Service.Get().StartInfo.Platform != OSPlatform.Windows; /// - /// Gets the best guess for the current host's platform based on the XL_PLATFORM environment variable or - /// heuristics. + /// Gets the current host's platform based on the injector launch arguments or heuristics. /// - /// - /// macOS users running without XL_PLATFORM being set will be reported as Linux users. Due to the way our - /// Wines work, there isn't a great (consistent) way to split the two apart if we're not told. - /// /// Returns the that Dalamud is currently running on. - public static OSPlatform GetHostPlatform() - { - switch (Environment.GetEnvironmentVariable("XL_PLATFORM")) - { - case "Windows": return OSPlatform.Windows; - case "MacOS": return OSPlatform.OSX; - case "Linux": return OSPlatform.Linux; - } - - // n.b. we had some fancy code here to check if the Wine host version returned "Darwin" but apparently - // *all* our Wines report Darwin if exports aren't hidden. As such, it is effectively impossible (without some - // (very cursed and inaccurate heuristics) to determine if we're on macOS or Linux unless we're explicitly told - // by our launcher. See commit a7aacb15e4603a367e2f980578271a9a639d8852 for the old check. - - return IsWine() ? OSPlatform.Linux : OSPlatform.Windows; - } + public static OSPlatform GetHostPlatform() => Service.Get().StartInfo.Platform; /// /// Heuristically determine if the Windows version is higher than Windows 11's first build. diff --git a/lib/CoreCLR/boot.cpp b/lib/CoreCLR/boot.cpp index 723a1ed21..f5e626c3f 100644 --- a/lib/CoreCLR/boot.cpp +++ b/lib/CoreCLR/boot.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include "CoreCLR.h" #include "..\..\Dalamud.Boot\logging.h" @@ -27,6 +28,64 @@ void ConsoleTeardown() std::optional g_clr; +static wchar_t* GetRuntimePath() +{ + int result; + std::wstring buffer; + wchar_t* runtime_path; + wchar_t* _appdata; + DWORD username_len = UNLEN + 1; + wchar_t username[UNLEN + 1]; + + buffer.resize(0); + result = GetEnvironmentVariableW(L"DALAMUD_RUNTIME", &buffer[0], 0); + + if (result) + { + buffer.resize(result); // The first pass returns the required length + result = GetEnvironmentVariableW(L"DALAMUD_RUNTIME", &buffer[0], result); + return _wcsdup(buffer.c_str()); + } + + // Detect Windows first + result = SHGetKnownFolderPath(FOLDERID_RoamingAppData, KF_FLAG_DEFAULT, nullptr, &_appdata); + + if (result != 0) + { + logging::E("Unable to get RoamingAppData path (err={})", result); + return NULL; + } + + std::filesystem::path fs_app_data(_appdata); + runtime_path = _wcsdup(fs_app_data.append("XIVLauncher").append("runtime").c_str()); + if (std::filesystem::exists(runtime_path)) + return runtime_path; + free(runtime_path); + + // Next XLCore on Linux + result = GetUserNameW(username, &username_len); + if (result != 0) + { + logging::E("Unable to get user name (err={})", result); + return NULL; + } + + std::filesystem::path homeDir = L"Z:\\home\\" + std::wstring(username); + runtime_path = _wcsdup(homeDir.append(".xlcore").append("runtime").c_str()); + if (std::filesystem::exists(runtime_path)) + return runtime_path; + free(runtime_path); + + // Finally XOM + homeDir = L"Z:\\Users\\" + std::wstring(username); + runtime_path = _wcsdup(homeDir.append("Library").append("Application Suppor").append("XIV on Mac").append("runtime").c_str()); + if (std::filesystem::exists(runtime_path)) + return runtime_path; + free(runtime_path); + + return NULL; +} + HRESULT InitializeClrAndGetEntryPoint( void* calling_module, bool enable_etw, @@ -62,31 +121,12 @@ HRESULT InitializeClrAndGetEntryPoint( SetEnvironmentVariable(L"COMPlus_ETWEnabled", enable_etw ? L"1" : L"0"); - wchar_t* dotnet_path; - wchar_t* _appdata; + wchar_t* dotnet_path = GetRuntimePath(); - std::wstring buffer; - buffer.resize(0); - result = GetEnvironmentVariableW(L"DALAMUD_RUNTIME", &buffer[0], 0); - - if (result) + if (!dotnet_path || !std::filesystem::exists(dotnet_path)) { - buffer.resize(result); // The first pass returns the required length - result = GetEnvironmentVariableW(L"DALAMUD_RUNTIME", &buffer[0], result); - dotnet_path = _wcsdup(buffer.c_str()); - } - else - { - result = SHGetKnownFolderPath(FOLDERID_RoamingAppData, KF_FLAG_DEFAULT, nullptr, &_appdata); - - if (result != 0) - { - logging::E("Unable to get RoamingAppData path (err={})", result); - return HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND); - } - - std::filesystem::path fs_app_data(_appdata); - dotnet_path = _wcsdup(fs_app_data.append("XIVLauncher").append("runtime").c_str()); + logging::E("Error: Unable to find .NET runtime path"); + return 1; } // =========================================================================== // @@ -95,12 +135,6 @@ HRESULT InitializeClrAndGetEntryPoint( logging::I("with config_path: {}", runtimeconfig_path); logging::I("with module_path: {}", module_path); - if (!std::filesystem::exists(dotnet_path)) - { - logging::E("Error: Unable to find .NET runtime path"); - return HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND); - } - get_hostfxr_parameters init_parameters { sizeof(get_hostfxr_parameters), From fdbfdbb2cdc348e1c7bb114287cc93c16cb34967 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Mon, 24 Mar 2025 17:00:27 +0100 Subject: [PATCH 22/55] Add SeStringEvaluator service (#2188) * Add SeStringEvaluator service * Move DrawCopyableText into WidgetUtil * Use Icon2RemapTable in SeStringRenderer * Beautify some code * Make sure to use the correct language * Add SeString Creator widget * Fix getting local parameters * Update expressionNames * misc changes * Use InvariantCulture in TryResolveSheet * Add SeStringEvaluatorAgingStep * Fix item id comparisons * Add SheetRedirectResolverAgingStep * Add NounProcessorAgingStep * Update SeString.CreateItemLink This also adds the internal ItemUtil class. * Fix name of SeStringCreator widget * Add Global Parameters tab to SeStringCreatorWidget * Load widgets on demand * Update SeStringCreatorWidget * Resizable SeStringCreatorWidget panels * Update GamepadStateAgingStep * Experimental status was removed in #2144 * Update SheetRedirectResolver, rewrite Noun params * Fixes for 4 am code * Remove incorrect column offset I have no idea how that happened. * Draw names of linked things --------- Co-authored-by: Soreepeong <3614868+Soreepeong@users.noreply.github.com> Co-authored-by: KazWolfe --- Dalamud/Game/ActionKind.cs | 89 + Dalamud/Game/Gui/GameGui.cs | 2 +- .../Internal/SeStringBuilderIconWrap.cs | 30 + .../Evaluator/Internal/SeStringContext.cs | 83 + .../Evaluator/Internal/SheetRedirectFlags.cs | 49 + .../Internal/SheetRedirectResolver.cs | 232 ++ .../Game/Text/Evaluator/SeStringEvaluator.cs | 1995 +++++++++++++++++ .../Game/Text/Evaluator/SeStringParameter.cs | 79 + .../Text/Noun/Enums/EnglishArticleType.cs | 17 + .../Game/Text/Noun/Enums/FrenchArticleType.cs | 32 + .../Game/Text/Noun/Enums/GermanArticleType.cs | 37 + .../Text/Noun/Enums/JapaneseArticleType.cs | 17 + Dalamud/Game/Text/Noun/NounParams.cs | 73 + Dalamud/Game/Text/Noun/NounProcessor.cs | 461 ++++ .../SeStringHandling/Payloads/ItemPayload.cs | 20 +- .../Game/Text/SeStringHandling/SeString.cs | 80 +- .../Internal/SeStringRenderer.cs | 74 +- .../Internal/Windows/Data/DataWindow.cs | 12 +- .../Internal/Windows/Data/WidgetUtil.cs | 34 + .../Data/Widgets/AtkArrayDataBrowserWidget.cs | 42 +- .../Windows/Data/Widgets/FateTableWidget.cs | 28 +- .../Data/Widgets/NounProcessorWidget.cs | 207 ++ .../Data/Widgets/SeStringCreatorWidget.cs | 1276 +++++++++++ .../AgingSteps/GamepadStateAgingStep.cs | 40 +- .../AgingSteps/NounProcessorAgingStep.cs | 259 +++ .../AgingSteps/SeStringEvaluatorAgingStep.cs | 92 + .../SheetRedirectResolverAgingStep.cs | 130 ++ .../Windows/SelfTest/SelfTestWindow.cs | 3 + Dalamud/Interface/Utility/ImGuiHelpers.cs | 4 - Dalamud/Plugin/Services/ISeStringEvaluator.cs | 79 + Dalamud/Utility/ActionKindExtensions.cs | 26 + Dalamud/Utility/ClientLanguageExtensions.cs | 36 + Dalamud/Utility/ItemUtil.cs | 159 ++ Dalamud/Utility/ObjectKindExtensions.cs | 33 + Dalamud/Utility/SeStringExtensions.cs | 152 ++ Dalamud/Utility/StringExtensions.cs | 45 + 36 files changed, 5831 insertions(+), 196 deletions(-) create mode 100644 Dalamud/Game/ActionKind.cs create mode 100644 Dalamud/Game/Text/Evaluator/Internal/SeStringBuilderIconWrap.cs create mode 100644 Dalamud/Game/Text/Evaluator/Internal/SeStringContext.cs create mode 100644 Dalamud/Game/Text/Evaluator/Internal/SheetRedirectFlags.cs create mode 100644 Dalamud/Game/Text/Evaluator/Internal/SheetRedirectResolver.cs create mode 100644 Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs create mode 100644 Dalamud/Game/Text/Evaluator/SeStringParameter.cs create mode 100644 Dalamud/Game/Text/Noun/Enums/EnglishArticleType.cs create mode 100644 Dalamud/Game/Text/Noun/Enums/FrenchArticleType.cs create mode 100644 Dalamud/Game/Text/Noun/Enums/GermanArticleType.cs create mode 100644 Dalamud/Game/Text/Noun/Enums/JapaneseArticleType.cs create mode 100644 Dalamud/Game/Text/Noun/NounParams.cs create mode 100644 Dalamud/Game/Text/Noun/NounProcessor.cs create mode 100644 Dalamud/Interface/Internal/Windows/Data/WidgetUtil.cs create mode 100644 Dalamud/Interface/Internal/Windows/Data/Widgets/NounProcessorWidget.cs create mode 100644 Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs create mode 100644 Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/NounProcessorAgingStep.cs create mode 100644 Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/SeStringEvaluatorAgingStep.cs create mode 100644 Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/SheetRedirectResolverAgingStep.cs create mode 100644 Dalamud/Plugin/Services/ISeStringEvaluator.cs create mode 100644 Dalamud/Utility/ActionKindExtensions.cs create mode 100644 Dalamud/Utility/ItemUtil.cs create mode 100644 Dalamud/Utility/ObjectKindExtensions.cs diff --git a/Dalamud/Game/ActionKind.cs b/Dalamud/Game/ActionKind.cs new file mode 100644 index 000000000..9a574f9a8 --- /dev/null +++ b/Dalamud/Game/ActionKind.cs @@ -0,0 +1,89 @@ +namespace Dalamud.Game; + +/// +/// Enum describing possible action kinds. +/// +public enum ActionKind +{ + /// + /// A Trait. + /// + Trait = 0, + + /// + /// An Action. + /// + Action = 1, + + /// + /// A usable Item. + /// + Item = 2, // does not work? + + /// + /// A usable EventItem. + /// + EventItem = 3, // does not work? + + /// + /// An EventAction. + /// + EventAction = 4, + + /// + /// A GeneralAction. + /// + GeneralAction = 5, + + /// + /// A BuddyAction. + /// + BuddyAction = 6, + + /// + /// A MainCommand. + /// + MainCommand = 7, + + /// + /// A Companion. + /// + Companion = 8, // unresolved?! + + /// + /// A CraftAction. + /// + CraftAction = 9, + + /// + /// An Action (again). + /// + Action2 = 10, // what's the difference? + + /// + /// A PetAction. + /// + PetAction = 11, + + /// + /// A CompanyAction. + /// + CompanyAction = 12, + + /// + /// A Mount. + /// + Mount = 13, + + // 14-18 unused + + /// + /// A BgcArmyAction. + /// + BgcArmyAction = 19, + + /// + /// An Ornament. + /// + Ornament = 20, +} diff --git a/Dalamud/Game/Gui/GameGui.cs b/Dalamud/Game/Gui/GameGui.cs index 7cd6d7360..1041464a7 100644 --- a/Dalamud/Game/Gui/GameGui.cs +++ b/Dalamud/Game/Gui/GameGui.cs @@ -323,7 +323,7 @@ internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui return ret; } - private void HandleActionHoverDetour(AgentActionDetail* hoverState, ActionKind actionKind, uint actionId, int a4, byte a5) + private void HandleActionHoverDetour(AgentActionDetail* hoverState, FFXIVClientStructs.FFXIV.Client.UI.Agent.ActionKind actionKind, uint actionId, int a4, byte a5) { this.handleActionHoverHook.Original(hoverState, actionKind, actionId, a4, a5); this.HoveredAction.ActionKind = (HoverActionKind)actionKind; diff --git a/Dalamud/Game/Text/Evaluator/Internal/SeStringBuilderIconWrap.cs b/Dalamud/Game/Text/Evaluator/Internal/SeStringBuilderIconWrap.cs new file mode 100644 index 000000000..65567d240 --- /dev/null +++ b/Dalamud/Game/Text/Evaluator/Internal/SeStringBuilderIconWrap.cs @@ -0,0 +1,30 @@ +using Lumina.Text; + +namespace Dalamud.Game.Text.Evaluator.Internal; + +/// +/// Wraps payloads in an open and close icon, for example the Auto Translation open/close brackets. +/// +internal readonly struct SeStringBuilderIconWrap : IDisposable +{ + private readonly SeStringBuilder builder; + private readonly uint iconClose; + + /// + /// Initializes a new instance of the struct.
+ /// Appends an icon macro with on creation, and an icon macro with + /// on disposal. + ///
+ /// The builder to use. + /// The open icon id. + /// The close icon id. + public SeStringBuilderIconWrap(SeStringBuilder builder, uint iconOpen, uint iconClose) + { + this.builder = builder; + this.iconClose = iconClose; + this.builder.AppendIcon(iconOpen); + } + + /// + public void Dispose() => this.builder.AppendIcon(this.iconClose); +} diff --git a/Dalamud/Game/Text/Evaluator/Internal/SeStringContext.cs b/Dalamud/Game/Text/Evaluator/Internal/SeStringContext.cs new file mode 100644 index 000000000..a32702f6c --- /dev/null +++ b/Dalamud/Game/Text/Evaluator/Internal/SeStringContext.cs @@ -0,0 +1,83 @@ +using System.Globalization; + +using Dalamud.Utility; + +using Lumina.Text; +using Lumina.Text.ReadOnly; + +namespace Dalamud.Game.Text.Evaluator.Internal; + +/// +/// A context wrapper used in . +/// +internal readonly ref struct SeStringContext +{ + /// + /// The to append text and macros to. + /// + internal readonly SeStringBuilder Builder; + + /// + /// A list of local parameters. + /// + internal readonly Span LocalParameters; + + /// + /// The target language, used for sheet lookups. + /// + internal readonly ClientLanguage Language; + + /// + /// Initializes a new instance of the struct. + /// + /// The to append text and macros to. + /// A list of local parameters. + /// The target language, used for sheet lookups. + internal SeStringContext(SeStringBuilder builder, Span localParameters, ClientLanguage language) + { + this.Builder = builder; + this.LocalParameters = localParameters; + this.Language = language; + } + + /// + /// Gets the of the current target . + /// + internal CultureInfo CultureInfo => Localization.GetCultureInfoFromLangCode(this.Language.ToCode()); + + /// + /// Tries to get a number from the local parameters at the specified index. + /// + /// The index in the list. + /// The local parameter number. + /// true if the local parameters list contained a parameter at given index, false otherwise. + internal bool TryGetLNum(int index, out uint value) + { + if (index >= 0 && this.LocalParameters.Length > index) + { + value = this.LocalParameters[index].UIntValue; + return true; + } + + value = 0; + return false; + } + + /// + /// Tries to get a string from the local parameters at the specified index. + /// + /// The index in the list. + /// The local parameter string. + /// true if the local parameters list contained a parameter at given index, false otherwise. + internal bool TryGetLStr(int index, out ReadOnlySeString value) + { + if (index >= 0 && this.LocalParameters.Length > index) + { + value = this.LocalParameters[index].StringValue; + return true; + } + + value = default; + return false; + } +} diff --git a/Dalamud/Game/Text/Evaluator/Internal/SheetRedirectFlags.cs b/Dalamud/Game/Text/Evaluator/Internal/SheetRedirectFlags.cs new file mode 100644 index 000000000..1c1171873 --- /dev/null +++ b/Dalamud/Game/Text/Evaluator/Internal/SheetRedirectFlags.cs @@ -0,0 +1,49 @@ +namespace Dalamud.Game.Text.Evaluator.Internal; + +/// +/// An enum providing additional information about the sheet redirect. +/// +[Flags] +internal enum SheetRedirectFlags +{ + /// + /// No flags. + /// + None = 0, + + /// + /// Resolved to a sheet related with items. + /// + Item = 1, + + /// + /// Resolved to the EventItem sheet. + /// + EventItem = 2, + + /// + /// Resolved to a high quality item. + /// + /// + /// Append Addon#9. + /// + HighQuality = 4, + + /// + /// Resolved to a collectible item. + /// + /// + /// Append Addon#150. + /// + Collectible = 8, + + /// + /// Resolved to a sheet related with actions. + /// + Action = 16, + + /// + /// Resolved to the Action sheet. + /// + ActionSheet = 32, +} diff --git a/Dalamud/Game/Text/Evaluator/Internal/SheetRedirectResolver.cs b/Dalamud/Game/Text/Evaluator/Internal/SheetRedirectResolver.cs new file mode 100644 index 000000000..57a58c80d --- /dev/null +++ b/Dalamud/Game/Text/Evaluator/Internal/SheetRedirectResolver.cs @@ -0,0 +1,232 @@ +using Dalamud.Data; +using Dalamud.Utility; + +using Lumina.Extensions; + +using ItemKind = Dalamud.Game.Text.SeStringHandling.Payloads.ItemPayload.ItemKind; +using LSheets = Lumina.Excel.Sheets; + +namespace Dalamud.Game.Text.Evaluator.Internal; + +/// +/// A service to resolve sheet redirects in expressions. +/// +[ServiceManager.EarlyLoadedService] +internal class SheetRedirectResolver : IServiceType +{ + private static readonly (string SheetName, uint ColumnIndex, bool ReturnActionSheetFlag)[] ActStrSheets = + [ + (nameof(LSheets.Trait), 0, false), + (nameof(LSheets.Action), 0, true), + (nameof(LSheets.Item), 0, false), + (nameof(LSheets.EventItem), 0, false), + (nameof(LSheets.EventAction), 0, false), + (nameof(LSheets.GeneralAction), 0, false), + (nameof(LSheets.BuddyAction), 0, false), + (nameof(LSheets.MainCommand), 5, false), + (nameof(LSheets.Companion), 0, false), + (nameof(LSheets.CraftAction), 0, false), + (nameof(LSheets.Action), 0, true), + (nameof(LSheets.PetAction), 0, false), + (nameof(LSheets.CompanyAction), 0, false), + (nameof(LSheets.Mount), 0, false), + (string.Empty, 0, false), + (string.Empty, 0, false), + (string.Empty, 0, false), + (string.Empty, 0, false), + (string.Empty, 0, false), + (nameof(LSheets.BgcArmyAction), 1, false), + (nameof(LSheets.Ornament), 8, false), + ]; + + private static readonly string[] ObjStrSheetNames = + [ + nameof(LSheets.BNpcName), + nameof(LSheets.ENpcResident), + nameof(LSheets.Treasure), + nameof(LSheets.Aetheryte), + nameof(LSheets.GatheringPointName), + nameof(LSheets.EObjName), + nameof(LSheets.Mount), + nameof(LSheets.Companion), + string.Empty, + string.Empty, + nameof(LSheets.Item), + ]; + + [ServiceManager.ServiceDependency] + private readonly DataManager dataManager = Service.Get(); + + [ServiceManager.ServiceConstructor] + private SheetRedirectResolver() + { + } + + /// + /// Resolves the sheet redirect, if any is present. + /// + /// The sheet name. + /// The row id. + /// The column index. Use ushort.MaxValue as default. + /// Flags giving additional information about the redirect. + internal SheetRedirectFlags Resolve(ref string sheetName, ref uint rowId, ref uint colIndex) + { + var flags = SheetRedirectFlags.None; + + switch (sheetName) + { + case nameof(LSheets.Item) or "ItemHQ" or "ItemMP": + { + flags |= SheetRedirectFlags.Item; + + var (itemId, kind) = ItemUtil.GetBaseId(rowId); + + if (kind == ItemKind.Hq || sheetName == "ItemHQ") + { + flags |= SheetRedirectFlags.HighQuality; + } + else if (kind == ItemKind.Collectible || sheetName == "ItemMP") // MP for Masterpiece?! + { + flags |= SheetRedirectFlags.Collectible; + } + + if (kind == ItemKind.EventItem && + rowId - 2_000_000 <= this.dataManager.GetExcelSheet().Count) + { + flags |= SheetRedirectFlags.EventItem; + sheetName = nameof(LSheets.EventItem); + } + else + { + sheetName = nameof(LSheets.Item); + rowId = itemId; + } + + if (colIndex is >= 4 and <= 7) + return SheetRedirectFlags.None; + + break; + } + + case "ActStr": + { + var returnActionSheetFlag = false; + (var index, rowId) = uint.DivRem(rowId, 1000000); + if (index < ActStrSheets.Length) + (sheetName, colIndex, returnActionSheetFlag) = ActStrSheets[index]; + + if (sheetName != nameof(LSheets.Companion) && colIndex != 13) + flags |= SheetRedirectFlags.Action; + + if (returnActionSheetFlag) + flags |= SheetRedirectFlags.ActionSheet; + + break; + } + + case "ObjStr": + { + (var index, rowId) = uint.DivRem(rowId, 1000000); + if (index < ObjStrSheetNames.Length) + sheetName = ObjStrSheetNames[index]; + + colIndex = 0; + + switch (index) + { + case 0: // BNpcName + if (rowId >= 100000) + rowId += 900000; + break; + + case 1: // ENpcResident + rowId += 1000000; + break; + + case 2: // Treasure + if (this.dataManager.GetExcelSheet().TryGetRow(rowId, out var treasureRow) && + treasureRow.Unknown0.IsEmpty) + rowId = 0; // defaulting to "Treasure Coffer" + break; + + case 3: // Aetheryte + rowId = this.dataManager.GetExcelSheet() + .TryGetRow(rowId, out var aetheryteRow) && aetheryteRow.IsAetheryte + ? 0u // "Aetheryte" + : 1; // "Aethernet Shard" + break; + + case 5: // EObjName + rowId += 2000000; + break; + } + + break; + } + + case nameof(LSheets.EObj) when colIndex is <= 7 or ushort.MaxValue: + sheetName = nameof(LSheets.EObjName); + break; + + case nameof(LSheets.Treasure) + when this.dataManager.GetExcelSheet().TryGetRow(rowId, out var treasureRow) && + treasureRow.Unknown0.IsEmpty: + rowId = 0; // defaulting to "Treasure Coffer" + break; + + case "WeatherPlaceName": + { + sheetName = nameof(LSheets.PlaceName); + + var placeNameSubId = rowId; + if (this.dataManager.GetExcelSheet().TryGetFirst( + r => r.PlaceNameSub.RowId == placeNameSubId, + out var row)) + rowId = row.PlaceNameParent.RowId; + break; + } + + case nameof(LSheets.InstanceContent) when colIndex == 3: + { + sheetName = nameof(LSheets.ContentFinderCondition); + colIndex = 43; + + if (this.dataManager.GetExcelSheet().TryGetRow(rowId, out var row)) + rowId = row.Order; + break; + } + + case nameof(LSheets.PartyContent) when colIndex == 2: + { + sheetName = nameof(LSheets.ContentFinderCondition); + colIndex = 43; + + if (this.dataManager.GetExcelSheet().TryGetRow(rowId, out var row)) + rowId = row.ContentFinderCondition.RowId; + break; + } + + case nameof(LSheets.PublicContent) when colIndex == 3: + { + sheetName = nameof(LSheets.ContentFinderCondition); + colIndex = 43; + + if (this.dataManager.GetExcelSheet().TryGetRow(rowId, out var row)) + rowId = row.ContentFinderCondition.RowId; + break; + } + + case nameof(LSheets.AkatsukiNote): + { + sheetName = nameof(LSheets.AkatsukiNoteString); + colIndex = 0; + + if (this.dataManager.Excel.GetSubrowSheet().TryGetRow(rowId, out var row)) + rowId = (uint)row[0].Unknown2; + break; + } + } + + return flags; + } +} diff --git a/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs b/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs new file mode 100644 index 000000000..723dbcb41 --- /dev/null +++ b/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs @@ -0,0 +1,1995 @@ +using System.Collections.Concurrent; +using System.Globalization; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; + +using Dalamud.Configuration.Internal; +using Dalamud.Data; +using Dalamud.Game.ClientState.Objects.Enums; +using Dalamud.Game.Config; +using Dalamud.Game.Text.Evaluator.Internal; +using Dalamud.Game.Text.Noun; +using Dalamud.Game.Text.Noun.Enums; +using Dalamud.Logging.Internal; +using Dalamud.Plugin.Services; +using Dalamud.Utility; + +using FFXIVClientStructs.FFXIV.Client.Game; +using FFXIVClientStructs.FFXIV.Client.Game.UI; +using FFXIVClientStructs.FFXIV.Client.UI; +using FFXIVClientStructs.FFXIV.Client.UI.Agent; +using FFXIVClientStructs.FFXIV.Client.UI.Info; +using FFXIVClientStructs.FFXIV.Client.UI.Misc; +using FFXIVClientStructs.FFXIV.Component.Text; + +using Lumina.Data.Structs.Excel; +using Lumina.Excel; +using Lumina.Excel.Sheets; +using Lumina.Extensions; +using Lumina.Text; +using Lumina.Text.Expressions; +using Lumina.Text.Payloads; +using Lumina.Text.ReadOnly; + +using AddonSheet = Lumina.Excel.Sheets.Addon; + +namespace Dalamud.Game.Text.Evaluator; + +#pragma warning disable SeStringEvaluator + +/// +/// Evaluator for SeStrings. +/// +[ServiceManager.EarlyLoadedService] +internal class SeStringEvaluator : IServiceType, ISeStringEvaluator +{ + private static readonly ModuleLog Log = new("SeStringEvaluator"); + + [ServiceManager.ServiceDependency] + private readonly DataManager dataManager = Service.Get(); + + [ServiceManager.ServiceDependency] + private readonly GameConfig gameConfig = Service.Get(); + + [ServiceManager.ServiceDependency] + private readonly DalamudConfiguration dalamudConfiguration = Service.Get(); + + [ServiceManager.ServiceDependency] + private readonly NounProcessor nounProcessor = Service.Get(); + + [ServiceManager.ServiceDependency] + private readonly SheetRedirectResolver sheetRedirectResolver = Service.Get(); + + private readonly ConcurrentDictionary, string> actStrCache = []; + private readonly ConcurrentDictionary, string> objStrCache = []; + + [ServiceManager.ServiceConstructor] + private SeStringEvaluator() + { + } + + /// + public ReadOnlySeString Evaluate( + ReadOnlySeString str, + Span localParameters = default, + ClientLanguage? language = null) + { + return this.Evaluate(str.AsSpan(), localParameters, language); + } + + /// + public ReadOnlySeString Evaluate( + ReadOnlySeStringSpan str, + Span localParameters = default, + ClientLanguage? language = null) + { + if (str.IsTextOnly()) + return new(str); + + var lang = language ?? this.dalamudConfiguration.EffectiveLanguage.ToClientLanguage(); + + // TODO: remove culture info toggling after supporting CultureInfo for SeStringBuilder.Append, + // and then remove try...finally block (discard builder from the pool on exception) + var previousCulture = CultureInfo.CurrentCulture; + var builder = SeStringBuilder.SharedPool.Get(); + try + { + CultureInfo.CurrentCulture = Localization.GetCultureInfoFromLangCode(lang.ToCode()); + return this.EvaluateAndAppendTo(builder, str, localParameters, lang).ToReadOnlySeString(); + } + finally + { + CultureInfo.CurrentCulture = previousCulture; + SeStringBuilder.SharedPool.Return(builder); + } + } + + /// + public ReadOnlySeString EvaluateFromAddon( + uint addonId, + Span localParameters = default, + ClientLanguage? language = null) + { + var lang = language ?? this.dalamudConfiguration.EffectiveLanguage.ToClientLanguage(); + + if (!this.dataManager.GetExcelSheet(lang).TryGetRow(addonId, out var addonRow)) + return default; + + return this.Evaluate(addonRow.Text.AsSpan(), localParameters, lang); + } + + /// + public ReadOnlySeString EvaluateFromLobby( + uint lobbyId, + Span localParameters = default, + ClientLanguage? language = null) + { + var lang = language ?? this.dalamudConfiguration.EffectiveLanguage.ToClientLanguage(); + + if (!this.dataManager.GetExcelSheet(lang).TryGetRow(lobbyId, out var lobbyRow)) + return default; + + return this.Evaluate(lobbyRow.Text.AsSpan(), localParameters, lang); + } + + /// + public ReadOnlySeString EvaluateFromLogMessage( + uint logMessageId, + Span localParameters = default, + ClientLanguage? language = null) + { + var lang = language ?? this.dalamudConfiguration.EffectiveLanguage.ToClientLanguage(); + + if (!this.dataManager.GetExcelSheet(lang).TryGetRow(logMessageId, out var logMessageRow)) + return default; + + return this.Evaluate(logMessageRow.Text.AsSpan(), localParameters, lang); + } + + /// + public string EvaluateActStr(ActionKind actionKind, uint id, ClientLanguage? language = null) => + this.actStrCache.GetOrAdd( + new(actionKind, id, language ?? this.dalamudConfiguration.EffectiveLanguage.ToClientLanguage()), + static (key, t) => t.EvaluateFromAddon(2026, [key.Kind.GetActStrId(key.Id)], key.Language) + .ExtractText() + .StripSoftHyphen(), + this); + + /// + public string EvaluateObjStr(ObjectKind objectKind, uint id, ClientLanguage? language = null) => + this.objStrCache.GetOrAdd( + new(objectKind, id, language ?? this.dalamudConfiguration.EffectiveLanguage.ToClientLanguage()), + static (key, t) => t.EvaluateFromAddon(2025, [key.Kind.GetObjStrId(key.Id)], key.Language) + .ExtractText() + .StripSoftHyphen(), + this); + + // TODO: move this to MapUtil? + private static uint ConvertRawToMapPos(Lumina.Excel.Sheets.Map map, short offset, float value) + { + var scale = map.SizeFactor / 100.0f; + return (uint)(10 - (int)(((((value + offset) * scale) + 1024f) * -0.2f) / scale)); + } + + private static uint ConvertRawToMapPosX(Lumina.Excel.Sheets.Map map, float x) + => ConvertRawToMapPos(map, map.OffsetX, x); + + private static uint ConvertRawToMapPosY(Lumina.Excel.Sheets.Map map, float y) + => ConvertRawToMapPos(map, map.OffsetY, y); + + private SeStringBuilder EvaluateAndAppendTo( + SeStringBuilder builder, + ReadOnlySeStringSpan str, + Span localParameters, + ClientLanguage language) + { + var context = new SeStringContext(builder, localParameters, language); + + foreach (var payload in str) + { + if (!this.ResolvePayload(in context, payload)) + { + context.Builder.Append(payload); + } + } + + return builder; + } + + private bool ResolvePayload(in SeStringContext context, ReadOnlySePayloadSpan payload) + { + if (payload.Type != ReadOnlySePayloadType.Macro) + return false; + + // if (context.HandlePayload(payload, in context)) + // return true; + + switch (payload.MacroCode) + { + case MacroCode.SetResetTime: + return this.TryResolveSetResetTime(in context, payload); + + case MacroCode.SetTime: + return this.TryResolveSetTime(in context, payload); + + case MacroCode.If: + return this.TryResolveIf(in context, payload); + + case MacroCode.Switch: + return this.TryResolveSwitch(in context, payload); + + case MacroCode.PcName: + return this.TryResolvePcName(in context, payload); + + case MacroCode.IfPcGender: + return this.TryResolveIfPcGender(in context, payload); + + case MacroCode.IfPcName: + return this.TryResolveIfPcName(in context, payload); + + // case MacroCode.Josa: + // case MacroCode.Josaro: + + case MacroCode.IfSelf: + return this.TryResolveIfSelf(in context, payload); + + // case MacroCode.NewLine: // pass through + // case MacroCode.Wait: // pass through + // case MacroCode.Icon: // pass through + + case MacroCode.Color: + return this.TryResolveColor(in context, payload); + + case MacroCode.EdgeColor: + return this.TryResolveEdgeColor(in context, payload); + + case MacroCode.ShadowColor: + return this.TryResolveShadowColor(in context, payload); + + // case MacroCode.SoftHyphen: // pass through + // case MacroCode.Key: + // case MacroCode.Scale: + + case MacroCode.Bold: + return this.TryResolveBold(in context, payload); + + case MacroCode.Italic: + return this.TryResolveItalic(in context, payload); + + // case MacroCode.Edge: + // case MacroCode.Shadow: + // case MacroCode.NonBreakingSpace: // pass through + // case MacroCode.Icon2: // pass through + // case MacroCode.Hyphen: // pass through + + case MacroCode.Num: + return this.TryResolveNum(in context, payload); + + case MacroCode.Hex: + return this.TryResolveHex(in context, payload); + + case MacroCode.Kilo: + return this.TryResolveKilo(in context, payload); + + // case MacroCode.Byte: + + case MacroCode.Sec: + return this.TryResolveSec(in context, payload); + + // case MacroCode.Time: + + case MacroCode.Float: + return this.TryResolveFloat(in context, payload); + + // case MacroCode.Link: // pass through + + case MacroCode.Sheet: + return this.TryResolveSheet(in context, payload); + + case MacroCode.String: + return this.TryResolveString(in context, payload); + + case MacroCode.Caps: + return this.TryResolveCaps(in context, payload); + + case MacroCode.Head: + return this.TryResolveHead(in context, payload); + + case MacroCode.Split: + return this.TryResolveSplit(in context, payload); + + case MacroCode.HeadAll: + return this.TryResolveHeadAll(in context, payload); + + case MacroCode.Fixed: + return this.TryResolveFixed(in context, payload); + + case MacroCode.Lower: + return this.TryResolveLower(in context, payload); + + case MacroCode.JaNoun: + return this.TryResolveNoun(ClientLanguage.Japanese, in context, payload); + + case MacroCode.EnNoun: + return this.TryResolveNoun(ClientLanguage.English, in context, payload); + + case MacroCode.DeNoun: + return this.TryResolveNoun(ClientLanguage.German, in context, payload); + + case MacroCode.FrNoun: + return this.TryResolveNoun(ClientLanguage.French, in context, payload); + + // case MacroCode.ChNoun: + + case MacroCode.LowerHead: + return this.TryResolveLowerHead(in context, payload); + + case MacroCode.ColorType: + return this.TryResolveColorType(in context, payload); + + case MacroCode.EdgeColorType: + return this.TryResolveEdgeColorType(in context, payload); + + // case MacroCode.Ruby: + + case MacroCode.Digit: + return this.TryResolveDigit(in context, payload); + + case MacroCode.Ordinal: + return this.TryResolveOrdinal(in context, payload); + + // case MacroCode.Sound: // pass through + + case MacroCode.LevelPos: + return this.TryResolveLevelPos(in context, payload); + + default: + return false; + } + } + + private unsafe bool TryResolveSetResetTime(in SeStringContext context, in ReadOnlySePayloadSpan payload) + { + DateTime date; + + if (payload.TryGetExpression(out var eHour, out var eWeekday) + && this.TryResolveInt(in context, eHour, out var eHourVal) + && this.TryResolveInt(in context, eWeekday, out var eWeekdayVal)) + { + var t = DateTime.UtcNow.AddDays(((eWeekdayVal - (int)DateTime.UtcNow.DayOfWeek) + 7) % 7); + date = new DateTime(t.Year, t.Month, t.Day, eHourVal, 0, 0, DateTimeKind.Utc).ToLocalTime(); + } + else if (payload.TryGetExpression(out eHour) + && this.TryResolveInt(in context, eHour, out eHourVal)) + { + var t = DateTime.UtcNow; + date = new DateTime(t.Year, t.Month, t.Day, eHourVal, 0, 0, DateTimeKind.Utc).ToLocalTime(); + } + else + { + return false; + } + + MacroDecoder.GetMacroTime()->SetTime(date); + + return true; + } + + private unsafe bool TryResolveSetTime(in SeStringContext context, in ReadOnlySePayloadSpan payload) + { + if (!payload.TryGetExpression(out var eTime) || !this.TryResolveUInt(in context, eTime, out var eTimeVal)) + return false; + + var date = DateTimeOffset.FromUnixTimeSeconds(eTimeVal).LocalDateTime; + MacroDecoder.GetMacroTime()->SetTime(date); + + return true; + } + + private bool TryResolveIf(in SeStringContext context, in ReadOnlySePayloadSpan payload) + { + return + payload.TryGetExpression(out var eCond, out var eTrue, out var eFalse) + && this.ResolveStringExpression( + context, + this.TryResolveBool(in context, eCond, out var eCondVal) && eCondVal + ? eTrue + : eFalse); + } + + private bool TryResolveSwitch(in SeStringContext context, in ReadOnlySePayloadSpan payload) + { + var cond = -1; + foreach (var e in payload) + { + switch (cond) + { + case -1: + cond = this.TryResolveUInt(in context, e, out var eVal) ? (int)eVal : 0; + break; + case > 1: + cond--; + break; + default: + return this.ResolveStringExpression(in context, e); + } + } + + return false; + } + + private unsafe bool TryResolvePcName(in SeStringContext context, in ReadOnlySePayloadSpan payload) + { + if (!payload.TryGetExpression(out var eEntityId)) + return false; + + if (!this.TryResolveUInt(in context, eEntityId, out var entityId)) + return false; + + // TODO: handle LogNameType + + NameCache.CharacterInfo characterInfo = default; + if (NameCache.Instance()->TryGetCharacterInfoByEntityId(entityId, &characterInfo)) + { + context.Builder.Append((ReadOnlySeStringSpan)characterInfo.Name.AsSpan()); + + if (characterInfo.HomeWorldId != AgentLobby.Instance()->LobbyData.HomeWorldId && + WorldHelper.Instance()->AllWorlds.TryGetValue((ushort)characterInfo.HomeWorldId, out var world, false)) + { + context.Builder.AppendIcon(88); + + if (this.gameConfig.UiConfig.TryGetUInt("LogCrossWorldName", out var logCrossWorldName) && + logCrossWorldName == 1) + context.Builder.Append((ReadOnlySeStringSpan)world.Name); + } + + return true; + } + + // TODO: lookup via InstanceContentCrystallineConflictDirector + // TODO: lookup via MJIManager + + return false; + } + + private unsafe bool TryResolveIfPcGender(in SeStringContext context, in ReadOnlySePayloadSpan payload) + { + if (!payload.TryGetExpression(out var eEntityId, out var eMale, out var eFemale)) + return false; + + if (!this.TryResolveUInt(in context, eEntityId, out var entityId)) + return false; + + NameCache.CharacterInfo characterInfo = default; + if (NameCache.Instance()->TryGetCharacterInfoByEntityId(entityId, &characterInfo)) + return this.ResolveStringExpression(in context, characterInfo.Sex == 0 ? eMale : eFemale); + + // TODO: lookup via InstanceContentCrystallineConflictDirector + + return false; + } + + private unsafe bool TryResolveIfPcName(in SeStringContext context, in ReadOnlySePayloadSpan payload) + { + if (!payload.TryGetExpression(out var eEntityId, out var eName, out var eTrue, out var eFalse)) + return false; + + if (!this.TryResolveUInt(in context, eEntityId, out var entityId) || !eName.TryGetString(out var name)) + return false; + + name = this.Evaluate(name, context.LocalParameters, context.Language).AsSpan(); + + NameCache.CharacterInfo characterInfo = default; + return NameCache.Instance()->TryGetCharacterInfoByEntityId(entityId, &characterInfo) && + this.ResolveStringExpression( + context, + name.Equals(characterInfo.Name.AsSpan()) + ? eTrue + : eFalse); + } + + private unsafe bool TryResolveIfSelf(in SeStringContext context, in ReadOnlySePayloadSpan payload) + { + if (!payload.TryGetExpression(out var eEntityId, out var eTrue, out var eFalse)) + return false; + + if (!this.TryResolveUInt(in context, eEntityId, out var entityId)) + return false; + + // the game uses LocalPlayer here, but using PlayerState seems more safe. + return this.ResolveStringExpression(in context, PlayerState.Instance()->EntityId == entityId ? eTrue : eFalse); + } + + private bool TryResolveColor(in SeStringContext context, in ReadOnlySePayloadSpan payload) + { + if (!payload.TryGetExpression(out var eColor)) + return false; + + if (eColor.TryGetPlaceholderExpression(out var ph) && ph == (int)ExpressionType.StackColor) + context.Builder.PopColor(); + else if (this.TryResolveUInt(in context, eColor, out var eColorVal)) + context.Builder.PushColorBgra(eColorVal); + + return true; + } + + private bool TryResolveEdgeColor(in SeStringContext context, in ReadOnlySePayloadSpan payload) + { + if (!payload.TryGetExpression(out var eColor)) + return false; + + if (eColor.TryGetPlaceholderExpression(out var ph) && ph == (int)ExpressionType.StackColor) + context.Builder.PopEdgeColor(); + else if (this.TryResolveUInt(in context, eColor, out var eColorVal)) + context.Builder.PushEdgeColorBgra(eColorVal); + + return true; + } + + private bool TryResolveShadowColor(in SeStringContext context, in ReadOnlySePayloadSpan payload) + { + if (!payload.TryGetExpression(out var eColor)) + return false; + + if (eColor.TryGetPlaceholderExpression(out var ph) && ph == (int)ExpressionType.StackColor) + context.Builder.PopShadowColor(); + else if (this.TryResolveUInt(in context, eColor, out var eColorVal)) + context.Builder.PushShadowColorBgra(eColorVal); + + return true; + } + + private bool TryResolveBold(in SeStringContext context, in ReadOnlySePayloadSpan payload) + { + if (!payload.TryGetExpression(out var eEnable) || + !this.TryResolveBool(in context, eEnable, out var eEnableVal)) + return false; + + context.Builder.AppendSetBold(eEnableVal); + + return true; + } + + private bool TryResolveItalic(in SeStringContext context, in ReadOnlySePayloadSpan payload) + { + if (!payload.TryGetExpression(out var eEnable) || + !this.TryResolveBool(in context, eEnable, out var eEnableVal)) + return false; + + context.Builder.AppendSetItalic(eEnableVal); + + return true; + } + + private bool TryResolveNum(in SeStringContext context, in ReadOnlySePayloadSpan payload) + { + if (!payload.TryGetExpression(out var eInt) || !this.TryResolveInt(in context, eInt, out var eIntVal)) + { + context.Builder.Append('0'); + return true; + } + + context.Builder.Append(eIntVal.ToString()); + + return true; + } + + private bool TryResolveHex(in SeStringContext context, in ReadOnlySePayloadSpan payload) + { + if (!payload.TryGetExpression(out var eUInt) || !this.TryResolveUInt(in context, eUInt, out var eUIntVal)) + { + // TODO: throw? + // ERROR: mismatch parameter type ('' is not numeric) + return false; + } + + context.Builder.Append("0x{0:X08}".Format(eUIntVal)); + + return true; + } + + private bool TryResolveKilo(in SeStringContext context, in ReadOnlySePayloadSpan payload) + { + if (!payload.TryGetExpression(out var eInt, out var eSep) || + !this.TryResolveInt(in context, eInt, out var eIntVal)) + { + context.Builder.Append('0'); + return true; + } + + if (eIntVal == int.MinValue) + { + // -2147483648 + context.Builder.Append("-2"u8); + this.ResolveStringExpression(in context, eSep); + context.Builder.Append("147"u8); + this.ResolveStringExpression(in context, eSep); + context.Builder.Append("483"u8); + this.ResolveStringExpression(in context, eSep); + context.Builder.Append("648"u8); + return true; + } + + if (eIntVal < 0) + { + context.Builder.Append('-'); + eIntVal = -eIntVal; + } + + if (eIntVal == 0) + { + context.Builder.Append('0'); + return true; + } + + var anyDigitPrinted = false; + for (var i = 1_000_000_000; i > 0; i /= 10) + { + var digit = (eIntVal / i) % 10; + switch (anyDigitPrinted) + { + case false when digit == 0: + continue; + case true when i % 3 == 0: + this.ResolveStringExpression(in context, eSep); + break; + } + + anyDigitPrinted = true; + context.Builder.Append((char)('0' + digit)); + } + + return true; + } + + private bool TryResolveSec(in SeStringContext context, in ReadOnlySePayloadSpan payload) + { + if (!payload.TryGetExpression(out var eInt) || !this.TryResolveUInt(in context, eInt, out var eIntVal)) + { + // TODO: throw? + // ERROR: mismatch parameter type ('' is not numeric) + return false; + } + + context.Builder.Append("{0:00}".Format(eIntVal)); + return true; + } + + private bool TryResolveFloat(in SeStringContext context, in ReadOnlySePayloadSpan payload) + { + if (!payload.TryGetExpression(out var eValue, out var eRadix, out var eSeparator) + || !this.TryResolveInt(in context, eValue, out var eValueVal) + || !this.TryResolveInt(in context, eRadix, out var eRadixVal)) + { + return false; + } + + var (integerPart, fractionalPart) = int.DivRem(eValueVal, eRadixVal); + if (fractionalPart < 0) + { + integerPart--; + fractionalPart += eRadixVal; + } + + context.Builder.Append(integerPart.ToString()); + this.ResolveStringExpression(in context, eSeparator); + + // brain fried code + Span fractionalDigits = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + var pos = fractionalDigits.Length - 1; + for (var r = eRadixVal; r > 1; r /= 10) + { + fractionalDigits[pos--] = (byte)('0' + (fractionalPart % 10)); + fractionalPart /= 10; + } + + context.Builder.Append(fractionalDigits[(pos + 1)..]); + + return true; + } + + private bool TryResolveSheet(in SeStringContext context, in ReadOnlySePayloadSpan payload) + { + var enu = payload.GetEnumerator(); + + if (!enu.MoveNext() || !enu.Current.TryGetString(out var eSheetNameStr)) + return false; + + if (!enu.MoveNext() || !this.TryResolveUInt(in context, enu.Current, out var eRowIdValue)) + return false; + + if (!enu.MoveNext() || !this.TryResolveUInt(in context, enu.Current, out var eColIndexValue)) + return false; + + var eColParamValue = 0u; + if (enu.MoveNext()) + this.TryResolveUInt(in context, enu.Current, out eColParamValue); + + var resolvedSheetName = this.Evaluate(eSheetNameStr, context.LocalParameters, context.Language).ExtractText(); + + this.sheetRedirectResolver.Resolve(ref resolvedSheetName, ref eRowIdValue, ref eColIndexValue); + + if (string.IsNullOrEmpty(resolvedSheetName)) + return false; + + if (!this.dataManager.Excel.SheetNames.Contains(resolvedSheetName)) + return false; + + if (!this.dataManager.GetExcelSheet(context.Language, resolvedSheetName) + .TryGetRow(eRowIdValue, out var row)) + return false; + + if (eColIndexValue >= row.Columns.Count) + return false; + + var column = row.Columns[(int)eColIndexValue]; + switch (column.Type) + { + case ExcelColumnDataType.String: + context.Builder.Append(this.Evaluate(row.ReadString(column.Offset), [eColParamValue], context.Language)); + return true; + case ExcelColumnDataType.Bool: + context.Builder.Append((row.ReadBool(column.Offset) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture)); + return true; + case ExcelColumnDataType.Int8: + context.Builder.Append(row.ReadInt8(column.Offset).ToString("D", CultureInfo.InvariantCulture)); + return true; + case ExcelColumnDataType.UInt8: + context.Builder.Append(row.ReadUInt8(column.Offset).ToString("D", CultureInfo.InvariantCulture)); + return true; + case ExcelColumnDataType.Int16: + context.Builder.Append(row.ReadInt16(column.Offset).ToString("D", CultureInfo.InvariantCulture)); + return true; + case ExcelColumnDataType.UInt16: + context.Builder.Append(row.ReadUInt16(column.Offset).ToString("D", CultureInfo.InvariantCulture)); + return true; + case ExcelColumnDataType.Int32: + context.Builder.Append(row.ReadInt32(column.Offset).ToString("D", CultureInfo.InvariantCulture)); + return true; + case ExcelColumnDataType.UInt32: + context.Builder.Append(row.ReadUInt32(column.Offset).ToString("D", CultureInfo.InvariantCulture)); + return true; + case ExcelColumnDataType.Float32: + context.Builder.Append(row.ReadFloat32(column.Offset).ToString("D", CultureInfo.InvariantCulture)); + return true; + case ExcelColumnDataType.Int64: + context.Builder.Append(row.ReadInt64(column.Offset).ToString("D", CultureInfo.InvariantCulture)); + return true; + case ExcelColumnDataType.UInt64: + context.Builder.Append(row.ReadUInt64(column.Offset).ToString("D", CultureInfo.InvariantCulture)); + return true; + case ExcelColumnDataType.PackedBool0: + context.Builder.Append((row.ReadPackedBool(column.Offset, 0) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture)); + return true; + case ExcelColumnDataType.PackedBool1: + context.Builder.Append((row.ReadPackedBool(column.Offset, 1) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture)); + return true; + case ExcelColumnDataType.PackedBool2: + context.Builder.Append((row.ReadPackedBool(column.Offset, 2) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture)); + return true; + case ExcelColumnDataType.PackedBool3: + context.Builder.Append((row.ReadPackedBool(column.Offset, 3) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture)); + return true; + case ExcelColumnDataType.PackedBool4: + context.Builder.Append((row.ReadPackedBool(column.Offset, 4) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture)); + return true; + case ExcelColumnDataType.PackedBool5: + context.Builder.Append((row.ReadPackedBool(column.Offset, 5) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture)); + return true; + case ExcelColumnDataType.PackedBool6: + context.Builder.Append((row.ReadPackedBool(column.Offset, 6) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture)); + return true; + case ExcelColumnDataType.PackedBool7: + context.Builder.Append((row.ReadPackedBool(column.Offset, 7) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture)); + return true; + default: + return false; + } + } + + private bool TryResolveString(in SeStringContext context, in ReadOnlySePayloadSpan payload) + { + return payload.TryGetExpression(out var eStr) && this.ResolveStringExpression(in context, eStr); + } + + private bool TryResolveCaps(in SeStringContext context, in ReadOnlySePayloadSpan payload) + { + if (!payload.TryGetExpression(out var eStr)) + return false; + + var builder = SeStringBuilder.SharedPool.Get(); + + try + { + var headContext = new SeStringContext(builder, context.LocalParameters, context.Language); + + if (!this.ResolveStringExpression(headContext, eStr)) + return false; + + var str = builder.ToReadOnlySeString(); + var pIdx = 0; + + foreach (var p in str) + { + pIdx++; + + if (p.Type == ReadOnlySePayloadType.Invalid) + continue; + + if (pIdx == 1 && p.Type == ReadOnlySePayloadType.Text) + { + context.Builder.Append(Encoding.UTF8.GetString(p.Body.ToArray()).ToUpper(context.CultureInfo)); + continue; + } + + context.Builder.Append(p); + } + + return true; + } + finally + { + SeStringBuilder.SharedPool.Return(builder); + } + } + + private bool TryResolveHead(in SeStringContext context, in ReadOnlySePayloadSpan payload) + { + if (!payload.TryGetExpression(out var eStr)) + return false; + + var builder = SeStringBuilder.SharedPool.Get(); + + try + { + var headContext = new SeStringContext(builder, context.LocalParameters, context.Language); + + if (!this.ResolveStringExpression(headContext, eStr)) + return false; + + var str = builder.ToReadOnlySeString(); + var pIdx = 0; + + foreach (var p in str) + { + pIdx++; + + if (p.Type == ReadOnlySePayloadType.Invalid) + continue; + + if (pIdx == 1 && p.Type == ReadOnlySePayloadType.Text) + { + context.Builder.Append(Encoding.UTF8.GetString(p.Body.Span).FirstCharToUpper(context.CultureInfo)); + continue; + } + + context.Builder.Append(p); + } + + return true; + } + finally + { + SeStringBuilder.SharedPool.Return(builder); + } + } + + private bool TryResolveSplit(in SeStringContext context, in ReadOnlySePayloadSpan payload) + { + if (!payload.TryGetExpression(out var eText, out var eSeparator, out var eIndex)) + return false; + + if (!eSeparator.TryGetString(out var eSeparatorVal) || !eIndex.TryGetUInt(out var eIndexVal) || eIndexVal <= 0) + return false; + + var builder = SeStringBuilder.SharedPool.Get(); + + try + { + var headContext = new SeStringContext(builder, context.LocalParameters, context.Language); + + if (!this.ResolveStringExpression(headContext, eText)) + return false; + + var separator = eSeparatorVal.ExtractText(); + if (separator.Length < 1) + return false; + + var splitted = builder.ToReadOnlySeString().ExtractText().Split(separator[0]); + if (eIndexVal <= splitted.Length) + { + context.Builder.Append(splitted[eIndexVal - 1]); + return true; + } + + return false; + } + finally + { + SeStringBuilder.SharedPool.Return(builder); + } + } + + private bool TryResolveHeadAll(in SeStringContext context, in ReadOnlySePayloadSpan payload) + { + if (!payload.TryGetExpression(out var eStr)) + return false; + + var builder = SeStringBuilder.SharedPool.Get(); + + try + { + var headContext = new SeStringContext(builder, context.LocalParameters, context.Language); + + if (!this.ResolveStringExpression(headContext, eStr)) + return false; + + var str = builder.ToReadOnlySeString(); + + foreach (var p in str) + { + if (p.Type == ReadOnlySePayloadType.Invalid) + continue; + + if (p.Type == ReadOnlySePayloadType.Text) + { + context.Builder.Append( + context.CultureInfo.TextInfo.ToTitleCase(Encoding.UTF8.GetString(p.Body.Span))); + + continue; + } + + context.Builder.Append(p); + } + + return true; + } + finally + { + SeStringBuilder.SharedPool.Return(builder); + } + } + + private bool TryResolveFixed(in SeStringContext context, in ReadOnlySePayloadSpan payload) + { + // This is handled by the second function in Client::UI::Misc::PronounModule_ProcessString + + var enu = payload.GetEnumerator(); + + if (!enu.MoveNext() || !this.TryResolveInt(in context, enu.Current, out var e0Val)) + return false; + + if (!enu.MoveNext() || !this.TryResolveInt(in context, enu.Current, out var e1Val)) + return false; + + return e0Val switch + { + 100 or 200 => e1Val switch + { + 1 => this.TryResolveFixedPlayerLink(in context, ref enu), + 2 => this.TryResolveFixedClassJobLevel(in context, ref enu), + 3 => this.TryResolveFixedMapLink(in context, ref enu), + 4 => this.TryResolveFixedItemLink(in context, ref enu), + 5 => this.TryResolveFixedChatSoundEffect(in context, ref enu), + 6 => this.TryResolveFixedObjStr(in context, ref enu), + 7 => this.TryResolveFixedString(in context, ref enu), + 8 => this.TryResolveFixedTimeRemaining(in context, ref enu), + // Reads a uint and saves it to PronounModule+0x3AC + // TODO: handle this? looks like it's for the mentor/beginner icon of the player link in novice network + // see "FF 50 50 8B B0" + 9 => true, + 10 => this.TryResolveFixedStatusLink(in context, ref enu), + 11 => this.TryResolveFixedPartyFinderLink(in context, ref enu), + 12 => this.TryResolveFixedQuestLink(in context, ref enu), + _ => false, + }, + _ => this.TryResolveFixedAutoTranslation(in context, payload, e0Val, e1Val), + }; + } + + private unsafe bool TryResolveFixedPlayerLink(in SeStringContext context, ref ReadOnlySePayloadSpan.Enumerator enu) + { + if (!enu.MoveNext() || !this.TryResolveUInt(in context, enu.Current, out var worldId)) + return false; + + if (!enu.MoveNext() || !enu.Current.TryGetString(out var playerName)) + return false; + + if (UIGlobals.IsValidPlayerCharacterName(playerName.ExtractText())) + { + var flags = 0u; + if (InfoModule.Instance()->IsInCrossWorldDuty()) + flags |= 0x10; + + context.Builder.PushLink(LinkMacroPayloadType.Character, flags, worldId, 0u, playerName); + context.Builder.Append(playerName); + context.Builder.PopLink(); + } + else + { + context.Builder.Append(playerName); + } + + if (worldId == AgentLobby.Instance()->LobbyData.HomeWorldId) + return true; + + if (!this.dataManager.GetExcelSheet(context.Language).TryGetRow(worldId, out var worldRow)) + return false; + + context.Builder.AppendIcon(88); + context.Builder.Append(worldRow.Name); + + return true; + } + + private bool TryResolveFixedClassJobLevel(in SeStringContext context, ref ReadOnlySePayloadSpan.Enumerator enu) + { + if (!enu.MoveNext() || !this.TryResolveInt(in context, enu.Current, out var classJobId) || classJobId <= 0) + return false; + + if (!enu.MoveNext() || !this.TryResolveInt(in context, enu.Current, out var level)) + return false; + + if (!this.dataManager.GetExcelSheet(context.Language) + .TryGetRow((uint)classJobId, out var classJobRow)) + return false; + + context.Builder.Append(classJobRow.Name); + + if (level != 0) + context.Builder.Append(context.CultureInfo, $"({level:D})"); + + return true; + } + + private bool TryResolveFixedMapLink(in SeStringContext context, ref ReadOnlySePayloadSpan.Enumerator enu) + { + if (!enu.MoveNext() || !this.TryResolveUInt(in context, enu.Current, out var territoryTypeId)) + return false; + + if (!enu.MoveNext() || !this.TryResolveUInt(in context, enu.Current, out var packedIds)) + return false; + + if (!enu.MoveNext() || !this.TryResolveInt(in context, enu.Current, out var rawX)) + return false; + + if (!enu.MoveNext() || !this.TryResolveInt(in context, enu.Current, out var rawY)) + return false; + + if (!enu.MoveNext() || !this.TryResolveInt(in context, enu.Current, out var rawZ)) + return false; + + if (!enu.MoveNext() || !this.TryResolveUInt(in context, enu.Current, out var placeNameIdInt)) + return false; + + var instance = packedIds >> 0x10; + var mapId = packedIds & 0xFF; + + if (this.dataManager.GetExcelSheet(context.Language) + .TryGetRow(territoryTypeId, out var territoryTypeRow)) + { + if (!this.dataManager.GetExcelSheet(context.Language) + .TryGetRow( + placeNameIdInt == 0 ? territoryTypeRow.PlaceName.RowId : placeNameIdInt, + out var placeNameRow)) + return false; + + if (!this.dataManager.GetExcelSheet().TryGetRow(mapId, out var mapRow)) + return false; + + var sb = SeStringBuilder.SharedPool.Get(); + + sb.Append(placeNameRow.Name); + if (instance is > 0 and <= 9) + sb.Append((char)((char)0xE0B0 + (char)instance)); + + var placeNameWithInstance = sb.ToReadOnlySeString(); + SeStringBuilder.SharedPool.Return(sb); + + var mapPosX = ConvertRawToMapPosX(mapRow, rawX / 1000f); + var mapPosY = ConvertRawToMapPosY(mapRow, rawY / 1000f); + + var linkText = rawZ == -30000 + ? this.EvaluateFromAddon( + 1635, + [placeNameWithInstance, mapPosX, mapPosY], + context.Language) + : this.EvaluateFromAddon( + 1636, + [placeNameWithInstance, mapPosX, mapPosY, rawZ / (rawZ >= 0 ? 10 : -10), rawZ], + context.Language); + + context.Builder.PushLinkMapPosition(territoryTypeId, mapId, rawX, rawY); + context.Builder.Append(this.EvaluateFromAddon(371, [linkText], context.Language)); + context.Builder.PopLink(); + + return true; + } + + var rowId = mapId switch + { + 0 => 875u, // "(No location set for map link)" + 1 => 874u, // "(Map link unavailable in this area)" + 2 => 13743u, // "(Unable to set map link)" + _ => 0u, + }; + if (rowId == 0u) + return false; + if (this.dataManager.GetExcelSheet(context.Language).TryGetRow(rowId, out var addonRow)) + context.Builder.Append(addonRow.Text); + return true; + } + + private bool TryResolveFixedItemLink(in SeStringContext context, ref ReadOnlySePayloadSpan.Enumerator enu) + { + if (!enu.MoveNext() || !this.TryResolveUInt(in context, enu.Current, out var itemId)) + return false; + + if (!enu.MoveNext() || !this.TryResolveUInt(in context, enu.Current, out var rarity)) + return false; + + if (!enu.MoveNext() || !this.TryResolveInt(in context, enu.Current, out var unk2)) + return false; + + if (!enu.MoveNext() || !this.TryResolveInt(in context, enu.Current, out var unk3)) + return false; + + if (!enu.MoveNext() || !enu.Current.TryGetString(out var itemName)) // TODO: unescape?? + return false; + + // rarity color start + context.Builder.Append(this.EvaluateFromAddon(6, [rarity], context.Language)); + + var v2 = (ushort)((unk2 & 0xFF) + (unk3 << 0x10)); // TODO: find out what this does + + context.Builder.PushLink(LinkMacroPayloadType.Item, itemId, rarity, v2); + + // arrow and item name + context.Builder.Append(this.EvaluateFromAddon(371, [itemName], context.Language)); + + context.Builder.PopLink(); + context.Builder.PopColor(); + + return true; + } + + private bool TryResolveFixedChatSoundEffect(in SeStringContext context, ref ReadOnlySePayloadSpan.Enumerator enu) + { + if (!enu.MoveNext() || !this.TryResolveUInt(in context, enu.Current, out var soundEffectId)) + return false; + + context.Builder.Append($""); + + // the game would play it here + + return true; + } + + private bool TryResolveFixedObjStr(in SeStringContext context, ref ReadOnlySePayloadSpan.Enumerator enu) + { + if (!enu.MoveNext() || !this.TryResolveUInt(in context, enu.Current, out var objStrId)) + return false; + + context.Builder.Append(this.EvaluateFromAddon(2025, [objStrId], context.Language)); + + return true; + } + + private bool TryResolveFixedString(in SeStringContext context, ref ReadOnlySePayloadSpan.Enumerator enu) + { + if (!enu.MoveNext() || !enu.Current.TryGetString(out var text)) + return false; + + // formats it through vsprintf using "%s"?? + context.Builder.Append(text.ExtractText()); + + return true; + } + + private bool TryResolveFixedTimeRemaining(in SeStringContext context, ref ReadOnlySePayloadSpan.Enumerator enu) + { + if (!enu.MoveNext() || !this.TryResolveUInt(in context, enu.Current, out var seconds)) + return false; + + if (seconds != 0) + { + context.Builder.Append(this.EvaluateFromAddon(33, [seconds / 60, seconds % 60], context.Language)); + } + else + { + if (this.dataManager.GetExcelSheet(context.Language).TryGetRow(48, out var addonRow)) + context.Builder.Append(addonRow.Text); + } + + return true; + } + + private bool TryResolveFixedStatusLink(in SeStringContext context, ref ReadOnlySePayloadSpan.Enumerator enu) + { + if (!enu.MoveNext() || !this.TryResolveUInt(in context, enu.Current, out var statusId)) + return false; + + if (!enu.MoveNext() || !this.TryResolveBool(in context, enu.Current, out var hasOverride)) + return false; + + if (!this.dataManager.GetExcelSheet(context.Language) + .TryGetRow(statusId, out var statusRow)) + return false; + + ReadOnlySeStringSpan statusName; + ReadOnlySeStringSpan statusDescription; + + if (hasOverride) + { + if (!enu.MoveNext() || !enu.Current.TryGetString(out statusName)) + return false; + + if (!enu.MoveNext() || !enu.Current.TryGetString(out statusDescription)) + return false; + } + else + { + statusName = statusRow.Name.AsSpan(); + statusDescription = statusRow.Description.AsSpan(); + } + + var sb = SeStringBuilder.SharedPool.Get(); + + switch (statusRow.StatusCategory) + { + case 1: + sb.Append(this.EvaluateFromAddon(376, default, context.Language)); + break; + + case 2: + sb.Append(this.EvaluateFromAddon(377, default, context.Language)); + break; + } + + sb.Append(statusName); + + var linkText = sb.ToReadOnlySeString(); + SeStringBuilder.SharedPool.Return(sb); + + context.Builder + .BeginMacro(MacroCode.Link) + .AppendUIntExpression((uint)LinkMacroPayloadType.Status) + .AppendUIntExpression(statusId) + .AppendUIntExpression(0) + .AppendUIntExpression(0) + .AppendStringExpression(statusName) + .AppendStringExpression(statusDescription) + .EndMacro(); + + context.Builder.Append(this.EvaluateFromAddon(371, [linkText], context.Language)); + + context.Builder.PopLink(); + + return true; + } + + private bool TryResolveFixedPartyFinderLink(in SeStringContext context, ref ReadOnlySePayloadSpan.Enumerator enu) + { + if (!enu.MoveNext() || !this.TryResolveUInt(in context, enu.Current, out var listingId)) + return false; + + if (!enu.MoveNext() || !this.TryResolveUInt(in context, enu.Current, out var unk1)) + return false; + + if (!enu.MoveNext() || !this.TryResolveUInt(in context, enu.Current, out var worldId)) + return false; + + if (!enu.MoveNext() || !this.TryResolveInt( + context, + enu.Current, + out var crossWorldFlag)) // 0 = cross world, 1 = not cross world + return false; + + if (!enu.MoveNext() || !enu.Current.TryGetString(out var playerName)) + return false; + + context.Builder + .BeginMacro(MacroCode.Link) + .AppendUIntExpression((uint)LinkMacroPayloadType.PartyFinder) + .AppendUIntExpression(listingId) + .AppendUIntExpression(unk1) + .AppendUIntExpression((uint)(crossWorldFlag << 0x10) + worldId) + .EndMacro(); + + context.Builder.Append( + this.EvaluateFromAddon( + 371, + [this.EvaluateFromAddon(2265, [playerName, crossWorldFlag], context.Language)], + context.Language)); + + context.Builder.PopLink(); + + return true; + } + + private bool TryResolveFixedQuestLink(in SeStringContext context, ref ReadOnlySePayloadSpan.Enumerator enu) + { + if (!enu.MoveNext() || !this.TryResolveUInt(in context, enu.Current, out var questId)) + return false; + + if (!enu.MoveNext() || !enu.MoveNext() || !enu.MoveNext()) // unused + return false; + + if (!enu.MoveNext() || !enu.Current.TryGetString(out var questName)) + return false; + + /* TODO: hide incomplete, repeatable special event quest names + if (!QuestManager.IsQuestComplete(questId) && !QuestManager.Instance()->IsQuestAccepted(questId)) + { + var questRecompleteManager = QuestRecompleteManager.Instance(); + if (questRecompleteManager == null || !questRecompleteManager->"E8 ?? ?? ?? ?? 0F B6 57 FF"(questId)) { + if (_excelService.TryGetRow(5497, context.Language, out var addonRow)) + questName = addonRow.Text.AsSpan(); + } + } + */ + + context.Builder + .BeginMacro(MacroCode.Link) + .AppendUIntExpression((uint)LinkMacroPayloadType.Quest) + .AppendUIntExpression(questId) + .AppendUIntExpression(0) + .AppendUIntExpression(0) + .EndMacro(); + + context.Builder.Append(this.EvaluateFromAddon(371, [questName], context.Language)); + + context.Builder.PopLink(); + + return true; + } + + private bool TryResolveFixedAutoTranslation( + in SeStringContext context, in ReadOnlySePayloadSpan payload, int e0Val, int e1Val) + { + // Auto-Translation / Completion + var group = (uint)(e0Val + 1); + var rowId = (uint)e1Val; + + using var icons = new SeStringBuilderIconWrap(context.Builder, 54, 55); + + if (!this.dataManager.GetExcelSheet(context.Language).TryGetFirst( + row => row.Group == group && !row.LookupTable.IsEmpty, + out var groupRow)) + return false; + + var lookupTable = ( + groupRow.LookupTable.IsTextOnly() + ? groupRow.LookupTable + : this.Evaluate( + groupRow.LookupTable.AsSpan(), + context.LocalParameters, + context.Language)).ExtractText(); + + // Completion sheet + if (lookupTable.Equals("@")) + { + if (this.dataManager.GetExcelSheet(context.Language).TryGetRow(rowId, out var completionRow)) + { + context.Builder.Append(completionRow.Text); + } + + return true; + } + + // CategoryDataCache + if (lookupTable.Equals("#")) + { + // couldn't find any, so we don't handle them :p + context.Builder.Append(payload); + return false; + } + + // All other sheets + var rangesStart = lookupTable.IndexOf('['); + // Sheet without ranges + if (rangesStart == -1) + { + if (this.dataManager.GetExcelSheet(context.Language, lookupTable).TryGetRow(rowId, out var row)) + { + context.Builder.Append(row.ReadStringColumn(0)); + return true; + } + } + + var sheetName = lookupTable[..rangesStart]; + var ranges = lookupTable[(rangesStart + 1)..^1]; + if (ranges.Length == 0) + return true; + + var isNoun = false; + var col = 0; + + if (ranges.StartsWith("noun")) + { + isNoun = true; + } + else if (ranges.StartsWith("col")) + { + var colRangeEnd = ranges.IndexOf(','); + if (colRangeEnd == -1) + colRangeEnd = ranges.Length; + + col = int.Parse(ranges[4..colRangeEnd]); + } + else if (ranges.StartsWith("tail")) + { + // couldn't find any, so we don't handle them :p + context.Builder.Append(payload); + return false; + } + + if (isNoun && context.Language == ClientLanguage.German && sheetName == "Companion") + { + context.Builder.Append(this.nounProcessor.ProcessNoun(new NounParams() + { + Language = ClientLanguage.German, + SheetName = sheetName, + RowId = rowId, + Quantity = 1, + ArticleType = (int)GermanArticleType.ZeroArticle, + })); + } + else if (this.dataManager.GetExcelSheet(context.Language, sheetName).TryGetRow(rowId, out var row)) + { + context.Builder.Append(row.ReadStringColumn(col)); + } + + return true; + } + + private bool TryResolveLower(in SeStringContext context, in ReadOnlySePayloadSpan payload) + { + if (!payload.TryGetExpression(out var eStr)) + return false; + + var builder = SeStringBuilder.SharedPool.Get(); + + try + { + var headContext = new SeStringContext(builder, context.LocalParameters, context.Language); + + if (!this.ResolveStringExpression(headContext, eStr)) + return false; + + var str = builder.ToReadOnlySeString(); + + foreach (var p in str) + { + if (p.Type == ReadOnlySePayloadType.Invalid) + continue; + + if (p.Type == ReadOnlySePayloadType.Text) + { + context.Builder.Append(Encoding.UTF8.GetString(p.Body.ToArray()).ToLower(context.CultureInfo)); + + continue; + } + + context.Builder.Append(p); + } + + return true; + } + finally + { + SeStringBuilder.SharedPool.Return(builder); + } + } + + private bool TryResolveNoun(ClientLanguage language, in SeStringContext context, in ReadOnlySePayloadSpan payload) + { + var eAmountVal = 1; + var eCaseVal = 1; + + var enu = payload.GetEnumerator(); + + if (!enu.MoveNext() || !enu.Current.TryGetString(out var eSheetNameStr)) + return false; + + var sheetName = this.Evaluate(eSheetNameStr, context.LocalParameters, context.Language).ExtractText(); + + if (!enu.MoveNext() || !this.TryResolveInt(in context, enu.Current, out var eArticleTypeVal)) + return false; + + if (!enu.MoveNext() || !this.TryResolveUInt(in context, enu.Current, out var eRowIdVal)) + return false; + + uint colIndex = ushort.MaxValue; + var flags = this.sheetRedirectResolver.Resolve(ref sheetName, ref eRowIdVal, ref colIndex); + + if (string.IsNullOrEmpty(sheetName)) + return false; + + // optional arguments + if (enu.MoveNext()) + { + if (!this.TryResolveInt(in context, enu.Current, out eAmountVal)) + return false; + + if (enu.MoveNext()) + { + if (!this.TryResolveInt(in context, enu.Current, out eCaseVal)) + return false; + + // For Chinese texts? + /* + if (enu.MoveNext()) + { + var eUnkInt5 = enu.Current; + if (!TryResolveInt(context,eUnkInt5, out eUnkInt5Val)) + return false; + } + */ + } + } + + context.Builder.Append( + this.nounProcessor.ProcessNoun(new NounParams() + { + Language = language, + SheetName = sheetName, + RowId = eRowIdVal, + Quantity = eAmountVal, + ArticleType = eArticleTypeVal, + GrammaticalCase = eCaseVal - 1, + IsActionSheet = flags.HasFlag(SheetRedirectFlags.Action), + })); + + return true; + } + + private bool TryResolveLowerHead(in SeStringContext context, in ReadOnlySePayloadSpan payload) + { + if (!payload.TryGetExpression(out var eStr)) + return false; + + var builder = SeStringBuilder.SharedPool.Get(); + + try + { + var headContext = new SeStringContext(builder, context.LocalParameters, context.Language); + + if (!this.ResolveStringExpression(headContext, eStr)) + return false; + + var str = builder.ToReadOnlySeString(); + var pIdx = 0; + + foreach (var p in str) + { + pIdx++; + + if (p.Type == ReadOnlySePayloadType.Invalid) + continue; + + if (pIdx == 1 && p.Type == ReadOnlySePayloadType.Text) + { + context.Builder.Append(Encoding.UTF8.GetString(p.Body.Span).FirstCharToLower(context.CultureInfo)); + continue; + } + + context.Builder.Append(p); + } + + return true; + } + finally + { + SeStringBuilder.SharedPool.Return(builder); + } + } + + private bool TryResolveColorType(in SeStringContext context, in ReadOnlySePayloadSpan payload) + { + if (!payload.TryGetExpression(out var eColorType) || + !this.TryResolveUInt(in context, eColorType, out var eColorTypeVal)) + return false; + + if (eColorTypeVal == 0) + context.Builder.PopColor(); + else if (this.dataManager.GetExcelSheet().TryGetRow(eColorTypeVal, out var row)) + context.Builder.PushColorBgra((row.UIForeground >> 8) | (row.UIForeground << 24)); + + return true; + } + + private bool TryResolveEdgeColorType(in SeStringContext context, in ReadOnlySePayloadSpan payload) + { + if (!payload.TryGetExpression(out var eColorType) || + !this.TryResolveUInt(in context, eColorType, out var eColorTypeVal)) + return false; + + if (eColorTypeVal == 0) + context.Builder.PopEdgeColor(); + else if (this.dataManager.GetExcelSheet().TryGetRow(eColorTypeVal, out var row)) + context.Builder.PushEdgeColorBgra((row.UIForeground >> 8) | (row.UIForeground << 24)); + + return true; + } + + private bool TryResolveDigit(in SeStringContext context, in ReadOnlySePayloadSpan payload) + { + if (!payload.TryGetExpression(out var eValue, out var eTargetLength)) + return false; + + if (!this.TryResolveInt(in context, eValue, out var eValueVal)) + return false; + + if (!this.TryResolveInt(in context, eTargetLength, out var eTargetLengthVal)) + return false; + + context.Builder.Append(eValueVal.ToString(new string('0', eTargetLengthVal))); + + return true; + } + + private bool TryResolveOrdinal(in SeStringContext context, in ReadOnlySePayloadSpan payload) + { + if (!payload.TryGetExpression(out var eValue) || !this.TryResolveUInt(in context, eValue, out var eValueVal)) + return false; + + // TODO: Culture support? + context.Builder.Append( + $"{eValueVal}{(eValueVal % 10) switch + { + _ when eValueVal is >= 10 and <= 19 => "th", + 1 => "st", + 2 => "nd", + 3 => "rd", + _ => "th", + }}"); + return true; + } + + private bool TryResolveLevelPos(in SeStringContext context, in ReadOnlySePayloadSpan payload) + { + if (!payload.TryGetExpression(out var eLevel) || !this.TryResolveUInt(in context, eLevel, out var eLevelVal)) + return false; + + if (!this.dataManager.GetExcelSheet(context.Language).TryGetRow(eLevelVal, out var level) || + !level.Map.IsValid) + return false; + + if (!this.dataManager.GetExcelSheet(context.Language).TryGetRow( + level.Map.Value.PlaceName.RowId, + out var placeName)) + return false; + + var mapPosX = ConvertRawToMapPosX(level.Map.Value, level.X); + var mapPosY = ConvertRawToMapPosY(level.Map.Value, level.Z); // Z is [sic] + + context.Builder.Append( + this.EvaluateFromAddon( + 1637, + [placeName.Name, mapPosX, mapPosY], + context.Language)); + + return true; + } + + private unsafe bool TryGetGNumDefault(uint parameterIndex, out uint value) + { + value = 0u; + + var rtm = RaptureTextModule.Instance(); + if (rtm is null) + return false; + + if (!ThreadSafety.IsMainThread) + { + Log.Error("Global parameters may only be used from the main thread."); + return false; + } + + ref var gp = ref rtm->TextModule.MacroDecoder.GlobalParameters; + if (parameterIndex >= gp.MySize) + return false; + + var p = rtm->TextModule.MacroDecoder.GlobalParameters[parameterIndex]; + switch (p.Type) + { + case TextParameterType.Integer: + value = (uint)p.IntValue; + return true; + + case TextParameterType.ReferencedUtf8String: + Log.Error("Requested a number; Utf8String global parameter at {parameterIndex}.", parameterIndex); + return false; + + case TextParameterType.String: + Log.Error("Requested a number; string global parameter at {parameterIndex}.", parameterIndex); + return false; + + case TextParameterType.Uninitialized: + Log.Error("Requested a number; uninitialized global parameter at {parameterIndex}.", parameterIndex); + return false; + + default: + return false; + } + } + + private unsafe bool TryProduceGStrDefault(SeStringBuilder builder, ClientLanguage language, uint parameterIndex) + { + var rtm = RaptureTextModule.Instance(); + if (rtm is null) + return false; + + ref var gp = ref rtm->TextModule.MacroDecoder.GlobalParameters; + if (parameterIndex >= gp.MySize) + return false; + + if (!ThreadSafety.IsMainThread) + { + Log.Error("Global parameters may only be used from the main thread."); + return false; + } + + var p = rtm->TextModule.MacroDecoder.GlobalParameters[parameterIndex]; + switch (p.Type) + { + case TextParameterType.Integer: + builder.Append($"{p.IntValue:D}"); + return true; + + case TextParameterType.ReferencedUtf8String: + this.EvaluateAndAppendTo( + builder, + p.ReferencedUtf8StringValue->Utf8String.AsSpan(), + null, + language); + return false; + + case TextParameterType.String: + this.EvaluateAndAppendTo(builder, new(p.StringValue), null, language); + return false; + + case TextParameterType.Uninitialized: + default: + return false; + } + } + + private unsafe bool TryResolveUInt( + in SeStringContext context, in ReadOnlySeExpressionSpan expression, out uint value) + { + if (expression.TryGetUInt(out value)) + return true; + + if (expression.TryGetPlaceholderExpression(out var exprType)) + { + // if (context.TryGetPlaceholderNum(exprType, out value)) + // return true; + + switch ((ExpressionType)exprType) + { + case ExpressionType.Millisecond: + value = (uint)DateTime.Now.Millisecond; + return true; + case ExpressionType.Second: + value = (uint)MacroDecoder.GetMacroTime()->tm_sec; + return true; + case ExpressionType.Minute: + value = (uint)MacroDecoder.GetMacroTime()->tm_min; + return true; + case ExpressionType.Hour: + value = (uint)MacroDecoder.GetMacroTime()->tm_hour; + return true; + case ExpressionType.Day: + value = (uint)MacroDecoder.GetMacroTime()->tm_mday; + return true; + case ExpressionType.Weekday: + value = (uint)MacroDecoder.GetMacroTime()->tm_wday; + return true; + case ExpressionType.Month: + value = (uint)MacroDecoder.GetMacroTime()->tm_mon + 1; + return true; + case ExpressionType.Year: + value = (uint)MacroDecoder.GetMacroTime()->tm_year + 1900; + return true; + default: + return false; + } + } + + if (expression.TryGetParameterExpression(out exprType, out var operand1)) + { + if (!this.TryResolveUInt(in context, operand1, out var paramIndex)) + return false; + if (paramIndex == 0) + return false; + paramIndex--; + return (ExpressionType)exprType switch + { + ExpressionType.LocalNumber => context.TryGetLNum((int)paramIndex, out value), // lnum + ExpressionType.GlobalNumber => this.TryGetGNumDefault(paramIndex, out value), // gnum + _ => false, // gstr, lstr + }; + } + + if (expression.TryGetBinaryExpression(out exprType, out operand1, out var operand2)) + { + switch ((ExpressionType)exprType) + { + case ExpressionType.GreaterThanOrEqualTo: + case ExpressionType.GreaterThan: + case ExpressionType.LessThanOrEqualTo: + case ExpressionType.LessThan: + if (!this.TryResolveInt(in context, operand1, out var value1) + || !this.TryResolveInt(in context, operand2, out var value2)) + { + return false; + } + + value = (ExpressionType)exprType switch + { + ExpressionType.GreaterThanOrEqualTo => value1 >= value2 ? 1u : 0u, + ExpressionType.GreaterThan => value1 > value2 ? 1u : 0u, + ExpressionType.LessThanOrEqualTo => value1 <= value2 ? 1u : 0u, + ExpressionType.LessThan => value1 < value2 ? 1u : 0u, + _ => 0u, + }; + return true; + + case ExpressionType.Equal: + case ExpressionType.NotEqual: + if (this.TryResolveInt(in context, operand1, out value1) && + this.TryResolveInt(in context, operand2, out value2)) + { + if ((ExpressionType)exprType == ExpressionType.Equal) + value = value1 == value2 ? 1u : 0u; + else + value = value1 == value2 ? 0u : 1u; + return true; + } + + if (operand1.TryGetString(out var strval1) && operand2.TryGetString(out var strval2)) + { + var resolvedStr1 = this.EvaluateAndAppendTo( + SeStringBuilder.SharedPool.Get(), + strval1, + context.LocalParameters, + context.Language); + var resolvedStr2 = this.EvaluateAndAppendTo( + SeStringBuilder.SharedPool.Get(), + strval2, + context.LocalParameters, + context.Language); + var equals = resolvedStr1.GetViewAsSpan().SequenceEqual(resolvedStr2.GetViewAsSpan()); + SeStringBuilder.SharedPool.Return(resolvedStr1); + SeStringBuilder.SharedPool.Return(resolvedStr2); + + if ((ExpressionType)exprType == ExpressionType.Equal) + value = equals ? 1u : 0u; + else + value = equals ? 0u : 1u; + return true; + } + + // compare int with string, string with int?? + + return true; + + default: + return false; + } + } + + if (expression.TryGetString(out var str)) + { + var evaluatedStr = this.Evaluate(str, context.LocalParameters, context.Language); + + foreach (var payload in evaluatedStr) + { + if (!payload.TryGetExpression(out var expr)) + return false; + + return this.TryResolveUInt(in context, expr, out value); + } + + return false; + } + + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool TryResolveInt(in SeStringContext context, in ReadOnlySeExpressionSpan expression, out int value) + { + if (this.TryResolveUInt(in context, expression, out var u32)) + { + value = (int)u32; + return true; + } + + value = 0; + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool TryResolveBool(in SeStringContext context, in ReadOnlySeExpressionSpan expression, out bool value) + { + if (this.TryResolveUInt(in context, expression, out var u32)) + { + value = u32 != 0; + return true; + } + + value = false; + return false; + } + + private bool ResolveStringExpression(in SeStringContext context, in ReadOnlySeExpressionSpan expression) + { + uint u32; + + if (expression.TryGetString(out var innerString)) + { + context.Builder.Append(this.Evaluate(innerString, context.LocalParameters, context.Language)); + return true; + } + + /* + if (expression.TryGetPlaceholderExpression(out var exprType)) + { + if (context.TryProducePlaceholder(context,exprType)) + return true; + } + */ + + if (expression.TryGetParameterExpression(out var exprType, out var operand1)) + { + if (!this.TryResolveUInt(in context, operand1, out var paramIndex)) + return false; + if (paramIndex == 0) + return false; + paramIndex--; + switch ((ExpressionType)exprType) + { + case ExpressionType.LocalNumber: // lnum + if (!context.TryGetLNum((int)paramIndex, out u32)) + return false; + + context.Builder.Append(unchecked((int)u32).ToString()); + return true; + + case ExpressionType.LocalString: // lstr + if (!context.TryGetLStr((int)paramIndex, out var str)) + return false; + + context.Builder.Append(str); + return true; + + case ExpressionType.GlobalNumber: // gnum + if (!this.TryGetGNumDefault(paramIndex, out u32)) + return false; + + context.Builder.Append(unchecked((int)u32).ToString()); + return true; + + case ExpressionType.GlobalString: // gstr + return this.TryProduceGStrDefault(context.Builder, context.Language, paramIndex); + + default: + return false; + } + } + + // Handles UInt and Binary expressions + if (!this.TryResolveUInt(in context, expression, out u32)) + return false; + + context.Builder.Append(((int)u32).ToString()); + return true; + } + + private readonly record struct StringCacheKey(TK Kind, uint Id, ClientLanguage Language) + where TK : struct, Enum; +} diff --git a/Dalamud/Game/Text/Evaluator/SeStringParameter.cs b/Dalamud/Game/Text/Evaluator/SeStringParameter.cs new file mode 100644 index 000000000..c1f238f56 --- /dev/null +++ b/Dalamud/Game/Text/Evaluator/SeStringParameter.cs @@ -0,0 +1,79 @@ +using System.Globalization; + +using Lumina.Text.ReadOnly; + +using DSeString = Dalamud.Game.Text.SeStringHandling.SeString; +using LSeString = Lumina.Text.SeString; + +namespace Dalamud.Game.Text.Evaluator; + +/// +/// A wrapper for a local parameter, holding either a number or a string. +/// +public readonly struct SeStringParameter +{ + private readonly uint num; + private readonly ReadOnlySeString str; + + /// + /// Initializes a new instance of the struct for a number parameter. + /// + /// The number value. + public SeStringParameter(uint value) + { + this.num = value; + } + + /// + /// Initializes a new instance of the struct for a string parameter. + /// + /// The string value. + public SeStringParameter(ReadOnlySeString value) + { + this.str = value; + this.IsString = true; + } + + /// + /// Initializes a new instance of the struct for a string parameter. + /// + /// The string value. + public SeStringParameter(string value) + { + this.str = new ReadOnlySeString(value); + this.IsString = true; + } + + /// + /// Gets a value indicating whether the backing type of this parameter is a string. + /// + public bool IsString { get; } + + /// + /// Gets a numeric value. + /// + public uint UIntValue => + !this.IsString + ? this.num + : uint.TryParse(this.str.ExtractText(), out var value) ? value : 0; + + /// + /// Gets a string value. + /// + public ReadOnlySeString StringValue => + this.IsString ? this.str : new(this.num.ToString("D", CultureInfo.InvariantCulture)); + + public static implicit operator SeStringParameter(int value) => new((uint)value); + + public static implicit operator SeStringParameter(uint value) => new(value); + + public static implicit operator SeStringParameter(ReadOnlySeString value) => new(value); + + public static implicit operator SeStringParameter(ReadOnlySeStringSpan value) => new(new ReadOnlySeString(value)); + + public static implicit operator SeStringParameter(LSeString value) => new(new ReadOnlySeString(value.RawData)); + + public static implicit operator SeStringParameter(DSeString value) => new(new ReadOnlySeString(value.Encode())); + + public static implicit operator SeStringParameter(string value) => new(value); +} diff --git a/Dalamud/Game/Text/Noun/Enums/EnglishArticleType.cs b/Dalamud/Game/Text/Noun/Enums/EnglishArticleType.cs new file mode 100644 index 000000000..9214bea0b --- /dev/null +++ b/Dalamud/Game/Text/Noun/Enums/EnglishArticleType.cs @@ -0,0 +1,17 @@ +namespace Dalamud.Game.Text.Noun.Enums; + +/// +/// Article types for . +/// +public enum EnglishArticleType +{ + /// + /// Indefinite article (a, an). + /// + Indefinite = 1, + + /// + /// Definite article (the). + /// + Definite = 2, +} diff --git a/Dalamud/Game/Text/Noun/Enums/FrenchArticleType.cs b/Dalamud/Game/Text/Noun/Enums/FrenchArticleType.cs new file mode 100644 index 000000000..3b6d6a63e --- /dev/null +++ b/Dalamud/Game/Text/Noun/Enums/FrenchArticleType.cs @@ -0,0 +1,32 @@ +namespace Dalamud.Game.Text.Noun.Enums; + +/// +/// Article types for . +/// +public enum FrenchArticleType +{ + /// + /// Indefinite article (une, des). + /// + Indefinite = 1, + + /// + /// Definite article (le, la, les). + /// + Definite = 2, + + /// + /// Possessive article (mon, mes). + /// + PossessiveFirstPerson = 3, + + /// + /// Possessive article (ton, tes). + /// + PossessiveSecondPerson = 4, + + /// + /// Possessive article (son, ses). + /// + PossessiveThirdPerson = 5, +} diff --git a/Dalamud/Game/Text/Noun/Enums/GermanArticleType.cs b/Dalamud/Game/Text/Noun/Enums/GermanArticleType.cs new file mode 100644 index 000000000..29124e172 --- /dev/null +++ b/Dalamud/Game/Text/Noun/Enums/GermanArticleType.cs @@ -0,0 +1,37 @@ +namespace Dalamud.Game.Text.Noun.Enums; + +/// +/// Article types for . +/// +public enum GermanArticleType +{ + /// + /// Unbestimmter Artikel (ein, eine, etc.). + /// + Indefinite = 1, + + /// + /// Bestimmter Artikel (der, die, das, etc.). + /// + Definite = 2, + + /// + /// Possessivartikel "dein" (dein, deine, etc.). + /// + Possessive = 3, + + /// + /// Negativartikel "kein" (kein, keine, etc.). + /// + Negative = 4, + + /// + /// Nullartikel. + /// + ZeroArticle = 5, + + /// + /// Demonstrativpronomen "dieser" (dieser, diese, etc.). + /// + Demonstrative = 6, +} diff --git a/Dalamud/Game/Text/Noun/Enums/JapaneseArticleType.cs b/Dalamud/Game/Text/Noun/Enums/JapaneseArticleType.cs new file mode 100644 index 000000000..14a29c4ff --- /dev/null +++ b/Dalamud/Game/Text/Noun/Enums/JapaneseArticleType.cs @@ -0,0 +1,17 @@ +namespace Dalamud.Game.Text.Noun.Enums; + +/// +/// Article types for . +/// +public enum JapaneseArticleType +{ + /// + /// Near listener (それら). + /// + NearListener = 1, + + /// + /// Distant from both speaker and listener (あれら). + /// + Distant = 2, +} diff --git a/Dalamud/Game/Text/Noun/NounParams.cs b/Dalamud/Game/Text/Noun/NounParams.cs new file mode 100644 index 000000000..3d5c424be --- /dev/null +++ b/Dalamud/Game/Text/Noun/NounParams.cs @@ -0,0 +1,73 @@ +using Dalamud.Game.Text.Noun.Enums; + +using Lumina.Text.ReadOnly; + +using LSheets = Lumina.Excel.Sheets; + +namespace Dalamud.Game.Text.Noun; + +/// +/// Parameters for noun processing. +/// +internal record struct NounParams() +{ + /// + /// The language of the sheet to be processed. + /// + public required ClientLanguage Language; + + /// + /// The name of the sheet containing the row to process. + /// + public required string SheetName = string.Empty; + + /// + /// The row id within the sheet to process. + /// + public required uint RowId; + + /// + /// The quantity of the entity (default is 1). Used to determine grammatical number (e.g., singular or plural). + /// + public int Quantity = 1; + + /// + /// The article type. + /// + /// + /// Depending on the , this has different meanings.
+ /// See , , , . + ///
+ public int ArticleType = 1; + + /// + /// The grammatical case (e.g., Nominative, Genitive, Dative, Accusative) used for German texts (default is 0). + /// + public int GrammaticalCase = 0; + + /// + /// An optional string that is placed in front of the text that should be linked, such as item names (default is an empty string; the game uses "//"). + /// + public ReadOnlySeString LinkMarker = default; + + /// + /// An indicator that this noun will be processed from an Action sheet. Only used for German texts. + /// + public bool IsActionSheet; + + /// + /// Gets the column offset. + /// + public readonly int ColumnOffset => this.SheetName switch + { + // See "E8 ?? ?? ?? ?? 44 8B 6B 08" + nameof(LSheets.BeastTribe) => 10, + nameof(LSheets.DeepDungeonItem) => 1, + nameof(LSheets.DeepDungeonEquipment) => 1, + nameof(LSheets.DeepDungeonMagicStone) => 1, + nameof(LSheets.DeepDungeonDemiclone) => 1, + nameof(LSheets.Glasses) => 4, + nameof(LSheets.GlassesStyle) => 15, + _ => 0, + }; +} diff --git a/Dalamud/Game/Text/Noun/NounProcessor.cs b/Dalamud/Game/Text/Noun/NounProcessor.cs new file mode 100644 index 000000000..18f8cd4a9 --- /dev/null +++ b/Dalamud/Game/Text/Noun/NounProcessor.cs @@ -0,0 +1,461 @@ +using System.Collections.Concurrent; + +using Dalamud.Configuration.Internal; +using Dalamud.Data; +using Dalamud.Game.Text.Noun.Enums; +using Dalamud.Logging.Internal; +using Dalamud.Utility; + +using Lumina.Excel; +using Lumina.Text.ReadOnly; + +using LSeStringBuilder = Lumina.Text.SeStringBuilder; +using LSheets = Lumina.Excel.Sheets; + +namespace Dalamud.Game.Text.Noun; + +/* +Attributive sheet: + Japanese: + Unknown0 = Singular Demonstrative + Unknown1 = Plural Demonstrative + English: + Unknown2 = Article before a singular noun beginning with a consonant sound + Unknown3 = Article before a generic noun beginning with a consonant sound + Unknown4 = N/A + Unknown5 = Article before a singular noun beginning with a vowel sound + Unknown6 = Article before a generic noun beginning with a vowel sound + Unknown7 = N/A + German: + Unknown8 = Nominative Masculine + Unknown9 = Nominative Feminine + Unknown10 = Nominative Neutral + Unknown11 = Nominative Plural + Unknown12 = Genitive Masculine + Unknown13 = Genitive Feminine + Unknown14 = Genitive Neutral + Unknown15 = Genitive Plural + Unknown16 = Dative Masculine + Unknown17 = Dative Feminine + Unknown18 = Dative Neutral + Unknown19 = Dative Plural + Unknown20 = Accusative Masculine + Unknown21 = Accusative Feminine + Unknown22 = Accusative Neutral + Unknown23 = Accusative Plural + French (unsure): + Unknown24 = Singular Article + Unknown25 = Singular Masculine Article + Unknown26 = Plural Masculine Article + Unknown27 = ? + Unknown28 = ? + Unknown29 = Singular Masculine/Feminine Article, before a noun beginning in a vowel or an h + Unknown30 = Plural Masculine/Feminine Article, before a noun beginning in a vowel or an h + Unknown31 = ? + Unknown32 = ? + Unknown33 = Singular Feminine Article + Unknown34 = Plural Feminine Article + Unknown35 = ? + Unknown36 = ? + Unknown37 = Singular Masculine/Feminine Article, before a noun beginning in a vowel or an h + Unknown38 = Plural Masculine/Feminine Article, before a noun beginning in a vowel or an h + Unknown39 = ? + Unknown40 = ? + +Placeholders: + [t] = article or grammatical gender (EN: the, DE: der, die, das) + [n] = amount (number) + [a] = declension + [p] = plural + [pa] = ? +*/ + +/// +/// Provides functionality to process texts from sheets containing grammatical placeholders. +/// +[ServiceManager.EarlyLoadedService] +internal class NounProcessor : IServiceType +{ + // column names from ExdSchema, most likely incorrect + private const int SingularColumnIdx = 0; + private const int AdjectiveColumnIdx = 1; + private const int PluralColumnIdx = 2; + private const int PossessivePronounColumnIdx = 3; + private const int StartsWithVowelColumnIdx = 4; + private const int Unknown5ColumnIdx = 5; // probably used in Chinese texts + private const int PronounColumnIdx = 6; + private const int ArticleColumnIdx = 7; + + private static readonly ModuleLog Log = new("NounProcessor"); + + [ServiceManager.ServiceDependency] + private readonly DataManager dataManager = Service.Get(); + + [ServiceManager.ServiceDependency] + private readonly DalamudConfiguration dalamudConfiguration = Service.Get(); + + private readonly ConcurrentDictionary cache = []; + + [ServiceManager.ServiceConstructor] + private NounProcessor() + { + } + + /// + /// Processes a specific row from a sheet and generates a formatted string based on grammatical and language-specific rules. + /// + /// Parameters for processing. + /// A ReadOnlySeString representing the processed text. + public ReadOnlySeString ProcessNoun(NounParams nounParams) + { + if (nounParams.GrammaticalCase < 0 || nounParams.GrammaticalCase > 5) + return default; + + if (this.cache.TryGetValue(nounParams, out var value)) + return value; + + var output = nounParams.Language switch + { + ClientLanguage.Japanese => this.ResolveNounJa(nounParams), + ClientLanguage.English => this.ResolveNounEn(nounParams), + ClientLanguage.German => this.ResolveNounDe(nounParams), + ClientLanguage.French => this.ResolveNounFr(nounParams), + _ => default, + }; + + this.cache.TryAdd(nounParams, output); + + return output; + } + + /// + /// Resolves noun placeholders in Japanese text. + /// + /// Parameters for processing. + /// A ReadOnlySeString representing the processed text. + /// + /// This is a C# implementation of Component::Text::Localize::NounJa.Resolve. + /// + private ReadOnlySeString ResolveNounJa(NounParams nounParams) + { + var sheet = this.dataManager.Excel.GetSheet(nounParams.Language.ToLumina(), nounParams.SheetName); + if (!sheet.TryGetRow(nounParams.RowId, out var row)) + { + Log.Warning("Sheet {SheetName} does not contain row #{RowId}", nounParams.SheetName, nounParams.RowId); + return default; + } + + var attributiveSheet = this.dataManager.Excel.GetSheet(nounParams.Language.ToLumina(), nameof(LSheets.Attributive)); + + var builder = LSeStringBuilder.SharedPool.Get(); + + // Ko-So-A-Do + var ksad = attributiveSheet.GetRow((uint)nounParams.ArticleType).ReadStringColumn(nounParams.Quantity > 1 ? 1 : 0); + if (!ksad.IsEmpty) + { + builder.Append(ksad); + + if (nounParams.Quantity > 1) + { + builder.ReplaceText("[n]"u8, ReadOnlySeString.FromText(nounParams.Quantity.ToString())); + } + } + + if (!nounParams.LinkMarker.IsEmpty) + builder.Append(nounParams.LinkMarker); + + var text = row.ReadStringColumn(nounParams.ColumnOffset); + if (!text.IsEmpty) + builder.Append(text); + + var ross = builder.ToReadOnlySeString(); + LSeStringBuilder.SharedPool.Return(builder); + return ross; + } + + /// + /// Resolves noun placeholders in English text. + /// + /// Parameters for processing. + /// A ReadOnlySeString representing the processed text. + /// + /// This is a C# implementation of Component::Text::Localize::NounEn.Resolve. + /// + private ReadOnlySeString ResolveNounEn(NounParams nounParams) + { + /* + a1->Offsets[0] = SingularColumnIdx + a1->Offsets[1] = PluralColumnIdx + a1->Offsets[2] = StartsWithVowelColumnIdx + a1->Offsets[3] = PossessivePronounColumnIdx + a1->Offsets[4] = ArticleColumnIdx + */ + + var sheet = this.dataManager.Excel.GetSheet(nounParams.Language.ToLumina(), nounParams.SheetName); + if (!sheet.TryGetRow(nounParams.RowId, out var row)) + { + Log.Warning("Sheet {SheetName} does not contain row #{RowId}", nounParams.SheetName, nounParams.RowId); + return default; + } + + var attributiveSheet = this.dataManager.Excel.GetSheet(nounParams.Language.ToLumina(), nameof(LSheets.Attributive)); + + var builder = LSeStringBuilder.SharedPool.Get(); + + var isProperNounColumn = nounParams.ColumnOffset + ArticleColumnIdx; + var isProperNoun = isProperNounColumn >= 0 ? row.ReadInt8Column(isProperNounColumn) : ~isProperNounColumn; + if (isProperNoun == 0) + { + var startsWithVowelColumn = nounParams.ColumnOffset + StartsWithVowelColumnIdx; + var startsWithVowel = startsWithVowelColumn >= 0 + ? row.ReadInt8Column(startsWithVowelColumn) + : ~startsWithVowelColumn; + + var articleColumn = startsWithVowel + (2 * (startsWithVowel + 1)); + var grammaticalNumberColumnOffset = nounParams.Quantity == 1 ? SingularColumnIdx : PluralColumnIdx; + var article = attributiveSheet.GetRow((uint)nounParams.ArticleType) + .ReadStringColumn(articleColumn + grammaticalNumberColumnOffset); + if (!article.IsEmpty) + builder.Append(article); + + if (!nounParams.LinkMarker.IsEmpty) + builder.Append(nounParams.LinkMarker); + } + + var text = row.ReadStringColumn(nounParams.ColumnOffset + (nounParams.Quantity == 1 ? SingularColumnIdx : PluralColumnIdx)); + if (!text.IsEmpty) + builder.Append(text); + + builder.ReplaceText("[n]"u8, ReadOnlySeString.FromText(nounParams.Quantity.ToString())); + + var ross = builder.ToReadOnlySeString(); + LSeStringBuilder.SharedPool.Return(builder); + return ross; + } + + /// + /// Resolves noun placeholders in German text. + /// + /// Parameters for processing. + /// A ReadOnlySeString representing the processed text. + /// + /// This is a C# implementation of Component::Text::Localize::NounDe.Resolve. + /// + private ReadOnlySeString ResolveNounDe(NounParams nounParams) + { + /* + a1->Offsets[0] = SingularColumnIdx + a1->Offsets[1] = PluralColumnIdx + a1->Offsets[2] = PronounColumnIdx + a1->Offsets[3] = AdjectiveColumnIdx + a1->Offsets[4] = PossessivePronounColumnIdx + a1->Offsets[5] = Unknown5ColumnIdx + a1->Offsets[6] = ArticleColumnIdx + */ + + var sheet = this.dataManager.Excel.GetSheet(nounParams.Language.ToLumina(), nounParams.SheetName); + if (!sheet.TryGetRow(nounParams.RowId, out var row)) + { + Log.Warning("Sheet {SheetName} does not contain row #{RowId}", nounParams.SheetName, nounParams.RowId); + return default; + } + + var attributiveSheet = this.dataManager.Excel.GetSheet(nounParams.Language.ToLumina(), nameof(LSheets.Attributive)); + + var builder = LSeStringBuilder.SharedPool.Get(); + ReadOnlySeString ross; + + if (nounParams.IsActionSheet) + { + builder.Append(row.ReadStringColumn(nounParams.GrammaticalCase)); + builder.ReplaceText("[n]"u8, ReadOnlySeString.FromText(nounParams.Quantity.ToString())); + + ross = builder.ToReadOnlySeString(); + LSeStringBuilder.SharedPool.Return(builder); + return ross; + } + + var genderIndexColumn = nounParams.ColumnOffset + PronounColumnIdx; + var genderIndex = genderIndexColumn >= 0 ? row.ReadInt8Column(genderIndexColumn) : ~genderIndexColumn; + + var articleIndexColumn = nounParams.ColumnOffset + ArticleColumnIdx; + var articleIndex = articleIndexColumn >= 0 ? row.ReadInt8Column(articleIndexColumn) : ~articleIndexColumn; + + var caseColumnOffset = (4 * nounParams.GrammaticalCase) + 8; + + var caseRowOffsetColumn = nounParams.ColumnOffset + (nounParams.Quantity == 1 ? AdjectiveColumnIdx : PossessivePronounColumnIdx); + var caseRowOffset = caseRowOffsetColumn >= 0 + ? row.ReadInt8Column(caseRowOffsetColumn) + : (sbyte)~caseRowOffsetColumn; + + if (nounParams.Quantity != 1) + genderIndex = 3; + + var hasT = false; + var text = row.ReadStringColumn(nounParams.ColumnOffset + (nounParams.Quantity == 1 ? SingularColumnIdx : PluralColumnIdx)); + if (!text.IsEmpty) + { + hasT = text.ContainsText("[t]"u8); + + if (articleIndex == 0 && !hasT) + { + var grammaticalGender = attributiveSheet.GetRow((uint)nounParams.ArticleType) + .ReadStringColumn(caseColumnOffset + genderIndex); // Genus + if (!grammaticalGender.IsEmpty) + builder.Append(grammaticalGender); + } + + if (!nounParams.LinkMarker.IsEmpty) + builder.Append(nounParams.LinkMarker); + + builder.Append(text); + + var plural = attributiveSheet.GetRow((uint)(caseRowOffset + 26)) + .ReadStringColumn(caseColumnOffset + genderIndex); + if (builder.ContainsText("[p]"u8)) + builder.ReplaceText("[p]"u8, plural); + else + builder.Append(plural); + + if (hasT) + { + var article = + attributiveSheet.GetRow(39).ReadStringColumn(caseColumnOffset + genderIndex); // Definiter Artikel + builder.ReplaceText("[t]"u8, article); + } + } + + var pa = attributiveSheet.GetRow(24).ReadStringColumn(caseColumnOffset + genderIndex); + builder.ReplaceText("[pa]"u8, pa); + + RawRow declensionRow; + + declensionRow = (GermanArticleType)nounParams.ArticleType switch + { + // Schwache Flexion eines Adjektivs?! + GermanArticleType.Possessive or GermanArticleType.Demonstrative => attributiveSheet.GetRow(25), + _ when hasT => attributiveSheet.GetRow(25), + + // Starke Deklination + GermanArticleType.ZeroArticle => attributiveSheet.GetRow(38), + + // Gemischte Deklination + GermanArticleType.Definite => attributiveSheet.GetRow(37), + + // Starke Flexion eines Artikels?! + GermanArticleType.Indefinite or GermanArticleType.Negative => attributiveSheet.GetRow(26), + _ => attributiveSheet.GetRow(26), + }; + + var declension = declensionRow.ReadStringColumn(caseColumnOffset + genderIndex); + builder.ReplaceText("[a]"u8, declension); + + builder.ReplaceText("[n]"u8, ReadOnlySeString.FromText(nounParams.Quantity.ToString())); + + ross = builder.ToReadOnlySeString(); + LSeStringBuilder.SharedPool.Return(builder); + return ross; + } + + /// + /// Resolves noun placeholders in French text. + /// + /// Parameters for processing. + /// A ReadOnlySeString representing the processed text. + /// + /// This is a C# implementation of Component::Text::Localize::NounFr.Resolve. + /// + private ReadOnlySeString ResolveNounFr(NounParams nounParams) + { + /* + a1->Offsets[0] = SingularColumnIdx + a1->Offsets[1] = PluralColumnIdx + a1->Offsets[2] = StartsWithVowelColumnIdx + a1->Offsets[3] = PronounColumnIdx + a1->Offsets[4] = Unknown5ColumnIdx + a1->Offsets[5] = ArticleColumnIdx + */ + + var sheet = this.dataManager.Excel.GetSheet(nounParams.Language.ToLumina(), nounParams.SheetName); + if (!sheet.TryGetRow(nounParams.RowId, out var row)) + { + Log.Warning("Sheet {SheetName} does not contain row #{RowId}", nounParams.SheetName, nounParams.RowId); + return default; + } + + var attributiveSheet = this.dataManager.Excel.GetSheet(nounParams.Language.ToLumina(), nameof(LSheets.Attributive)); + + var builder = LSeStringBuilder.SharedPool.Get(); + ReadOnlySeString ross; + + var startsWithVowelColumn = nounParams.ColumnOffset + StartsWithVowelColumnIdx; + var startsWithVowel = startsWithVowelColumn >= 0 + ? row.ReadInt8Column(startsWithVowelColumn) + : ~startsWithVowelColumn; + + var pronounColumn = nounParams.ColumnOffset + PronounColumnIdx; + var pronoun = pronounColumn >= 0 ? row.ReadInt8Column(pronounColumn) : ~pronounColumn; + + var articleColumn = nounParams.ColumnOffset + ArticleColumnIdx; + var article = articleColumn >= 0 ? row.ReadInt8Column(articleColumn) : ~articleColumn; + + var v20 = 4 * (startsWithVowel + 6 + (2 * pronoun)); + + if (article != 0) + { + var v21 = attributiveSheet.GetRow((uint)nounParams.ArticleType).ReadStringColumn(v20); + if (!v21.IsEmpty) + builder.Append(v21); + + if (!nounParams.LinkMarker.IsEmpty) + builder.Append(nounParams.LinkMarker); + + var text = row.ReadStringColumn(nounParams.ColumnOffset + (nounParams.Quantity <= 1 ? SingularColumnIdx : PluralColumnIdx)); + if (!text.IsEmpty) + builder.Append(text); + + if (nounParams.Quantity <= 1) + builder.ReplaceText("[n]"u8, ReadOnlySeString.FromText(nounParams.Quantity.ToString())); + + ross = builder.ToReadOnlySeString(); + LSeStringBuilder.SharedPool.Return(builder); + return ross; + } + + var v17 = row.ReadInt8Column(nounParams.ColumnOffset + Unknown5ColumnIdx); + if (v17 != 0 && (nounParams.Quantity > 1 || v17 == 2)) + { + var v29 = attributiveSheet.GetRow((uint)nounParams.ArticleType).ReadStringColumn(v20 + 2); + if (!v29.IsEmpty) + { + builder.Append(v29); + + if (!nounParams.LinkMarker.IsEmpty) + builder.Append(nounParams.LinkMarker); + + var text = row.ReadStringColumn(nounParams.ColumnOffset + PluralColumnIdx); + if (!text.IsEmpty) + builder.Append(text); + } + } + else + { + var v27 = attributiveSheet.GetRow((uint)nounParams.ArticleType).ReadStringColumn(v20 + (v17 != 0 ? 1 : 3)); + if (!v27.IsEmpty) + builder.Append(v27); + + if (!nounParams.LinkMarker.IsEmpty) + builder.Append(nounParams.LinkMarker); + + var text = row.ReadStringColumn(nounParams.ColumnOffset + SingularColumnIdx); + if (!text.IsEmpty) + builder.Append(text); + } + + builder.ReplaceText("[n]"u8, ReadOnlySeString.FromText(nounParams.Quantity.ToString())); + + ross = builder.ToReadOnlySeString(); + LSeStringBuilder.SharedPool.Return(builder); + return ross; + } +} diff --git a/Dalamud/Game/Text/SeStringHandling/Payloads/ItemPayload.cs b/Dalamud/Game/Text/SeStringHandling/Payloads/ItemPayload.cs index c31707ff2..d6fd897b8 100644 --- a/Dalamud/Game/Text/SeStringHandling/Payloads/ItemPayload.cs +++ b/Dalamud/Game/Text/SeStringHandling/Payloads/ItemPayload.cs @@ -4,6 +4,8 @@ using System.Linq; using System.Text; using Dalamud.Data; +using Dalamud.Utility; + using Lumina.Excel; using Lumina.Excel.Sheets; using Newtonsoft.Json; @@ -73,6 +75,7 @@ public class ItemPayload : Payload /// /// Kinds of items that can be fetched from this payload. /// + [Api12ToDo("Move this out of ItemPayload. It's used in other classes too.")] public enum ItemKind : uint { /// @@ -121,7 +124,7 @@ public class ItemPayload : Payload /// Gets the actual item ID of this payload. /// [JsonIgnore] - public uint ItemId => GetAdjustedId(this.rawItemId).ItemId; + public uint ItemId => ItemUtil.GetBaseId(this.rawItemId).ItemId; /// /// Gets the raw, unadjusted item ID of this payload. @@ -161,7 +164,7 @@ public class ItemPayload : Payload /// The created item payload. public static ItemPayload FromRaw(uint rawItemId, string? displayNameOverride = null) { - var (id, kind) = GetAdjustedId(rawItemId); + var (id, kind) = ItemUtil.GetBaseId(rawItemId); return new ItemPayload(id, kind, displayNameOverride); } @@ -230,7 +233,7 @@ public class ItemPayload : Payload protected override void DecodeImpl(BinaryReader reader, long endOfStream) { this.rawItemId = GetInteger(reader); - this.Kind = GetAdjustedId(this.rawItemId).Kind; + this.Kind = ItemUtil.GetBaseId(this.rawItemId).Kind; if (reader.BaseStream.Position + 3 < endOfStream) { @@ -255,15 +258,4 @@ public class ItemPayload : Payload this.displayName = Encoding.UTF8.GetString(itemNameBytes); } } - - private static (uint ItemId, ItemKind Kind) GetAdjustedId(uint rawItemId) - { - return rawItemId switch - { - > 500_000 and < 1_000_000 => (rawItemId - 500_000, ItemKind.Collectible), - > 1_000_000 and < 2_000_000 => (rawItemId - 1_000_000, ItemKind.Hq), - > 2_000_000 => (rawItemId, ItemKind.EventItem), // EventItem IDs are NOT adjusted - _ => (rawItemId, ItemKind.Normal), - }; - } } diff --git a/Dalamud/Game/Text/SeStringHandling/SeString.cs b/Dalamud/Game/Text/SeStringHandling/SeString.cs index 7f1955da5..b7618305a 100644 --- a/Dalamud/Game/Text/SeStringHandling/SeString.cs +++ b/Dalamud/Game/Text/SeStringHandling/SeString.cs @@ -5,11 +5,16 @@ using System.Runtime.InteropServices; using System.Text; using Dalamud.Data; +using Dalamud.Game.Text.Evaluator; using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Utility; + using Lumina.Excel.Sheets; + using Newtonsoft.Json; +using LSeStringBuilder = Lumina.Text.SeStringBuilder; + namespace Dalamud.Game.Text.SeStringHandling; /// @@ -187,57 +192,32 @@ public class SeString /// An SeString containing all the payloads necessary to display an item link in the chat log. public static SeString CreateItemLink(uint itemId, ItemPayload.ItemKind kind = ItemPayload.ItemKind.Normal, string? displayNameOverride = null) { - var data = Service.Get(); + var clientState = Service.Get(); + var seStringEvaluator = Service.Get(); - var displayName = displayNameOverride; - var rarity = 1; // default: white - if (displayName == null) - { - switch (kind) - { - case ItemPayload.ItemKind.Normal: - case ItemPayload.ItemKind.Collectible: - case ItemPayload.ItemKind.Hq: - var item = data.GetExcelSheet()?.GetRowOrDefault(itemId); - displayName = item?.Name.ExtractText(); - rarity = item?.Rarity ?? 1; - break; - case ItemPayload.ItemKind.EventItem: - displayName = data.GetExcelSheet()?.GetRowOrDefault(itemId)?.Name.ExtractText(); - break; - default: - throw new ArgumentOutOfRangeException(nameof(kind), kind, null); - } - } + var rawId = ItemUtil.GetRawId(itemId, kind); - if (displayName == null) - { + var displayName = displayNameOverride ?? ItemUtil.GetItemName(rawId); + if (displayName.IsEmpty) throw new Exception("Invalid item ID specified, could not determine item name."); - } - if (kind == ItemPayload.ItemKind.Hq) - { - displayName += $" {(char)SeIconChar.HighQuality}"; - } - else if (kind == ItemPayload.ItemKind.Collectible) - { - displayName += $" {(char)SeIconChar.Collectible}"; - } + var copyName = ItemUtil.GetItemName(rawId, false).ExtractText(); + var textColor = ItemUtil.GetItemRarityColorType(rawId); + var textEdgeColor = textColor + 1u; - var textColor = (ushort)(549 + ((rarity - 1) * 2)); - var textGlowColor = (ushort)(textColor + 1); + var sb = LSeStringBuilder.SharedPool.Get(); + var itemLink = sb + .PushColorType(textColor) + .PushEdgeColorType(textEdgeColor) + .PushLinkItem(rawId, copyName) + .Append(displayName) + .PopLink() + .PopEdgeColorType() + .PopColorType() + .ToReadOnlySeString(); + LSeStringBuilder.SharedPool.Return(sb); - // Note: `SeStringBuilder.AddItemLink` uses this function, so don't call it here! - return new SeStringBuilder() - .AddUiForeground(textColor) - .AddUiGlow(textGlowColor) - .Add(new ItemPayload(itemId, kind)) - .Append(TextArrowPayloads) - .AddText(displayName) - .AddUiGlowOff() - .AddUiForegroundOff() - .Add(RawPayload.LinkTerminator) - .Build(); + return SeString.Parse(seStringEvaluator.EvaluateFromAddon(371, [itemLink], clientState.ClientLanguage)); } /// @@ -301,7 +281,7 @@ public class SeString public static SeString CreateMapLink( uint territoryId, uint mapId, float xCoord, float yCoord, float fudgeFactor = 0.05f) => CreateMapLinkWithInstance(territoryId, mapId, null, xCoord, yCoord, fudgeFactor); - + /// /// Creates an SeString representing an entire Payload chain that can be used to link a map position in the chat log. /// @@ -340,7 +320,7 @@ public class SeString /// An SeString containing all of the payloads necessary to display a map link in the chat log. public static SeString? CreateMapLink(string placeName, float xCoord, float yCoord, float fudgeFactor = 0.05f) => CreateMapLinkWithInstance(placeName, null, xCoord, yCoord, fudgeFactor); - + /// /// Creates an SeString representing an entire Payload chain that can be used to link a map position in the chat log, matching a specified zone name. /// Returns null if no corresponding PlaceName was found. @@ -511,7 +491,7 @@ public class SeString { messageBytes.AddRange(p.Encode()); } - + // Add Null Terminator messageBytes.Add(0); @@ -526,7 +506,7 @@ public class SeString { return this.TextValue; } - + private static string GetMapLinkNameString(string placeName, int? instance, string coordinateString) { var instanceString = string.Empty; @@ -534,7 +514,7 @@ public class SeString { instanceString = (SeIconChar.Instance1 + instance.Value - 1).ToIconString(); } - + return $"{placeName}{instanceString} {coordinateString}"; } } diff --git a/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs index 4937e4af0..9221c2dc5 100644 --- a/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs +++ b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs @@ -7,7 +7,6 @@ using BitFaster.Caching.Lru; using Dalamud.Data; using Dalamud.Game; -using Dalamud.Game.Config; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Interface.ImGuiSeStringRenderer.Internal.TextProcessing; using Dalamud.Interface.Utility; @@ -44,9 +43,6 @@ internal unsafe class SeStringRenderer : IInternalDisposableService /// of this placeholder. On its own, usually displayed like [OBJ]. private const int ObjectReplacementCharacter = '\uFFFC'; - [ServiceManager.ServiceDependency] - private readonly GameConfig gameConfig = Service.Get(); - /// Cache of compiled SeStrings from . private readonly ConcurrentLru cache = new(1024); @@ -570,70 +566,16 @@ internal unsafe class SeStringRenderer : IInternalDisposableService // Apply gamepad key mapping to icons. case MacroCode.Icon2 when payload.TryGetExpression(out var icon) && icon.TryGetInt(out var iconId): - var configName = (BitmapFontIcon)iconId switch + ref var iconMapping = ref RaptureAtkModule.Instance()->AtkFontManager.Icon2RemapTable; + for (var i = 0; i < 30; i++) { - ControllerShoulderLeft => SystemConfigOption.PadButton_L1, - ControllerShoulderRight => SystemConfigOption.PadButton_R1, - ControllerTriggerLeft => SystemConfigOption.PadButton_L2, - ControllerTriggerRight => SystemConfigOption.PadButton_R2, - ControllerButton3 => SystemConfigOption.PadButton_Triangle, - ControllerButton1 => SystemConfigOption.PadButton_Cross, - ControllerButton0 => SystemConfigOption.PadButton_Circle, - ControllerButton2 => SystemConfigOption.PadButton_Square, - ControllerStart => SystemConfigOption.PadButton_Start, - ControllerBack => SystemConfigOption.PadButton_Select, - ControllerAnalogLeftStick => SystemConfigOption.PadButton_LS, - ControllerAnalogLeftStickIn => SystemConfigOption.PadButton_LS, - ControllerAnalogLeftStickUpDown => SystemConfigOption.PadButton_LS, - ControllerAnalogLeftStickLeftRight => SystemConfigOption.PadButton_LS, - ControllerAnalogRightStick => SystemConfigOption.PadButton_RS, - ControllerAnalogRightStickIn => SystemConfigOption.PadButton_RS, - ControllerAnalogRightStickUpDown => SystemConfigOption.PadButton_RS, - ControllerAnalogRightStickLeftRight => SystemConfigOption.PadButton_RS, - _ => (SystemConfigOption?)null, - }; - - if (configName is null || !this.gameConfig.TryGet(configName.Value, out PadButtonValue pb)) - return (BitmapFontIcon)iconId; - - return pb switch - { - PadButtonValue.Autorun_Support => ControllerShoulderLeft, - PadButtonValue.Hotbar_Set_Change => ControllerShoulderRight, - PadButtonValue.XHB_Left_Start => ControllerTriggerLeft, - PadButtonValue.XHB_Right_Start => ControllerTriggerRight, - PadButtonValue.Jump => ControllerButton3, - PadButtonValue.Accept => ControllerButton0, - PadButtonValue.Cancel => ControllerButton1, - PadButtonValue.Map_Sub => ControllerButton2, - PadButtonValue.MainCommand => ControllerStart, - PadButtonValue.HUD_Select => ControllerBack, - PadButtonValue.Move_Operation => (BitmapFontIcon)iconId switch + if (iconMapping[i].IconId == iconId) { - ControllerAnalogLeftStick => ControllerAnalogLeftStick, - ControllerAnalogLeftStickIn => ControllerAnalogLeftStickIn, - ControllerAnalogLeftStickUpDown => ControllerAnalogLeftStickUpDown, - ControllerAnalogLeftStickLeftRight => ControllerAnalogLeftStickLeftRight, - ControllerAnalogRightStick => ControllerAnalogLeftStick, - ControllerAnalogRightStickIn => ControllerAnalogLeftStickIn, - ControllerAnalogRightStickUpDown => ControllerAnalogLeftStickUpDown, - ControllerAnalogRightStickLeftRight => ControllerAnalogLeftStickLeftRight, - _ => (BitmapFontIcon)iconId, - }, - PadButtonValue.Camera_Operation => (BitmapFontIcon)iconId switch - { - ControllerAnalogLeftStick => ControllerAnalogRightStick, - ControllerAnalogLeftStickIn => ControllerAnalogRightStickIn, - ControllerAnalogLeftStickUpDown => ControllerAnalogRightStickUpDown, - ControllerAnalogLeftStickLeftRight => ControllerAnalogRightStickLeftRight, - ControllerAnalogRightStick => ControllerAnalogRightStick, - ControllerAnalogRightStickIn => ControllerAnalogRightStickIn, - ControllerAnalogRightStickUpDown => ControllerAnalogRightStickUpDown, - ControllerAnalogRightStickLeftRight => ControllerAnalogRightStickLeftRight, - _ => (BitmapFontIcon)iconId, - }, - _ => (BitmapFontIcon)iconId, - }; + return (BitmapFontIcon)iconMapping[i].RemappedIconId; + } + } + + return (BitmapFontIcon)iconId; } return None; diff --git a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs index f3ec882fc..7326f6745 100644 --- a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs +++ b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs @@ -47,11 +47,13 @@ internal class DataWindow : Window, IDisposable new KeyStateWidget(), new MarketBoardWidget(), new NetworkMonitorWidget(), + new NounProcessorWidget(), new ObjectTableWidget(), new PartyListWidget(), new PluginIpcWidget(), new SeFontTestWidget(), new ServicesWidget(), + new SeStringCreatorWidget(), new SeStringRendererTestWidget(), new StartInfoWidget(), new TargetWidget(), @@ -68,6 +70,7 @@ internal class DataWindow : Window, IDisposable private bool isExcept; private bool selectionCollapsed; private IDataWindowWidget currentWidget; + private bool isLoaded; /// /// Initializes a new instance of the class. @@ -81,8 +84,6 @@ internal class DataWindow : Window, IDisposable this.RespectCloseHotkey = false; this.orderedModules = this.modules.OrderBy(module => module.DisplayName); this.currentWidget = this.orderedModules.First(); - - this.Load(); } /// @@ -91,6 +92,7 @@ internal class DataWindow : Window, IDisposable /// public override void OnOpen() { + this.Load(); } /// @@ -183,6 +185,7 @@ internal class DataWindow : Window, IDisposable if (ImGuiComponents.IconButton("forceReload", FontAwesomeIcon.Sync)) { + this.isLoaded = false; this.Load(); } @@ -236,6 +239,11 @@ internal class DataWindow : Window, IDisposable private void Load() { + if (this.isLoaded) + return; + + this.isLoaded = true; + foreach (var widget in this.modules) { widget.Load(); diff --git a/Dalamud/Interface/Internal/Windows/Data/WidgetUtil.cs b/Dalamud/Interface/Internal/Windows/Data/WidgetUtil.cs new file mode 100644 index 000000000..209970e24 --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/Data/WidgetUtil.cs @@ -0,0 +1,34 @@ +using Dalamud.Interface.Utility; + +using ImGuiNET; + +namespace Dalamud.Interface.Internal.Windows.Data; + +/// +/// Common utilities used in Widgets. +/// +internal class WidgetUtil +{ + /// + /// Draws text that can be copied on click. + /// + /// The text shown and to be copied. + /// The text in the tooltip. + internal static void DrawCopyableText(string text, string tooltipText = "Copy") + { + ImGuiHelpers.SafeTextWrapped(text); + + if (ImGui.IsItemHovered()) + { + ImGui.SetMouseCursor(ImGuiMouseCursor.Hand); + ImGui.BeginTooltip(); + ImGui.TextUnformatted(tooltipText); + ImGui.EndTooltip(); + } + + if (ImGui.IsItemClicked()) + { + ImGui.SetClipboardText(text); + } + } +} diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AtkArrayDataBrowserWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AtkArrayDataBrowserWidget.cs index 791dc5310..c3499570c 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/AtkArrayDataBrowserWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AtkArrayDataBrowserWidget.cs @@ -57,24 +57,6 @@ internal unsafe class AtkArrayDataBrowserWidget : IDataWindowWidget this.DrawExtendArrayTab(); } - private static void DrawCopyableText(string text, string tooltipText) - { - ImGuiHelpers.SafeTextWrapped(text); - - if (ImGui.IsItemHovered()) - { - ImGui.SetMouseCursor(ImGuiMouseCursor.Hand); - ImGui.BeginTooltip(); - ImGui.TextUnformatted(tooltipText); - ImGui.EndTooltip(); - } - - if (ImGui.IsItemClicked()) - { - ImGui.SetClipboardText(text); - } - } - private void DrawArrayList(Type? arrayType, int arrayCount, short* arrayKeys, AtkArrayData** arrays, ref int selectedIndex) { using var table = ImRaii.Table("ArkArrayTable", 3, ImGuiTableFlags.ScrollY | ImGuiTableFlags.Borders, new Vector2(300, -1)); @@ -162,7 +144,7 @@ internal unsafe class AtkArrayDataBrowserWidget : IDataWindowWidget ImGui.SameLine(); ImGui.TextUnformatted("Address: "); ImGui.SameLine(0, 0); - DrawCopyableText($"0x{(nint)array:X}", "Copy address"); + WidgetUtil.DrawCopyableText($"0x{(nint)array:X}", "Copy address"); if (array->SubscribedAddonsCount > 0) { @@ -238,22 +220,22 @@ internal unsafe class AtkArrayDataBrowserWidget : IDataWindowWidget var ptr = &array->IntArray[i]; ImGui.TableNextColumn(); // Address - DrawCopyableText($"0x{(nint)ptr:X}", "Copy entry address"); + WidgetUtil.DrawCopyableText($"0x{(nint)ptr:X}", "Copy entry address"); ImGui.TableNextColumn(); // Integer - DrawCopyableText((*ptr).ToString(), "Copy value"); + WidgetUtil.DrawCopyableText((*ptr).ToString(), "Copy value"); ImGui.TableNextColumn(); // Short - DrawCopyableText((*(short*)ptr).ToString(), "Copy as short"); + WidgetUtil.DrawCopyableText((*(short*)ptr).ToString(), "Copy as short"); ImGui.TableNextColumn(); // Byte - DrawCopyableText((*(byte*)ptr).ToString(), "Copy as byte"); + WidgetUtil.DrawCopyableText((*(byte*)ptr).ToString(), "Copy as byte"); ImGui.TableNextColumn(); // Float - DrawCopyableText((*(float*)ptr).ToString(), "Copy as float"); + WidgetUtil.DrawCopyableText((*(float*)ptr).ToString(), "Copy as float"); ImGui.TableNextColumn(); // Hex - DrawCopyableText($"0x{array->IntArray[i]:X2}", "Copy Hex"); + WidgetUtil.DrawCopyableText($"0x{array->IntArray[i]:X2}", "Copy Hex"); } } @@ -333,11 +315,11 @@ internal unsafe class AtkArrayDataBrowserWidget : IDataWindowWidget if (this.showTextAddress) { if (!isNull) - DrawCopyableText($"0x{(nint)array->StringArray[i]:X}", "Copy text address"); + WidgetUtil.DrawCopyableText($"0x{(nint)array->StringArray[i]:X}", "Copy text address"); } else { - DrawCopyableText($"0x{(nint)(&array->StringArray[i]):X}", "Copy entry address"); + WidgetUtil.DrawCopyableText($"0x{(nint)(&array->StringArray[i]):X}", "Copy entry address"); } ImGui.TableNextColumn(); // Managed @@ -351,7 +333,7 @@ internal unsafe class AtkArrayDataBrowserWidget : IDataWindowWidget { if (this.showMacroString) { - DrawCopyableText(new ReadOnlySeStringSpan(array->StringArray[i]).ToString(), "Copy text"); + WidgetUtil.DrawCopyableText(new ReadOnlySeStringSpan(array->StringArray[i]).ToString(), "Copy text"); } else { @@ -408,11 +390,11 @@ internal unsafe class AtkArrayDataBrowserWidget : IDataWindowWidget ImGui.TextUnformatted($"#{i}"); ImGui.TableNextColumn(); // Address - DrawCopyableText($"0x{(nint)(&array->DataArray[i]):X}", "Copy entry address"); + WidgetUtil.DrawCopyableText($"0x{(nint)(&array->DataArray[i]):X}", "Copy entry address"); ImGui.TableNextColumn(); // Pointer if (!isNull) - DrawCopyableText($"0x{(nint)array->DataArray[i]:X}", "Copy address"); + WidgetUtil.DrawCopyableText($"0x{(nint)array->DataArray[i]:X}", "Copy address"); } } } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/FateTableWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/FateTableWidget.cs index 1a43f2b2d..34b04dae0 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/FateTableWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/FateTableWidget.cs @@ -69,10 +69,10 @@ internal class FateTableWidget : IDataWindowWidget ImGui.TextUnformatted($"#{i}"); ImGui.TableNextColumn(); // Address - DrawCopyableText($"0x{fate.Address:X}", "Click to copy Address"); + WidgetUtil.DrawCopyableText($"0x{fate.Address:X}", "Click to copy Address"); ImGui.TableNextColumn(); // FateId - DrawCopyableText(fate.FateId.ToString(), "Click to copy FateId (RowId of Fate sheet)"); + WidgetUtil.DrawCopyableText(fate.FateId.ToString(), "Click to copy FateId (RowId of Fate sheet)"); ImGui.TableNextColumn(); // State ImGui.TextUnformatted(fate.State.ToString()); @@ -140,7 +140,7 @@ internal class FateTableWidget : IDataWindowWidget ImGui.TableNextColumn(); // Name - DrawCopyableText(fate.Name.ToString(), "Click to copy Name"); + WidgetUtil.DrawCopyableText(fate.Name.ToString(), "Click to copy Name"); ImGui.TableNextColumn(); // Progress ImGui.TextUnformatted($"{fate.Progress}%"); @@ -156,28 +156,10 @@ internal class FateTableWidget : IDataWindowWidget ImGui.TextUnformatted(fate.HasBonus.ToString()); ImGui.TableNextColumn(); // Position - DrawCopyableText(fate.Position.ToString(), "Click to copy Position"); + WidgetUtil.DrawCopyableText(fate.Position.ToString(), "Click to copy Position"); ImGui.TableNextColumn(); // Radius - DrawCopyableText(fate.Radius.ToString(), "Click to copy Radius"); - } - } - - private static void DrawCopyableText(string text, string tooltipText) - { - ImGuiHelpers.SafeTextWrapped(text); - - if (ImGui.IsItemHovered()) - { - ImGui.SetMouseCursor(ImGuiMouseCursor.Hand); - ImGui.BeginTooltip(); - ImGui.TextUnformatted(tooltipText); - ImGui.EndTooltip(); - } - - if (ImGui.IsItemClicked()) - { - ImGui.SetClipboardText(text); + WidgetUtil.DrawCopyableText(fate.Radius.ToString(), "Click to copy Radius"); } } } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/NounProcessorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/NounProcessorWidget.cs new file mode 100644 index 000000000..bc0bd0ac9 --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/NounProcessorWidget.cs @@ -0,0 +1,207 @@ +using System.Linq; +using System.Text; + +using Dalamud.Data; +using Dalamud.Game; +using Dalamud.Game.ClientState; +using Dalamud.Game.Text.Noun; +using Dalamud.Game.Text.Noun.Enums; +using Dalamud.Interface.Utility.Raii; + +using ImGuiNET; +using Lumina.Data; +using Lumina.Excel; +using Lumina.Excel.Sheets; + +namespace Dalamud.Interface.Internal.Windows.Data.Widgets; + +/// +/// Widget for the NounProcessor service. +/// +internal class NounProcessorWidget : IDataWindowWidget +{ + /// A list of German grammatical cases. + internal static readonly string[] GermanCases = ["Nominative", "Genitive", "Dative", "Accusative"]; + + private static readonly Type[] NounSheets = [ + typeof(Aetheryte), + typeof(BNpcName), + typeof(BeastTribe), + typeof(DeepDungeonEquipment), + typeof(DeepDungeonItem), + typeof(DeepDungeonMagicStone), + typeof(DeepDungeonDemiclone), + typeof(ENpcResident), + typeof(EObjName), + typeof(EurekaAetherItem), + typeof(EventItem), + typeof(GCRankGridaniaFemaleText), + typeof(GCRankGridaniaMaleText), + typeof(GCRankLimsaFemaleText), + typeof(GCRankLimsaMaleText), + typeof(GCRankUldahFemaleText), + typeof(GCRankUldahMaleText), + typeof(GatheringPointName), + typeof(Glasses), + typeof(GlassesStyle), + typeof(HousingPreset), + typeof(Item), + typeof(MJIName), + typeof(Mount), + typeof(Ornament), + typeof(TripleTriadCard), + ]; + + private ClientLanguage[] languages = []; + private string[] languageNames = []; + + private int selectedSheetNameIndex = 0; + private int selectedLanguageIndex = 0; + private int rowId = 1; + private int amount = 1; + + /// + public string[]? CommandShortcuts { get; init; } = { "noun" }; + + /// + public string DisplayName { get; init; } = "Noun Processor"; + + /// + public bool Ready { get; set; } + + /// + public void Load() + { + this.languages = Enum.GetValues(); + this.languageNames = Enum.GetNames(); + this.selectedLanguageIndex = (int)Service.Get().ClientLanguage; + + this.Ready = true; + } + + /// + public void Draw() + { + var nounProcessor = Service.Get(); + var dataManager = Service.Get(); + var clientState = Service.Get(); + + var sheetType = NounSheets.ElementAt(this.selectedSheetNameIndex); + var language = this.languages[this.selectedLanguageIndex]; + + ImGui.SetNextItemWidth(300); + if (ImGui.Combo("###SelectedSheetName", ref this.selectedSheetNameIndex, NounSheets.Select(t => t.Name).ToArray(), NounSheets.Length)) + { + this.rowId = 1; + } + + ImGui.SameLine(); + + ImGui.SetNextItemWidth(120); + if (ImGui.Combo("###SelectedLanguage", ref this.selectedLanguageIndex, this.languageNames, this.languageNames.Length)) + { + language = this.languages[this.selectedLanguageIndex]; + this.rowId = 1; + } + + ImGui.SetNextItemWidth(120); + var sheet = dataManager.Excel.GetSheet(Language.English, sheetType.Name); + var minRowId = (int)sheet.FirstOrDefault().RowId; + var maxRowId = (int)sheet.LastOrDefault().RowId; + if (ImGui.InputInt("RowId###RowId", ref this.rowId, 1, 10, ImGuiInputTextFlags.AutoSelectAll)) + { + if (this.rowId < minRowId) + this.rowId = minRowId; + + if (this.rowId >= maxRowId) + this.rowId = maxRowId; + } + + ImGui.SameLine(); + ImGui.TextUnformatted($"(Range: {minRowId} - {maxRowId})"); + + ImGui.SetNextItemWidth(120); + if (ImGui.InputInt("Amount###Amount", ref this.amount, 1, 10, ImGuiInputTextFlags.AutoSelectAll)) + { + if (this.amount <= 0) + this.amount = 1; + } + + var articleTypeEnumType = language switch + { + ClientLanguage.Japanese => typeof(JapaneseArticleType), + ClientLanguage.German => typeof(GermanArticleType), + ClientLanguage.French => typeof(FrenchArticleType), + _ => typeof(EnglishArticleType), + }; + + var numCases = language == ClientLanguage.German ? 4 : 1; + +#if DEBUG + if (ImGui.Button("Copy as self-test entry")) + { + var sb = new StringBuilder(); + + foreach (var articleType in Enum.GetValues(articleTypeEnumType)) + { + for (var grammaticalCase = 0; grammaticalCase < numCases; grammaticalCase++) + { + var nounParams = new NounParams() + { + SheetName = sheetType.Name, + RowId = (uint)this.rowId, + Language = language, + Quantity = this.amount, + ArticleType = (int)articleType, + GrammaticalCase = grammaticalCase, + }; + var output = nounProcessor.ProcessNoun(nounParams).ExtractText().Replace("\"", "\\\""); + var caseParam = language == ClientLanguage.German ? $"(int)GermanCases.{GermanCases[grammaticalCase]}" : "1"; + sb.AppendLine($"new(nameof(LSheets.{sheetType.Name}), {this.rowId}, ClientLanguage.{language}, {this.amount}, (int){articleTypeEnumType.Name}.{Enum.GetName(articleTypeEnumType, articleType)}, {caseParam}, \"{output}\"),"); + } + } + + ImGui.SetClipboardText(sb.ToString()); + } +#endif + + using var table = ImRaii.Table("TextDecoderTable", 1 + numCases, ImGuiTableFlags.ScrollY | ImGuiTableFlags.RowBg | ImGuiTableFlags.Borders | ImGuiTableFlags.NoSavedSettings); + if (!table) return; + + ImGui.TableSetupColumn("ArticleType", ImGuiTableColumnFlags.WidthFixed, 150); + for (var i = 0; i < numCases; i++) + ImGui.TableSetupColumn(language == ClientLanguage.German ? GermanCases[i] : "Text"); + ImGui.TableSetupScrollFreeze(6, 1); + ImGui.TableHeadersRow(); + + foreach (var articleType in Enum.GetValues(articleTypeEnumType)) + { + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + ImGui.TableHeader(articleType.ToString()); + + for (var currentCase = 0; currentCase < numCases; currentCase++) + { + ImGui.TableNextColumn(); + + try + { + var nounParams = new NounParams() + { + SheetName = sheetType.Name, + RowId = (uint)this.rowId, + Language = language, + Quantity = this.amount, + ArticleType = (int)articleType, + GrammaticalCase = currentCase, + }; + ImGui.TextUnformatted(nounProcessor.ProcessNoun(nounParams).ExtractText()); + } + catch (Exception ex) + { + ImGui.TextUnformatted(ex.ToString()); + } + } + } + } +} diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs new file mode 100644 index 000000000..2a56cb6c7 --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs @@ -0,0 +1,1276 @@ +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Text; + +using Dalamud.Configuration.Internal; +using Dalamud.Data; +using Dalamud.Game; +using Dalamud.Game.Text.Evaluator; +using Dalamud.Game.Text.Noun.Enums; +using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; +using Dalamud.Memory; +using Dalamud.Utility; + +using FFXIVClientStructs.FFXIV.Client.System.String; +using FFXIVClientStructs.FFXIV.Client.UI; +using FFXIVClientStructs.FFXIV.Client.UI.Misc; +using FFXIVClientStructs.FFXIV.Component.Text; + +using ImGuiNET; + +using Lumina.Data; +using Lumina.Data.Files.Excel; +using Lumina.Data.Structs.Excel; +using Lumina.Excel; +using Lumina.Excel.Sheets; +using Lumina.Text.Expressions; +using Lumina.Text.Payloads; +using Lumina.Text.ReadOnly; + +using LSeStringBuilder = Lumina.Text.SeStringBuilder; + +namespace Dalamud.Interface.Internal.Windows.Data.Widgets; + +/// +/// Widget to create SeStrings. +/// +internal class SeStringCreatorWidget : IDataWindowWidget +{ + private const LinkMacroPayloadType DalamudLinkType = (LinkMacroPayloadType)Payload.EmbeddedInfoType.DalamudLink - 1; + + private readonly Dictionary expressionNames = new() + { + { MacroCode.SetResetTime, ["Hour", "WeekDay"] }, + { MacroCode.SetTime, ["Time"] }, + { MacroCode.If, ["Condition", "StatementTrue", "StatementFalse"] }, + { MacroCode.Switch, ["Condition"] }, + { MacroCode.PcName, ["EntityId"] }, + { MacroCode.IfPcGender, ["EntityId", "CaseMale", "CaseFemale"] }, + { MacroCode.IfPcName, ["EntityId", "CaseTrue", "CaseFalse"] }, + // { MacroCode.Josa, [] }, + // { MacroCode.Josaro, [] }, + { MacroCode.IfSelf, ["EntityId", "CaseTrue", "CaseFalse"] }, + // { MacroCode.NewLine, [] }, + { MacroCode.Wait, ["Seconds"] }, + { MacroCode.Icon, ["IconId"] }, + { MacroCode.Color, ["Color"] }, + { MacroCode.EdgeColor, ["Color"] }, + { MacroCode.ShadowColor, ["Color"] }, + // { MacroCode.SoftHyphen, [] }, + // { MacroCode.Key, [] }, + // { MacroCode.Scale, [] }, + { MacroCode.Bold, ["Enabled"] }, + { MacroCode.Italic, ["Enabled"] }, + // { MacroCode.Edge, [] }, + // { MacroCode.Shadow, [] }, + // { MacroCode.NonBreakingSpace, [] }, + { MacroCode.Icon2, ["IconId"] }, + // { MacroCode.Hyphen, [] }, + { MacroCode.Num, ["Value"] }, + { MacroCode.Hex, ["Value"] }, + { MacroCode.Kilo, ["Value", "Separator"] }, + { MacroCode.Byte, ["Value"] }, + { MacroCode.Sec, ["Time"] }, + { MacroCode.Time, ["Value"] }, + { MacroCode.Float, ["Value", "Radix", "Separator"] }, + { MacroCode.Link, ["Type"] }, + { MacroCode.Sheet, ["SheetName", "RowId", "ColumnIndex", "ColumnParam"] }, + { MacroCode.String, ["String"] }, + { MacroCode.Caps, ["String"] }, + { MacroCode.Head, ["String"] }, + { MacroCode.Split, ["String", "Separator"] }, + { MacroCode.HeadAll, ["String"] }, + // { MacroCode.Fixed, [] }, + { MacroCode.Lower, ["String"] }, + { MacroCode.JaNoun, ["SheetName", "ArticleType", "RowId", "Amount", "Case", "UnkInt5"] }, + { MacroCode.EnNoun, ["SheetName", "ArticleType", "RowId", "Amount", "Case", "UnkInt5"] }, + { MacroCode.DeNoun, ["SheetName", "ArticleType", "RowId", "Amount", "Case", "UnkInt5"] }, + { MacroCode.FrNoun, ["SheetName", "ArticleType", "RowId", "Amount", "Case", "UnkInt5"] }, + { MacroCode.ChNoun, ["SheetName", "ArticleType", "RowId", "Amount", "Case", "UnkInt5"] }, + { MacroCode.LowerHead, ["String"] }, + { MacroCode.ColorType, ["ColorType"] }, + { MacroCode.EdgeColorType, ["ColorType"] }, + { MacroCode.Digit, ["Value", "TargetLength"] }, + { MacroCode.Ordinal, ["Value"] }, + { MacroCode.Sound, ["IsJingle", "SoundId"] }, + { MacroCode.LevelPos, ["LevelId"] }, + }; + + private readonly Dictionary linkExpressionNames = new() + { + { LinkMacroPayloadType.Character, ["Flags", "WorldId"] }, + { LinkMacroPayloadType.Item, ["ItemId", "Rarity"] }, + { LinkMacroPayloadType.MapPosition, ["TerritoryType/MapId", "RawX", "RawY"] }, + { LinkMacroPayloadType.Quest, ["QuestId"] }, + { LinkMacroPayloadType.Achievement, ["AchievementId"] }, + { LinkMacroPayloadType.HowTo, ["HowToId"] }, + // PartyFinderNotification + { LinkMacroPayloadType.Status, ["StatusId"] }, + { LinkMacroPayloadType.PartyFinder, ["ListingId", string.Empty, "WorldId"] }, + { LinkMacroPayloadType.AkatsukiNote, ["AkatsukiNoteId"] }, + { DalamudLinkType, ["CommandId", "Extra1", "Extra2", "ExtraString"] }, + }; + + private readonly Dictionary fixedExpressionNames = new() + { + { 1, ["Type0", "Type1", "WorldId"] }, + { 2, ["Type0", "Type1", "ClassJobId", "Level"] }, + { 3, ["Type0", "Type1", "TerritoryTypeId", "Instance & MapId", "RawX", "RawY", "RawZ", "PlaceNameIdOverride"] }, + { 4, ["Type0", "Type1", "ItemId", "Rarity", string.Empty, string.Empty, "Item Name"] }, + { 5, ["Type0", "Type1", "Sound Effect Id"] }, + { 6, ["Type0", "Type1", "ObjStrId"] }, + { 7, ["Type0", "Type1", "Text"] }, + { 8, ["Type0", "Type1", "Seconds"] }, + { 9, ["Type0", "Type1", string.Empty] }, + { 10, ["Type0", "Type1", "StatusId", "HasOverride", "NameOverride", "DescriptionOverride"] }, + { 11, ["Type0", "Type1", "ListingId", string.Empty, "WorldId", "CrossWorldFlag"] }, + { 12, ["Type0", "Type1", "QuestId", string.Empty, string.Empty, string.Empty, "QuestName"] }, + }; + + private readonly List entries = [ + new TextEntry(TextEntryType.String, "Welcome to "), + new TextEntry(TextEntryType.Macro, ""), + new TextEntry(TextEntryType.Macro, ""), + new TextEntry(TextEntryType.String, "Dalamud"), + new TextEntry(TextEntryType.Macro, ""), + new TextEntry(TextEntryType.Macro, ""), + new TextEntry(TextEntryType.Macro, " "), + ]; + + private SeStringParameter[]? localParameters = [Util.GetScmVersion()]; + private ReadOnlySeString input; + private ClientLanguage? language; + private int importSelectedSheetName; + private int importRowId; + private string[]? validImportSheetNames; + private float inputsWidth; + private float lastContentWidth; + + private enum TextEntryType + { + String, + Macro, + Fixed, + } + + /// + public string[]? CommandShortcuts { get; init; } = []; + + /// + public string DisplayName { get; init; } = "SeString Creator"; + + /// + public bool Ready { get; set; } + + /// + public void Load() + { + this.language = Service.Get().EffectiveLanguage.ToClientLanguage(); + this.UpdateInputString(false); + this.Ready = true; + } + + /// + public void Draw() + { + var contentWidth = ImGui.GetContentRegionAvail().X; + + // split panels in the middle by default + if (this.inputsWidth == 0) + { + this.inputsWidth = contentWidth / 2f; + } + + // resize panels relative to the window size + if (contentWidth != this.lastContentWidth) + { + var originalWidth = this.lastContentWidth != 0 ? this.lastContentWidth : contentWidth; + this.inputsWidth = this.inputsWidth / originalWidth * contentWidth; + this.lastContentWidth = contentWidth; + } + + using var tabBar = ImRaii.TabBar("SeStringCreatorWidgetTabBar"); + if (!tabBar) return; + + this.DrawCreatorTab(contentWidth); + this.DrawGlobalParametersTab(); + } + + private void DrawCreatorTab(float contentWidth) + { + using var tab = ImRaii.TabItem("Creator"); + if (!tab) return; + + this.DrawControls(); + ImGui.Spacing(); + this.DrawInputs(); + + this.localParameters ??= this.GetLocalParameters(this.input.AsSpan(), []); + + var evaluated = Service.Get().Evaluate( + this.input.AsSpan(), + this.localParameters, + this.language); + + ImGui.SameLine(0, 0); + + ImGui.Button("###InputPanelResizer", new Vector2(4, -1)); + if (ImGui.IsItemActive()) + { + this.inputsWidth += ImGui.GetIO().MouseDelta.X; + } + + if (ImGui.IsItemHovered()) + { + ImGui.SetMouseCursor(ImGuiMouseCursor.ResizeEW); + + if (ImGui.IsMouseDoubleClicked(ImGuiMouseButton.Left)) + { + this.inputsWidth = contentWidth / 2f; + } + } + + ImGui.SameLine(); + + using var child = ImRaii.Child("Preview", new Vector2(ImGui.GetContentRegionAvail().X, -1)); + if (!child) return; + + if (this.localParameters!.Length != 0) + { + ImGui.Spacing(); + this.DrawParameters(); + } + + this.DrawPreview(evaluated); + + ImGui.Spacing(); + this.DrawPayloads(evaluated); + } + + private unsafe void DrawGlobalParametersTab() + { + using var tab = ImRaii.TabItem("Global Parameters"); + if (!tab) return; + + using var table = ImRaii.Table("GlobalParametersTable", 5, ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY | ImGuiTableFlags.NoSavedSettings); + if (!table) return; + + ImGui.TableSetupColumn("Id", ImGuiTableColumnFlags.WidthFixed, 40); + ImGui.TableSetupColumn("Type", ImGuiTableColumnFlags.WidthFixed, 100); + ImGui.TableSetupColumn("ValuePtr", ImGuiTableColumnFlags.WidthFixed, 120); + ImGui.TableSetupColumn("Value", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("Description", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupScrollFreeze(5, 1); + ImGui.TableHeadersRow(); + + var deque = RaptureTextModule.Instance()->GlobalParameters; + for (var i = 0u; i < deque.MySize; i++) + { + var item = deque[i]; + + ImGui.TableNextRow(); + ImGui.TableNextColumn(); // Id + ImGui.TextUnformatted(i.ToString()); + + ImGui.TableNextColumn(); // Type + ImGui.TextUnformatted(item.Type.ToString()); + + ImGui.TableNextColumn(); // ValuePtr + WidgetUtil.DrawCopyableText($"0x{(nint)item.ValuePtr:X}"); + + ImGui.TableNextColumn(); // Value + switch (item.Type) + { + case TextParameterType.Integer: + WidgetUtil.DrawCopyableText($"0x{item.IntValue:X}"); + ImGui.SameLine(); + WidgetUtil.DrawCopyableText(item.IntValue.ToString()); + break; + + case TextParameterType.ReferencedUtf8String: + if (item.ReferencedUtf8StringValue != null) + WidgetUtil.DrawCopyableText(new ReadOnlySeStringSpan(item.ReferencedUtf8StringValue->Utf8String).ToString()); + else + ImGui.TextUnformatted("null"); + + break; + + case TextParameterType.String: + if (item.StringValue != null) + WidgetUtil.DrawCopyableText(MemoryHelper.ReadStringNullTerminated((nint)item.StringValue)); + else + ImGui.TextUnformatted("null"); + break; + } + + ImGui.TableNextColumn(); + ImGui.TextUnformatted(i switch + { + 0 => "Player Name", + 1 => "Temp Player 1 Name", + 2 => "Temp Player 2 Name", + 3 => "Player Sex", + 4 => "Temp Player 1 Sex", + 5 => "Temp Player 2 Sex", + 6 => "Temp Player 1 Unk 1", + 7 => "Temp Player 2 Unk 1", + 10 => "Eorzea Time Hours", + 11 => "Eorzea Time Minutes", + 12 => "ColorSay", + 13 => "ColorShout", + 14 => "ColorTell", + 15 => "ColorParty", + 16 => "ColorAlliance", + 17 => "ColorLS1", + 18 => "ColorLS2", + 19 => "ColorLS3", + 20 => "ColorLS4", + 21 => "ColorLS5", + 22 => "ColorLS6", + 23 => "ColorLS7", + 24 => "ColorLS8", + 25 => "ColorFCompany", + 26 => "ColorPvPGroup", + 27 => "ColorPvPGroupAnnounce", + 28 => "ColorBeginner", + 29 => "ColorEmoteUser", + 30 => "ColorEmote", + 31 => "ColorYell", + 32 => "ColorFCAnnounce", + 33 => "ColorBeginnerAnnounce", + 34 => "ColorCWLS", + 35 => "ColorAttackSuccess", + 36 => "ColorAttackFailure", + 37 => "ColorAction", + 38 => "ColorItem", + 39 => "ColorCureGive", + 40 => "ColorBuffGive", + 41 => "ColorDebuffGive", + 42 => "ColorEcho", + 43 => "ColorSysMsg", + 51 => "Player Grand Company Rank (Maelstrom)", + 52 => "Player Grand Company Rank (Twin Adders)", + 53 => "Player Grand Company Rank (Immortal Flames)", + 54 => "Companion Name", + 55 => "Content Name", + 56 => "ColorSysBattle", + 57 => "ColorSysGathering", + 58 => "ColorSysErr", + 59 => "ColorNpcSay", + 60 => "ColorItemNotice", + 61 => "ColorGrowup", + 62 => "ColorLoot", + 63 => "ColorCraft", + 64 => "ColorGathering", + 65 => "Temp Player 1 Unk 2", + 66 => "Temp Player 2 Unk 2", + 67 => "Player ClassJobId", + 68 => "Player Level", + 70 => "Player Race", + 71 => "Player Synced Level", + 77 => "Client/Plattform?", + 78 => "Player BirthMonth", + 82 => "Datacenter Region", + 83 => "ColorCWLS2", + 84 => "ColorCWLS3", + 85 => "ColorCWLS4", + 86 => "ColorCWLS5", + 87 => "ColorCWLS6", + 88 => "ColorCWLS7", + 89 => "ColorCWLS8", + 91 => "Player Grand Company", + 92 => "TerritoryType Id", + 93 => "Is Soft Keyboard Enabled", + 94 => "LogSetRoleColor 1: LogColorRoleTank", + 95 => "LogSetRoleColor 2: LogColorRoleTank", + 96 => "LogSetRoleColor 1: LogColorRoleHealer", + 97 => "LogSetRoleColor 2: LogColorRoleHealer", + 98 => "LogSetRoleColor 1: LogColorRoleDPS", + 99 => "LogSetRoleColor 2: LogColorRoleDPS", + 100 => "LogSetRoleColor 1: LogColorOtherClass", + 101 => "LogSetRoleColor 2: LogColorOtherClass", + 102 => "Has Login Security Token", + _ => string.Empty, + }); + } + } + + private unsafe void DrawControls() + { + if (ImGui.Button("Add entry")) + { + this.entries.Add(new(TextEntryType.String, string.Empty)); + } + + ImGui.SameLine(); + + if (ImGui.Button("Add from Sheet")) + { + ImGui.OpenPopup("AddFromSheetPopup"); + } + + this.DrawAddFromSheetPopup(); + + ImGui.SameLine(); + + if (ImGui.Button("Print")) + { + var output = Utf8String.CreateEmpty(); + var temp = Utf8String.CreateEmpty(); + var temp2 = Utf8String.CreateEmpty(); + + foreach (var entry in this.entries) + { + switch (entry.Type) + { + case TextEntryType.String: + output->ConcatCStr(entry.Message); + break; + + case TextEntryType.Macro: + temp->Clear(); + RaptureTextModule.Instance()->MacroEncoder.EncodeString(temp, entry.Message); + output->Append(temp); + break; + + case TextEntryType.Fixed: + temp->SetString(entry.Message); + temp2->Clear(); + + RaptureTextModule.Instance()->TextModule.ProcessMacroCode(temp2, temp->StringPtr); + var out1 = PronounModule.Instance()->ProcessString(temp2, true); + var out2 = PronounModule.Instance()->ProcessString(out1, false); + + output->Append(out2); + break; + } + } + + RaptureLogModule.Instance()->PrintString(output->StringPtr); + temp2->Dtor(true); + temp->Dtor(true); + output->Dtor(true); + } + + ImGui.SameLine(); + + if (ImGui.Button("Print Evaluated")) + { + var sb = new LSeStringBuilder(); + + foreach (var entry in this.entries) + { + switch (entry.Type) + { + case TextEntryType.String: + sb.Append(entry.Message); + break; + + case TextEntryType.Macro: + case TextEntryType.Fixed: + sb.AppendMacroString(entry.Message); + break; + } + } + + RaptureLogModule.Instance()->PrintString(Service.Get().Evaluate(sb.ToReadOnlySeString())); + } + + if (this.entries.Count != 0) + { + ImGui.SameLine(); + + if (ImGui.Button("Copy MacroString")) + { + var sb = new LSeStringBuilder(); + + foreach (var entry in this.entries) + { + switch (entry.Type) + { + case TextEntryType.String: + sb.Append(entry.Message); + break; + + case TextEntryType.Macro: + case TextEntryType.Fixed: + sb.AppendMacroString(entry.Message); + break; + } + } + + ImGui.SetClipboardText(sb.ToReadOnlySeString().ToString()); + } + + ImGui.SameLine(); + + if (ImGui.Button("Clear entries")) + { + this.entries.Clear(); + this.UpdateInputString(); + } + } + + var raptureTextModule = RaptureTextModule.Instance(); + if (!raptureTextModule->MacroEncoder.EncoderError.IsEmpty) + { + ImGui.SameLine(); + ImGui.TextUnformatted(raptureTextModule->MacroEncoder.EncoderError.ToString()); // TODO: EncoderError doesn't clear + } + + ImGui.SameLine(); + ImGui.SetNextItemWidth(90 * ImGuiHelpers.GlobalScale); + using (var dropdown = ImRaii.Combo("##Language", this.language.ToString() ?? "Language...")) + { + if (dropdown) + { + var values = Enum.GetValues().OrderBy((ClientLanguage lang) => lang.ToString()); + foreach (var value in values) + { + if (ImGui.Selectable(Enum.GetName(value), value == this.language)) + { + this.language = value; + this.UpdateInputString(); + } + } + } + } + } + + private void DrawAddFromSheetPopup() + { + using var popup = ImRaii.Popup("AddFromSheetPopup"); + if (!popup) return; + + var dataManager = Service.Get(); + + this.validImportSheetNames ??= dataManager.Excel.SheetNames.Where(sheetName => + { + try + { + var headerFile = dataManager.GameData.GetFile($"exd/{sheetName}.exh"); + if (headerFile.Header.Variant != ExcelVariant.Default) + return false; + + var sheet = dataManager.Excel.GetSheet(Language.English, sheetName); + return sheet.Columns.Any(col => col.Type == ExcelColumnDataType.String); + } + catch + { + return false; + } + }).OrderBy(sheetName => sheetName, StringComparer.InvariantCulture).ToArray(); + + var sheetChanged = ImGui.Combo("Sheet Name", ref this.importSelectedSheetName, this.validImportSheetNames, this.validImportSheetNames.Length); + + try + { + var sheet = dataManager.Excel.GetSheet(this.language?.ToLumina() ?? Language.English, this.validImportSheetNames[this.importSelectedSheetName]); + var minRowId = (int)sheet.FirstOrDefault().RowId; + var maxRowId = (int)sheet.LastOrDefault().RowId; + + var rowIdChanged = ImGui.InputInt("RowId", ref this.importRowId, 1, 10); + + ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X); + ImGui.TextUnformatted($"(Range: {minRowId} - {maxRowId})"); + + if (sheetChanged || rowIdChanged) + { + if (sheetChanged || this.importRowId < minRowId) + this.importRowId = minRowId; + + if (this.importRowId > maxRowId) + this.importRowId = maxRowId; + } + + if (!sheet.TryGetRow((uint)this.importRowId, out var row)) + { + ImGui.TextColored(new Vector4(1, 0, 0, 1), "Row not found"); + return; + } + + ImGui.TextUnformatted("Select string to add:"); + + using var table = ImRaii.Table("StringSelectionTable", 2, ImGuiTableFlags.Borders | ImGuiTableFlags.NoSavedSettings); + if (!table) return; + + ImGui.TableSetupColumn("Column", ImGuiTableColumnFlags.WidthFixed, 50); + ImGui.TableSetupColumn("Value", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupScrollFreeze(0, 1); + ImGui.TableHeadersRow(); + + for (var i = 0; i < sheet.Columns.Count; i++) + { + var column = sheet.Columns[i]; + if (column.Type != ExcelColumnDataType.String) + continue; + + var value = row.ReadStringColumn(i); + if (value.IsEmpty) + continue; + + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(i.ToString()); + + ImGui.TableNextColumn(); + if (ImGui.Selectable($"{value.ToString().Truncate(100)}###Column{i}")) + { + foreach (var payload in value) + { + switch (payload.Type) + { + case ReadOnlySePayloadType.Text: + this.entries.Add(new(TextEntryType.String, Encoding.UTF8.GetString(payload.Body.Span))); + break; + + case ReadOnlySePayloadType.Macro: + this.entries.Add(new(TextEntryType.Macro, payload.ToString())); + break; + } + } + + this.UpdateInputString(); + ImGui.CloseCurrentPopup(); + } + } + } + catch (Exception e) + { + ImGui.TextUnformatted(e.Message); + return; + } + } + + private unsafe void DrawInputs() + { + using var child = ImRaii.Child("Inputs", new Vector2(this.inputsWidth, -1)); + if (!child) return; + + using var table = ImRaii.Table("StringMakerTable", 3, ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY | ImGuiTableFlags.NoSavedSettings); + if (!table) return; + + ImGui.TableSetupColumn("Type", ImGuiTableColumnFlags.WidthFixed, 100); + ImGui.TableSetupColumn("Text", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("Actions", ImGuiTableColumnFlags.WidthFixed, 80); + ImGui.TableSetupScrollFreeze(3, 1); + ImGui.TableHeadersRow(); + + var arrowUpButtonSize = this.GetIconButtonSize(FontAwesomeIcon.ArrowUp); + var arrowDownButtonSize = this.GetIconButtonSize(FontAwesomeIcon.ArrowDown); + var trashButtonSize = this.GetIconButtonSize(FontAwesomeIcon.Trash); + var terminalButtonSize = this.GetIconButtonSize(FontAwesomeIcon.Terminal); + + var entryToRemove = -1; + var entryToMoveUp = -1; + var entryToMoveDown = -1; + var updateString = false; + + for (var i = 0; i < this.entries.Count; i++) + { + var key = $"##Entry{i}"; + var entry = this.entries[i]; + + ImGui.TableNextRow(); + + ImGui.TableNextColumn(); // Type + var type = (int)entry.Type; + ImGui.SetNextItemWidth(-1); + if (ImGui.Combo($"##Type{i}", ref type, ["String", "Macro", "Fixed"], 3)) + { + entry.Type = (TextEntryType)type; + updateString |= true; + } + + ImGui.TableNextColumn(); // Text + var message = entry.Message; + ImGui.SetNextItemWidth(-1); + if (ImGui.InputText($"##{i}_Message", ref message, 255)) + { + entry.Message = message; + updateString |= true; + } + + ImGui.TableNextColumn(); // Actions + + if (i > 0) + { + if (this.IconButton(key + "_Up", FontAwesomeIcon.ArrowUp, "Move up")) + { + entryToMoveUp = i; + } + } + else + { + ImGui.Dummy(arrowUpButtonSize); + } + + ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X); + + if (i < this.entries.Count - 1) + { + if (this.IconButton(key + "_Down", FontAwesomeIcon.ArrowDown, "Move down")) + { + entryToMoveDown = i; + } + } + else + { + ImGui.Dummy(arrowDownButtonSize); + } + + ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X); + + if (ImGui.IsKeyDown(ImGuiKey.LeftShift) || ImGui.IsKeyDown(ImGuiKey.RightShift)) + { + if (this.IconButton(key + "_Delete", FontAwesomeIcon.Trash, "Delete")) + { + entryToRemove = i; + } + } + else + { + this.IconButton( + key + "_Delete", + FontAwesomeIcon.Trash, + "Delete with shift", + disabled: true); + } + } + + table.Dispose(); + + if (entryToMoveUp != -1) + { + var removedItem = this.entries[entryToMoveUp]; + this.entries.RemoveAt(entryToMoveUp); + this.entries.Insert(entryToMoveUp - 1, removedItem); + updateString |= true; + } + + if (entryToMoveDown != -1) + { + var removedItem = this.entries[entryToMoveDown]; + this.entries.RemoveAt(entryToMoveDown); + this.entries.Insert(entryToMoveDown + 1, removedItem); + updateString |= true; + } + + if (entryToRemove != -1) + { + this.entries.RemoveAt(entryToRemove); + updateString |= true; + } + + if (updateString) + { + this.UpdateInputString(); + } + } + + private unsafe void UpdateInputString(bool resetLocalParameters = true) + { + var sb = new LSeStringBuilder(); + + foreach (var entry in this.entries) + { + switch (entry.Type) + { + case TextEntryType.String: + sb.Append(entry.Message); + break; + + case TextEntryType.Macro: + case TextEntryType.Fixed: + sb.AppendMacroString(entry.Message); + break; + } + } + + this.input = sb.ToReadOnlySeString(); + + if (resetLocalParameters) + this.localParameters = null; + } + + private void DrawPreview(ReadOnlySeString str) + { + using var nodeColor = ImRaii.PushColor(ImGuiCol.Text, 0xFF00FF00); + using var node = ImRaii.TreeNode("Preview", ImGuiTreeNodeFlags.DefaultOpen); + nodeColor.Pop(); + if (!node) return; + + ImGui.Dummy(new Vector2(0, ImGui.GetTextLineHeight())); + ImGui.SameLine(0, 0); + ImGuiHelpers.SeStringWrapped(str); + } + + private void DrawParameters() + { + using var nodeColor = ImRaii.PushColor(ImGuiCol.Text, 0xFF00FF00); + using var node = ImRaii.TreeNode("Parameters", ImGuiTreeNodeFlags.DefaultOpen); + nodeColor.Pop(); + if (!node) return; + + for (var i = 0; i < this.localParameters!.Length; i++) + { + if (this.localParameters[i].IsString) + { + var str = this.localParameters[i].StringValue.ExtractText(); + if (ImGui.InputText($"lstr({i + 1})", ref str, 255)) + { + this.localParameters[i] = new(str); + } + } + else + { + var num = (int)this.localParameters[i].UIntValue; + if (ImGui.InputInt($"lnum({i + 1})", ref num)) + { + this.localParameters[i] = new((uint)num); + } + } + } + } + + private void DrawPayloads(ReadOnlySeString evaluated) + { + using (var nodeColor = ImRaii.PushColor(ImGuiCol.Text, 0xFF00FF00)) + using (var node = ImRaii.TreeNode("Payloads", ImGuiTreeNodeFlags.DefaultOpen | ImGuiTreeNodeFlags.SpanAvailWidth)) + { + nodeColor.Pop(); + if (node) this.DrawSeString("payloads", this.input.AsSpan(), treeNodeFlags: ImGuiTreeNodeFlags.DefaultOpen | ImGuiTreeNodeFlags.SpanAvailWidth); + } + + if (this.input.Equals(evaluated)) + return; + + using (var nodeColor = ImRaii.PushColor(ImGuiCol.Text, 0xFF00FF00)) + using (var node = ImRaii.TreeNode("Payloads (Evaluated)", ImGuiTreeNodeFlags.DefaultOpen | ImGuiTreeNodeFlags.SpanAvailWidth)) + { + nodeColor.Pop(); + if (node) this.DrawSeString("payloads-evaluated", evaluated.AsSpan(), treeNodeFlags: ImGuiTreeNodeFlags.DefaultOpen | ImGuiTreeNodeFlags.SpanAvailWidth); + } + } + + private void DrawSeString(string id, ReadOnlySeStringSpan rosss, bool asTreeNode = false, bool renderSeString = false, int depth = 0, ImGuiTreeNodeFlags treeNodeFlags = ImGuiTreeNodeFlags.None) + { + using var seStringId = ImRaii.PushId(id); + + if (rosss.PayloadCount == 0) + { + ImGui.Dummy(Vector2.Zero); + return; + } + + using var node = asTreeNode ? this.SeStringTreeNode(id, rosss) : null; + if (asTreeNode && !node!) return; + + if (!asTreeNode && renderSeString) + { + ImGuiHelpers.SeStringWrapped(rosss, new() + { + ForceEdgeColor = true, + }); + } + + var payloadIdx = -1; + foreach (var payload in rosss) + { + payloadIdx++; + using var payloadId = ImRaii.PushId(payloadIdx); + + var preview = payload.Type.ToString(); + if (payload.Type == ReadOnlySePayloadType.Macro) + preview += $": {payload.MacroCode}"; + + using var nodeColor = ImRaii.PushColor(ImGuiCol.Text, 0xFF00FFFF); + using var payloadNode = ImRaii.TreeNode($"[{payloadIdx}] {preview}", ImGuiTreeNodeFlags.DefaultOpen | ImGuiTreeNodeFlags.SpanAvailWidth); + nodeColor.Pop(); + if (!payloadNode) continue; + + using var table = ImRaii.Table($"##Payload{payloadIdx}Table", 2); + if (!table) return; + + ImGui.TableSetupColumn("Label", ImGuiTableColumnFlags.WidthFixed, 120); + ImGui.TableSetupColumn("Tree", ImGuiTableColumnFlags.WidthStretch); + + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(payload.Type == ReadOnlySePayloadType.Text ? "Text" : "ToString()"); + ImGui.TableNextColumn(); + var text = payload.ToString(); + WidgetUtil.DrawCopyableText($"\"{text}\"", text); + + if (payload.Type != ReadOnlySePayloadType.Macro) + continue; + + if (payload.ExpressionCount > 0) + { + var exprIdx = 0; + uint? subType = null; + uint? fixedType = null; + + if (payload.MacroCode == MacroCode.Link && payload.TryGetExpression(out var linkExpr1) && linkExpr1.TryGetUInt(out var linkExpr1Val)) + { + subType = linkExpr1Val; + } + else if (payload.MacroCode == MacroCode.Fixed && payload.TryGetExpression(out var fixedTypeExpr, out var linkExpr2) && fixedTypeExpr.TryGetUInt(out var fixedTypeVal) && linkExpr2.TryGetUInt(out var linkExpr2Val)) + { + subType = linkExpr2Val; + fixedType = fixedTypeVal; + } + + foreach (var expr in payload) + { + using var exprId = ImRaii.PushId(exprIdx); + + this.DrawExpression(payload.MacroCode, subType, fixedType, exprIdx++, expr); + } + } + } + } + + private unsafe void DrawExpression(MacroCode macroCode, uint? subType, uint? fixedType, int exprIdx, ReadOnlySeExpressionSpan expr) + { + ImGui.TableNextRow(); + + ImGui.TableNextColumn(); + var expressionName = this.GetExpressionName(macroCode, subType, exprIdx, expr); + ImGui.TextUnformatted($"[{exprIdx}] " + (string.IsNullOrEmpty(expressionName) ? $"Expr {exprIdx}" : expressionName)); + + ImGui.TableNextColumn(); + + if (expr.Body.IsEmpty) + { + ImGui.TextUnformatted("(?)"); + return; + } + + if (expr.TryGetUInt(out var u32)) + { + if (macroCode is MacroCode.Icon or MacroCode.Icon2 && exprIdx == 0) + { + var iconId = u32; + + if (macroCode == MacroCode.Icon2) + { + var iconMapping = RaptureAtkModule.Instance()->AtkFontManager.Icon2RemapTable; + for (var i = 0; i < 30; i++) + { + if (iconMapping[i].IconId == iconId) + { + iconId = iconMapping[i].RemappedIconId; + break; + } + } + } + + var builder = LSeStringBuilder.SharedPool.Get(); + builder.AppendIcon(iconId); + ImGuiHelpers.SeStringWrapped(builder.ToArray()); + LSeStringBuilder.SharedPool.Return(builder); + + ImGui.SameLine(); + } + + WidgetUtil.DrawCopyableText(u32.ToString()); + ImGui.SameLine(); + WidgetUtil.DrawCopyableText($"0x{u32:X}"); + + if (macroCode == MacroCode.Link && exprIdx == 0) + { + var name = subType != null && (LinkMacroPayloadType)subType == DalamudLinkType + ? "Dalamud" + : Enum.GetName((LinkMacroPayloadType)u32); + + if (!string.IsNullOrEmpty(name)) + { + ImGui.SameLine(); + ImGui.TextUnformatted(name); + } + } + + if (macroCode is MacroCode.JaNoun or MacroCode.EnNoun or MacroCode.DeNoun or MacroCode.FrNoun && exprIdx == 1) + { + var language = macroCode switch + { + MacroCode.JaNoun => ClientLanguage.Japanese, + MacroCode.DeNoun => ClientLanguage.German, + MacroCode.FrNoun => ClientLanguage.French, + _ => ClientLanguage.English, + }; + var articleTypeEnumType = language switch + { + ClientLanguage.Japanese => typeof(JapaneseArticleType), + ClientLanguage.German => typeof(GermanArticleType), + ClientLanguage.French => typeof(FrenchArticleType), + _ => typeof(EnglishArticleType), + }; + ImGui.SameLine(); + ImGui.TextUnformatted(Enum.GetName(articleTypeEnumType, u32)); + } + + if (macroCode is MacroCode.DeNoun && exprIdx == 4 && u32 is >= 0 and <= 3) + { + ImGui.SameLine(); + ImGui.TextUnformatted(NounProcessorWidget.GermanCases[u32]); + } + + if (macroCode is MacroCode.Fixed && subType != null && fixedType != null && fixedType is 100 or 200 && subType == 5 && exprIdx == 2) + { + ImGui.SameLine(); + if (ImGui.SmallButton("Play")) + { + UIGlobals.PlayChatSoundEffect(u32 + 1); + } + } + + if (macroCode is MacroCode.Link && subType != null && exprIdx == 1) + { + var dataManager = Service.Get(); + + switch ((LinkMacroPayloadType)subType) + { + case LinkMacroPayloadType.Item when dataManager.GetExcelSheet(this.language).TryGetRow(u32, out var itemRow): + ImGui.SameLine(); + ImGui.TextUnformatted(itemRow.Name.ExtractText()); + break; + + case LinkMacroPayloadType.Quest when dataManager.GetExcelSheet(this.language).TryGetRow(u32, out var questRow): + ImGui.SameLine(); + ImGui.TextUnformatted(questRow.Name.ExtractText()); + break; + + case LinkMacroPayloadType.Achievement when dataManager.GetExcelSheet(this.language).TryGetRow(u32, out var achievementRow): + ImGui.SameLine(); + ImGui.TextUnformatted(achievementRow.Name.ExtractText()); + break; + + case LinkMacroPayloadType.HowTo when dataManager.GetExcelSheet(this.language).TryGetRow(u32, out var howToRow): + ImGui.SameLine(); + ImGui.TextUnformatted(howToRow.Name.ExtractText()); + break; + + case LinkMacroPayloadType.Status when dataManager.GetExcelSheet(this.language).TryGetRow(u32, out var statusRow): + ImGui.SameLine(); + ImGui.TextUnformatted(statusRow.Name.ExtractText()); + break; + + case LinkMacroPayloadType.AkatsukiNote when + dataManager.GetSubrowExcelSheet(this.language).TryGetRow(u32, out var akatsukiNoteRow) && + dataManager.GetExcelSheet(this.language).TryGetRow((uint)akatsukiNoteRow[0].Unknown2, out var akatsukiNoteStringRow): + ImGui.SameLine(); + ImGui.TextUnformatted(akatsukiNoteStringRow.Unknown0.ExtractText()); + break; + } + } + + return; + } + + if (expr.TryGetString(out var s)) + { + this.DrawSeString("Preview", s, treeNodeFlags: ImGuiTreeNodeFlags.DefaultOpen); + return; + } + + if (expr.TryGetPlaceholderExpression(out var exprType)) + { + if (((ExpressionType)exprType).GetNativeName() is { } nativeName) + { + ImGui.TextUnformatted(nativeName); + return; + } + + ImGui.TextUnformatted($"?x{exprType:X02}"); + return; + } + + if (expr.TryGetParameterExpression(out exprType, out var e1)) + { + if (((ExpressionType)exprType).GetNativeName() is { } nativeName) + { + ImGui.TextUnformatted($"{nativeName}({e1.ToString()})"); + return; + } + + throw new InvalidOperationException("All native names must be defined for unary expressions."); + } + + if (expr.TryGetBinaryExpression(out exprType, out e1, out var e2)) + { + if (((ExpressionType)exprType).GetNativeName() is { } nativeName) + { + ImGui.TextUnformatted($"{e1.ToString()} {nativeName} {e2.ToString()}"); + return; + } + + throw new InvalidOperationException("All native names must be defined for binary expressions."); + } + + var sb = new StringBuilder(); + sb.EnsureCapacity(1 + 3 * expr.Body.Length); + sb.Append($"({expr.Body[0]:X02}"); + for (var i = 1; i < expr.Body.Length; i++) + sb.Append($" {expr.Body[i]:X02}"); + sb.Append(')'); + ImGui.TextUnformatted(sb.ToString()); + } + + private string GetExpressionName(MacroCode macroCode, uint? subType, int idx, ReadOnlySeExpressionSpan expr) + { + if (this.expressionNames.TryGetValue(macroCode, out var names) && idx < names.Length) + return names[idx]; + + if (macroCode == MacroCode.Switch) + return $"Case {idx - 1}"; + + if (macroCode == MacroCode.Link && subType != null && this.linkExpressionNames.TryGetValue((LinkMacroPayloadType)subType, out var linkNames) && idx - 1 < linkNames.Length) + return linkNames[idx - 1]; + + if (macroCode == MacroCode.Fixed && subType != null && this.fixedExpressionNames.TryGetValue((uint)subType, out var fixedNames) && idx < fixedNames.Length) + return fixedNames[idx]; + + if (macroCode == MacroCode.Link && idx == 4) + return "Copy String"; + + return string.Empty; + } + + private SeStringParameter[] GetLocalParameters(ReadOnlySeStringSpan rosss, Dictionary? parameters) + { + parameters ??= []; + + void ProcessString(ReadOnlySeStringSpan rosss) + { + foreach (var payload in rosss) + { + foreach (var expression in payload) + { + ProcessExpression(expression); + } + } + } + + void ProcessExpression(ReadOnlySeExpressionSpan expression) + { + if (expression.TryGetString(out var exprString)) + { + ProcessString(exprString); + return; + } + + if (expression.TryGetBinaryExpression(out var expressionType, out var operand1, out var operand2)) + { + ProcessExpression(operand1); + ProcessExpression(operand2); + return; + } + + if (expression.TryGetParameterExpression(out expressionType, out var operand)) + { + if (!operand.TryGetUInt(out var index)) + return; + + if (parameters.ContainsKey(index)) + return; + + if (expressionType == (int)ExpressionType.LocalNumber) + { + parameters[index] = new SeStringParameter(0); + return; + } + else if (expressionType == (int)ExpressionType.LocalString) + { + parameters[index] = new SeStringParameter(string.Empty); + return; + } + } + } + + ProcessString(rosss); + + if (parameters.Count > 0) + { + var last = parameters.OrderBy(x => x.Key).Last(); + + if (parameters.Count != last.Key) + { + // fill missing local parameter slots, so we can go off the array index in SeStringContext + + for (var i = 1u; i <= last.Key; i++) + { + if (!parameters.ContainsKey(i)) + parameters[i] = new SeStringParameter(0); + } + } + } + + return parameters.OrderBy(x => x.Key).Select(x => x.Value).ToArray(); + } + + private ImRaii.IEndObject SeStringTreeNode(string id, ReadOnlySeStringSpan previewText, uint color = 0xFF00FFFF, ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags.None) + { + using var titleColor = ImRaii.PushColor(ImGuiCol.Text, color); + var node = ImRaii.TreeNode("##" + id, flags); + ImGui.SameLine(); + ImGuiHelpers.SeStringWrapped(previewText, new() + { + ForceEdgeColor = true, + WrapWidth = 9999, + }); + return node; + } + + private bool IconButton(string key, FontAwesomeIcon icon, string tooltip, Vector2 size = default, bool disabled = false, bool active = false) + { + using var iconFont = ImRaii.PushFont(UiBuilder.IconFont); + if (!key.StartsWith("##")) key = "##" + key; + + var disposables = new List(); + + if (disabled) + { + disposables.Add(ImRaii.PushColor(ImGuiCol.Text, ImGui.GetStyle().Colors[(int)ImGuiCol.TextDisabled])); + disposables.Add(ImRaii.PushColor(ImGuiCol.ButtonActive, ImGui.GetStyle().Colors[(int)ImGuiCol.Button])); + disposables.Add(ImRaii.PushColor(ImGuiCol.ButtonHovered, ImGui.GetStyle().Colors[(int)ImGuiCol.Button])); + } + else if (active) + { + disposables.Add(ImRaii.PushColor(ImGuiCol.Button, ImGui.GetStyle().Colors[(int)ImGuiCol.ButtonActive])); + } + + var pressed = ImGui.Button(icon.ToIconString() + key, size); + + foreach (var disposable in disposables) + disposable.Dispose(); + + iconFont?.Dispose(); + + if (ImGui.IsItemHovered()) + { + ImGui.BeginTooltip(); + ImGui.TextUnformatted(tooltip); + ImGui.EndTooltip(); + } + + return pressed; + } + + private Vector2 GetIconButtonSize(FontAwesomeIcon icon) + { + using var iconFont = ImRaii.PushFont(UiBuilder.IconFont); + return ImGui.CalcTextSize(icon.ToIconString()) + ImGui.GetStyle().FramePadding * 2; + } + + private class TextEntry(TextEntryType type, string text) + { + public string Message { get; set; } = text; + + public TextEntryType Type { get; set; } = type; + } +} diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/GamepadStateAgingStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/GamepadStateAgingStep.cs index ccee570c7..323d82bbc 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/GamepadStateAgingStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/GamepadStateAgingStep.cs @@ -1,6 +1,11 @@ -using Dalamud.Game.ClientState.GamePad; +using System.Linq; -using ImGuiNET; +using Dalamud.Game.ClientState.GamePad; +using Dalamud.Interface.Utility; + +using Lumina.Text.Payloads; + +using LSeStringBuilder = Lumina.Text.SeStringBuilder; namespace Dalamud.Interface.Internal.Windows.SelfTest.AgingSteps; @@ -17,11 +22,34 @@ internal class GamepadStateAgingStep : IAgingStep { var gamepadState = Service.Get(); - ImGui.Text("Hold down North, East, L1"); + var buttons = new (GamepadButtons Button, uint IconId)[] + { + (GamepadButtons.North, 11), + (GamepadButtons.East, 8), + (GamepadButtons.L1, 12), + }; - if (gamepadState.Raw(GamepadButtons.North) == 1 - && gamepadState.Raw(GamepadButtons.East) == 1 - && gamepadState.Raw(GamepadButtons.L1) == 1) + var builder = LSeStringBuilder.SharedPool.Get(); + + builder.Append("Hold down "); + + for (var i = 0; i < buttons.Length; i++) + { + var (button, iconId) = buttons[i]; + + builder.BeginMacro(MacroCode.Icon).AppendUIntExpression(iconId).EndMacro(); + builder.PushColorRgba(gamepadState.Raw(button) == 1 ? 0x0000FF00u : 0x000000FF); + builder.Append(button.ToString()); + builder.PopColor(); + + builder.Append(i < buttons.Length - 1 ? ", " : "."); + } + + ImGuiHelpers.SeStringWrapped(builder.ToReadOnlySeString()); + + LSeStringBuilder.SharedPool.Return(builder); + + if (buttons.All(tuple => gamepadState.Raw(tuple.Button) == 1)) { return SelfTestStepResult.Pass; } diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/NounProcessorAgingStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/NounProcessorAgingStep.cs new file mode 100644 index 000000000..4073616b2 --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/NounProcessorAgingStep.cs @@ -0,0 +1,259 @@ +using Dalamud.Game; +using Dalamud.Game.Text.Noun; +using Dalamud.Game.Text.Noun.Enums; + +using ImGuiNET; + +using LSheets = Lumina.Excel.Sheets; + +namespace Dalamud.Interface.Internal.Windows.SelfTest.AgingSteps; + +/// +/// Test setup for NounProcessor. +/// +internal class NounProcessorAgingStep : IAgingStep +{ + private NounTestEntry[] tests = + [ + new(nameof(LSheets.BNpcName), 1330, ClientLanguage.Japanese, 1, (int)JapaneseArticleType.NearListener, 1, "その蜂蜜酒の運び人"), + new(nameof(LSheets.BNpcName), 1330, ClientLanguage.Japanese, 1, (int)JapaneseArticleType.Distant, 1, "蜂蜜酒の運び人"), + new(nameof(LSheets.BNpcName), 1330, ClientLanguage.Japanese, 2, (int)JapaneseArticleType.NearListener, 1, "それらの蜂蜜酒の運び人"), + new(nameof(LSheets.BNpcName), 1330, ClientLanguage.Japanese, 2, (int)JapaneseArticleType.Distant, 1, "あれらの蜂蜜酒の運び人"), + + new(nameof(LSheets.BNpcName), 1330, ClientLanguage.English, 1, (int)EnglishArticleType.Indefinite, 1, "a mead-porting Midlander"), + new(nameof(LSheets.BNpcName), 1330, ClientLanguage.English, 1, (int)EnglishArticleType.Definite, 1, "the mead-porting Midlander"), + new(nameof(LSheets.BNpcName), 1330, ClientLanguage.English, 2, (int)EnglishArticleType.Indefinite, 1, "mead-porting Midlanders"), + new(nameof(LSheets.BNpcName), 1330, ClientLanguage.English, 2, (int)EnglishArticleType.Definite, 1, "mead-porting Midlanders"), + + new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 1, (int)GermanArticleType.Indefinite, (int)GermanCases.Nominative, "ein Met schleppender Wiesländer"), + new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 1, (int)GermanArticleType.Indefinite, (int)GermanCases.Genitive, "eines Met schleppenden Wiesländers"), + new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 1, (int)GermanArticleType.Indefinite, (int)GermanCases.Dative, "einem Met schleppenden Wiesländer"), + new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 1, (int)GermanArticleType.Indefinite, (int)GermanCases.Accusative, "einen Met schleppenden Wiesländer"), + new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 1, (int)GermanArticleType.Definite, (int)GermanCases.Nominative, "der Met schleppender Wiesländer"), + new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 1, (int)GermanArticleType.Definite, (int)GermanCases.Genitive, "des Met schleppenden Wiesländers"), + new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 1, (int)GermanArticleType.Definite, (int)GermanCases.Dative, "dem Met schleppenden Wiesländer"), + new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 1, (int)GermanArticleType.Definite, (int)GermanCases.Accusative, "den Met schleppenden Wiesländer"), + new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 1, (int)GermanArticleType.Possessive, (int)GermanCases.Nominative, "dein Met schleppende Wiesländer"), + new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 1, (int)GermanArticleType.Possessive, (int)GermanCases.Genitive, "deines Met schleppenden Wiesländers"), + new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 1, (int)GermanArticleType.Possessive, (int)GermanCases.Dative, "deinem Met schleppenden Wiesländer"), + new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 1, (int)GermanArticleType.Possessive, (int)GermanCases.Accusative, "deinen Met schleppenden Wiesländer"), + new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 1, (int)GermanArticleType.Negative, (int)GermanCases.Nominative, "kein Met schleppender Wiesländer"), + new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 1, (int)GermanArticleType.Negative, (int)GermanCases.Genitive, "keines Met schleppenden Wiesländers"), + new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 1, (int)GermanArticleType.Negative, (int)GermanCases.Dative, "keinem Met schleppenden Wiesländer"), + new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 1, (int)GermanArticleType.Negative, (int)GermanCases.Accusative, "keinen Met schleppenden Wiesländer"), + new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 1, (int)GermanArticleType.ZeroArticle, (int)GermanCases.Nominative, "Met schleppender Wiesländer"), + new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 1, (int)GermanArticleType.ZeroArticle, (int)GermanCases.Genitive, "Met schleppenden Wiesländers"), + new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 1, (int)GermanArticleType.ZeroArticle, (int)GermanCases.Dative, "Met schleppendem Wiesländer"), + new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 1, (int)GermanArticleType.ZeroArticle, (int)GermanCases.Accusative, "Met schleppenden Wiesländer"), + new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 1, (int)GermanArticleType.Demonstrative, (int)GermanCases.Nominative, "dieser Met schleppende Wiesländer"), + new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 1, (int)GermanArticleType.Demonstrative, (int)GermanCases.Genitive, "dieses Met schleppenden Wiesländers"), + new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 1, (int)GermanArticleType.Demonstrative, (int)GermanCases.Dative, "diesem Met schleppenden Wiesländer"), + new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 1, (int)GermanArticleType.Demonstrative, (int)GermanCases.Accusative, "diesen Met schleppenden Wiesländer"), + new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 2, (int)GermanArticleType.Indefinite, (int)GermanCases.Nominative, "2 Met schleppenden Wiesländer"), + new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 2, (int)GermanArticleType.Indefinite, (int)GermanCases.Genitive, "2 Met schleppenden Wiesländer"), + new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 2, (int)GermanArticleType.Indefinite, (int)GermanCases.Dative, "2 Met schleppenden Wiesländern"), + new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 2, (int)GermanArticleType.Indefinite, (int)GermanCases.Accusative, "2 Met schleppenden Wiesländer"), + new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 2, (int)GermanArticleType.Definite, (int)GermanCases.Nominative, "die Met schleppende Wiesländer"), + new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 2, (int)GermanArticleType.Definite, (int)GermanCases.Genitive, "der Met schleppender Wiesländer"), + new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 2, (int)GermanArticleType.Definite, (int)GermanCases.Dative, "den Met schleppenden Wiesländern"), + new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 2, (int)GermanArticleType.Definite, (int)GermanCases.Accusative, "die Met schleppende Wiesländer"), + new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 2, (int)GermanArticleType.Possessive, (int)GermanCases.Nominative, "deine Met schleppenden Wiesländer"), + new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 2, (int)GermanArticleType.Possessive, (int)GermanCases.Genitive, "deiner Met schleppenden Wiesländer"), + new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 2, (int)GermanArticleType.Possessive, (int)GermanCases.Dative, "deinen Met schleppenden Wiesländern"), + new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 2, (int)GermanArticleType.Possessive, (int)GermanCases.Accusative, "deine Met schleppenden Wiesländer"), + new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 2, (int)GermanArticleType.Negative, (int)GermanCases.Nominative, "keine Met schleppenden Wiesländer"), + new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 2, (int)GermanArticleType.Negative, (int)GermanCases.Genitive, "keiner Met schleppenden Wiesländer"), + new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 2, (int)GermanArticleType.Negative, (int)GermanCases.Dative, "keinen Met schleppenden Wiesländern"), + new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 2, (int)GermanArticleType.Negative, (int)GermanCases.Accusative, "keine Met schleppenden Wiesländer"), + new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 2, (int)GermanArticleType.ZeroArticle, (int)GermanCases.Nominative, "Met schleppende Wiesländer"), + new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 2, (int)GermanArticleType.ZeroArticle, (int)GermanCases.Genitive, "Met schleppender Wiesländer"), + new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 2, (int)GermanArticleType.ZeroArticle, (int)GermanCases.Dative, "Met schleppenden Wiesländern"), + new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 2, (int)GermanArticleType.ZeroArticle, (int)GermanCases.Accusative, "Met schleppende Wiesländer"), + new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 2, (int)GermanArticleType.Demonstrative, (int)GermanCases.Nominative, "diese Met schleppenden Wiesländer"), + new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 2, (int)GermanArticleType.Demonstrative, (int)GermanCases.Genitive, "dieser Met schleppenden Wiesländer"), + new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 2, (int)GermanArticleType.Demonstrative, (int)GermanCases.Dative, "diesen Met schleppenden Wiesländern"), + new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 2, (int)GermanArticleType.Demonstrative, (int)GermanCases.Accusative, "diese Met schleppenden Wiesländer"), + + new(nameof(LSheets.BNpcName), 1330, ClientLanguage.French, 1, (int)FrenchArticleType.Indefinite, 1, "un livreur d'hydromel"), + new(nameof(LSheets.BNpcName), 1330, ClientLanguage.French, 1, (int)FrenchArticleType.Definite, 1, "le livreur d'hydromel"), + new(nameof(LSheets.BNpcName), 1330, ClientLanguage.French, 1, (int)FrenchArticleType.PossessiveFirstPerson, 1, "mon livreur d'hydromel"), + new(nameof(LSheets.BNpcName), 1330, ClientLanguage.French, 1, (int)FrenchArticleType.PossessiveSecondPerson, 1, "ton livreur d'hydromel"), + new(nameof(LSheets.BNpcName), 1330, ClientLanguage.French, 1, (int)FrenchArticleType.PossessiveThirdPerson, 1, "son livreur d'hydromel"), + new(nameof(LSheets.BNpcName), 1330, ClientLanguage.French, 2, (int)FrenchArticleType.Indefinite, 1, "des livreurs d'hydromel"), + new(nameof(LSheets.BNpcName), 1330, ClientLanguage.French, 2, (int)FrenchArticleType.Definite, 1, "les livreurs d'hydromel"), + new(nameof(LSheets.BNpcName), 1330, ClientLanguage.French, 2, (int)FrenchArticleType.PossessiveFirstPerson, 1, "mes livreurs d'hydromel"), + new(nameof(LSheets.BNpcName), 1330, ClientLanguage.French, 2, (int)FrenchArticleType.PossessiveSecondPerson, 1, "tes livreurs d'hydromel"), + new(nameof(LSheets.BNpcName), 1330, ClientLanguage.French, 2, (int)FrenchArticleType.PossessiveThirdPerson, 1, "ses livreurs d'hydromel"), + + new(nameof(LSheets.ENpcResident), 1031947, ClientLanguage.Japanese, 1, (int)JapaneseArticleType.NearListener, 1, "その酔いどれのネル"), + new(nameof(LSheets.ENpcResident), 1031947, ClientLanguage.Japanese, 1, (int)JapaneseArticleType.Distant, 1, "酔いどれのネル"), + + new(nameof(LSheets.ENpcResident), 1031947, ClientLanguage.English, 1, (int)EnglishArticleType.Indefinite, 1, "Nell Half-full"), + new(nameof(LSheets.ENpcResident), 1031947, ClientLanguage.English, 1, (int)EnglishArticleType.Definite, 1, "Nell Half-full"), + + new(nameof(LSheets.ENpcResident), 1031947, ClientLanguage.German, 1, (int)GermanArticleType.Indefinite, (int)GermanCases.Nominative, "Nell die Beschwipste"), + new(nameof(LSheets.ENpcResident), 1031947, ClientLanguage.German, 1, (int)GermanArticleType.Indefinite, (int)GermanCases.Genitive, "Nell der Beschwipsten"), + new(nameof(LSheets.ENpcResident), 1031947, ClientLanguage.German, 1, (int)GermanArticleType.Indefinite, (int)GermanCases.Dative, "Nell der Beschwipsten"), + new(nameof(LSheets.ENpcResident), 1031947, ClientLanguage.German, 1, (int)GermanArticleType.Indefinite, (int)GermanCases.Accusative, "Nell die Beschwipste"), + new(nameof(LSheets.ENpcResident), 1031947, ClientLanguage.German, 1, (int)GermanArticleType.Definite, (int)GermanCases.Nominative, "Nell die Beschwipste"), + new(nameof(LSheets.ENpcResident), 1031947, ClientLanguage.German, 1, (int)GermanArticleType.Definite, (int)GermanCases.Genitive, "Nell der Beschwipsten"), + new(nameof(LSheets.ENpcResident), 1031947, ClientLanguage.German, 1, (int)GermanArticleType.Definite, (int)GermanCases.Dative, "Nell der Beschwipsten"), + new(nameof(LSheets.ENpcResident), 1031947, ClientLanguage.German, 1, (int)GermanArticleType.Definite, (int)GermanCases.Accusative, "Nell die Beschwipste"), + new(nameof(LSheets.ENpcResident), 1031947, ClientLanguage.German, 1, (int)GermanArticleType.Possessive, (int)GermanCases.Nominative, "Nell die Beschwipste"), + new(nameof(LSheets.ENpcResident), 1031947, ClientLanguage.German, 1, (int)GermanArticleType.Possessive, (int)GermanCases.Genitive, "Nell der Beschwipsten"), + new(nameof(LSheets.ENpcResident), 1031947, ClientLanguage.German, 1, (int)GermanArticleType.Possessive, (int)GermanCases.Dative, "Nell der Beschwipsten"), + new(nameof(LSheets.ENpcResident), 1031947, ClientLanguage.German, 1, (int)GermanArticleType.Possessive, (int)GermanCases.Accusative, "Nell die Beschwipste"), + new(nameof(LSheets.ENpcResident), 1031947, ClientLanguage.German, 1, (int)GermanArticleType.Negative, (int)GermanCases.Nominative, "Nell die Beschwipste"), + new(nameof(LSheets.ENpcResident), 1031947, ClientLanguage.German, 1, (int)GermanArticleType.Negative, (int)GermanCases.Genitive, "Nell der Beschwipsten"), + new(nameof(LSheets.ENpcResident), 1031947, ClientLanguage.German, 1, (int)GermanArticleType.Negative, (int)GermanCases.Dative, "Nell der Beschwipsten"), + new(nameof(LSheets.ENpcResident), 1031947, ClientLanguage.German, 1, (int)GermanArticleType.Negative, (int)GermanCases.Accusative, "Nell die Beschwipste"), + new(nameof(LSheets.ENpcResident), 1031947, ClientLanguage.German, 1, (int)GermanArticleType.ZeroArticle, (int)GermanCases.Nominative, "Nell die Beschwipste"), + new(nameof(LSheets.ENpcResident), 1031947, ClientLanguage.German, 1, (int)GermanArticleType.ZeroArticle, (int)GermanCases.Genitive, "Nell der Beschwipsten"), + new(nameof(LSheets.ENpcResident), 1031947, ClientLanguage.German, 1, (int)GermanArticleType.ZeroArticle, (int)GermanCases.Dative, "Nell der Beschwipsten"), + new(nameof(LSheets.ENpcResident), 1031947, ClientLanguage.German, 1, (int)GermanArticleType.ZeroArticle, (int)GermanCases.Accusative, "Nell die Beschwipste"), + new(nameof(LSheets.ENpcResident), 1031947, ClientLanguage.German, 1, (int)GermanArticleType.Demonstrative, (int)GermanCases.Nominative, "Nell die Beschwipste"), + new(nameof(LSheets.ENpcResident), 1031947, ClientLanguage.German, 1, (int)GermanArticleType.Demonstrative, (int)GermanCases.Genitive, "Nell der Beschwipsten"), + new(nameof(LSheets.ENpcResident), 1031947, ClientLanguage.German, 1, (int)GermanArticleType.Demonstrative, (int)GermanCases.Dative, "Nell der Beschwipsten"), + new(nameof(LSheets.ENpcResident), 1031947, ClientLanguage.German, 1, (int)GermanArticleType.Demonstrative, (int)GermanCases.Accusative, "Nell die Beschwipste"), + + new(nameof(LSheets.ENpcResident), 1031947, ClientLanguage.French, 1, (int)FrenchArticleType.Indefinite, 1, "Nell la Boit-sans-soif"), + new(nameof(LSheets.ENpcResident), 1031947, ClientLanguage.French, 1, (int)FrenchArticleType.Definite, 1, "Nell la Boit-sans-soif"), + new(nameof(LSheets.ENpcResident), 1031947, ClientLanguage.French, 1, (int)FrenchArticleType.PossessiveFirstPerson, 1, "ma Nell la Boit-sans-soif"), + new(nameof(LSheets.ENpcResident), 1031947, ClientLanguage.French, 1, (int)FrenchArticleType.PossessiveSecondPerson, 1, "ta Nell la Boit-sans-soif"), + new(nameof(LSheets.ENpcResident), 1031947, ClientLanguage.French, 1, (int)FrenchArticleType.PossessiveThirdPerson, 1, "sa Nell la Boit-sans-soif"), + + new(nameof(LSheets.Item), 44348, ClientLanguage.Japanese, 1, (int)JapaneseArticleType.NearListener, 1, "その希少トームストーン:幻想"), + new(nameof(LSheets.Item), 44348, ClientLanguage.Japanese, 1, (int)JapaneseArticleType.Distant, 1, "希少トームストーン:幻想"), + new(nameof(LSheets.Item), 44348, ClientLanguage.Japanese, 2, (int)JapaneseArticleType.NearListener, 1, "それらの希少トームストーン:幻想"), + new(nameof(LSheets.Item), 44348, ClientLanguage.Japanese, 2, (int)JapaneseArticleType.Distant, 1, "あれらの希少トームストーン:幻想"), + + new(nameof(LSheets.Item), 44348, ClientLanguage.English, 1, (int)EnglishArticleType.Indefinite, 1, "an irregular tomestone of phantasmagoria"), + new(nameof(LSheets.Item), 44348, ClientLanguage.English, 1, (int)EnglishArticleType.Definite, 1, "the irregular tomestone of phantasmagoria"), + new(nameof(LSheets.Item), 44348, ClientLanguage.English, 2, (int)EnglishArticleType.Indefinite, 1, "irregular tomestones of phantasmagoria"), + new(nameof(LSheets.Item), 44348, ClientLanguage.English, 2, (int)EnglishArticleType.Definite, 1, "irregular tomestones of phantasmagoria"), + + new(nameof(LSheets.Item), 44348, ClientLanguage.German, 1, (int)GermanArticleType.Indefinite, (int)GermanCases.Nominative, "ein ungewöhnlicher Allagischer Stein der Phantasmagorie"), + new(nameof(LSheets.Item), 44348, ClientLanguage.German, 1, (int)GermanArticleType.Indefinite, (int)GermanCases.Genitive, "eines ungewöhnlichen Allagischen Steins der Phantasmagorie"), + new(nameof(LSheets.Item), 44348, ClientLanguage.German, 1, (int)GermanArticleType.Indefinite, (int)GermanCases.Dative, "einem ungewöhnlichen Allagischen Stein der Phantasmagorie"), + new(nameof(LSheets.Item), 44348, ClientLanguage.German, 1, (int)GermanArticleType.Indefinite, (int)GermanCases.Accusative, "einen ungewöhnlichen Allagischen Stein der Phantasmagorie"), + new(nameof(LSheets.Item), 44348, ClientLanguage.German, 1, (int)GermanArticleType.Definite, (int)GermanCases.Nominative, "der ungewöhnlicher Allagischer Stein der Phantasmagorie"), + new(nameof(LSheets.Item), 44348, ClientLanguage.German, 1, (int)GermanArticleType.Definite, (int)GermanCases.Genitive, "des ungewöhnlichen Allagischen Steins der Phantasmagorie"), + new(nameof(LSheets.Item), 44348, ClientLanguage.German, 1, (int)GermanArticleType.Definite, (int)GermanCases.Dative, "dem ungewöhnlichen Allagischen Stein der Phantasmagorie"), + new(nameof(LSheets.Item), 44348, ClientLanguage.German, 1, (int)GermanArticleType.Definite, (int)GermanCases.Accusative, "den ungewöhnlichen Allagischen Stein der Phantasmagorie"), + new(nameof(LSheets.Item), 44348, ClientLanguage.German, 1, (int)GermanArticleType.Possessive, (int)GermanCases.Nominative, "dein ungewöhnliche Allagische Stein der Phantasmagorie"), + new(nameof(LSheets.Item), 44348, ClientLanguage.German, 1, (int)GermanArticleType.Possessive, (int)GermanCases.Genitive, "deines ungewöhnlichen Allagischen Steins der Phantasmagorie"), + new(nameof(LSheets.Item), 44348, ClientLanguage.German, 1, (int)GermanArticleType.Possessive, (int)GermanCases.Dative, "deinem ungewöhnlichen Allagischen Stein der Phantasmagorie"), + new(nameof(LSheets.Item), 44348, ClientLanguage.German, 1, (int)GermanArticleType.Possessive, (int)GermanCases.Accusative, "deinen ungewöhnlichen Allagischen Stein der Phantasmagorie"), + new(nameof(LSheets.Item), 44348, ClientLanguage.German, 1, (int)GermanArticleType.Negative, (int)GermanCases.Nominative, "kein ungewöhnlicher Allagischer Stein der Phantasmagorie"), + new(nameof(LSheets.Item), 44348, ClientLanguage.German, 1, (int)GermanArticleType.Negative, (int)GermanCases.Genitive, "keines ungewöhnlichen Allagischen Steins der Phantasmagorie"), + new(nameof(LSheets.Item), 44348, ClientLanguage.German, 1, (int)GermanArticleType.Negative, (int)GermanCases.Dative, "keinem ungewöhnlichen Allagischen Stein der Phantasmagorie"), + new(nameof(LSheets.Item), 44348, ClientLanguage.German, 1, (int)GermanArticleType.Negative, (int)GermanCases.Accusative, "keinen ungewöhnlichen Allagischen Stein der Phantasmagorie"), + new(nameof(LSheets.Item), 44348, ClientLanguage.German, 1, (int)GermanArticleType.ZeroArticle, (int)GermanCases.Nominative, "ungewöhnlicher Allagischer Stein der Phantasmagorie"), + new(nameof(LSheets.Item), 44348, ClientLanguage.German, 1, (int)GermanArticleType.ZeroArticle, (int)GermanCases.Genitive, "ungewöhnlichen Allagischen Steins der Phantasmagorie"), + new(nameof(LSheets.Item), 44348, ClientLanguage.German, 1, (int)GermanArticleType.ZeroArticle, (int)GermanCases.Dative, "ungewöhnlichem Allagischem Stein der Phantasmagorie"), + new(nameof(LSheets.Item), 44348, ClientLanguage.German, 1, (int)GermanArticleType.ZeroArticle, (int)GermanCases.Accusative, "ungewöhnlichen Allagischen Stein der Phantasmagorie"), + new(nameof(LSheets.Item), 44348, ClientLanguage.German, 1, (int)GermanArticleType.Demonstrative, (int)GermanCases.Nominative, "dieser ungewöhnliche Allagische Stein der Phantasmagorie"), + new(nameof(LSheets.Item), 44348, ClientLanguage.German, 1, (int)GermanArticleType.Demonstrative, (int)GermanCases.Genitive, "dieses ungewöhnlichen Allagischen Steins der Phantasmagorie"), + new(nameof(LSheets.Item), 44348, ClientLanguage.German, 1, (int)GermanArticleType.Demonstrative, (int)GermanCases.Dative, "diesem ungewöhnlichen Allagischen Stein der Phantasmagorie"), + new(nameof(LSheets.Item), 44348, ClientLanguage.German, 1, (int)GermanArticleType.Demonstrative, (int)GermanCases.Accusative, "diesen ungewöhnlichen Allagischen Stein der Phantasmagorie"), + new(nameof(LSheets.Item), 44348, ClientLanguage.German, 2, (int)GermanArticleType.Indefinite, (int)GermanCases.Nominative, "2 ungewöhnlichen Allagischen Steine der Phantasmagorie"), + new(nameof(LSheets.Item), 44348, ClientLanguage.German, 2, (int)GermanArticleType.Indefinite, (int)GermanCases.Genitive, "2 ungewöhnlichen Allagischen Steine der Phantasmagorie"), + new(nameof(LSheets.Item), 44348, ClientLanguage.German, 2, (int)GermanArticleType.Indefinite, (int)GermanCases.Dative, "2 ungewöhnlichen Allagischen Steinen der Phantasmagorie"), + new(nameof(LSheets.Item), 44348, ClientLanguage.German, 2, (int)GermanArticleType.Indefinite, (int)GermanCases.Accusative, "2 ungewöhnlichen Allagischen Steine der Phantasmagorie"), + new(nameof(LSheets.Item), 44348, ClientLanguage.German, 2, (int)GermanArticleType.Definite, (int)GermanCases.Nominative, "die ungewöhnliche Allagische Steine der Phantasmagorie"), + new(nameof(LSheets.Item), 44348, ClientLanguage.German, 2, (int)GermanArticleType.Definite, (int)GermanCases.Genitive, "der ungewöhnlicher Allagischer Steine der Phantasmagorie"), + new(nameof(LSheets.Item), 44348, ClientLanguage.German, 2, (int)GermanArticleType.Definite, (int)GermanCases.Dative, "den ungewöhnlichen Allagischen Steinen der Phantasmagorie"), + new(nameof(LSheets.Item), 44348, ClientLanguage.German, 2, (int)GermanArticleType.Definite, (int)GermanCases.Accusative, "die ungewöhnliche Allagische Steine der Phantasmagorie"), + new(nameof(LSheets.Item), 44348, ClientLanguage.German, 2, (int)GermanArticleType.Possessive, (int)GermanCases.Nominative, "deine ungewöhnlichen Allagischen Steine der Phantasmagorie"), + new(nameof(LSheets.Item), 44348, ClientLanguage.German, 2, (int)GermanArticleType.Possessive, (int)GermanCases.Genitive, "deiner ungewöhnlichen Allagischen Steine der Phantasmagorie"), + new(nameof(LSheets.Item), 44348, ClientLanguage.German, 2, (int)GermanArticleType.Possessive, (int)GermanCases.Dative, "deinen ungewöhnlichen Allagischen Steinen der Phantasmagorie"), + new(nameof(LSheets.Item), 44348, ClientLanguage.German, 2, (int)GermanArticleType.Possessive, (int)GermanCases.Accusative, "deine ungewöhnlichen Allagischen Steine der Phantasmagorie"), + new(nameof(LSheets.Item), 44348, ClientLanguage.German, 2, (int)GermanArticleType.Negative, (int)GermanCases.Nominative, "keine ungewöhnlichen Allagischen Steine der Phantasmagorie"), + new(nameof(LSheets.Item), 44348, ClientLanguage.German, 2, (int)GermanArticleType.Negative, (int)GermanCases.Genitive, "keiner ungewöhnlichen Allagischen Steine der Phantasmagorie"), + new(nameof(LSheets.Item), 44348, ClientLanguage.German, 2, (int)GermanArticleType.Negative, (int)GermanCases.Dative, "keinen ungewöhnlichen Allagischen Steinen der Phantasmagorie"), + new(nameof(LSheets.Item), 44348, ClientLanguage.German, 2, (int)GermanArticleType.Negative, (int)GermanCases.Accusative, "keine ungewöhnlichen Allagischen Steine der Phantasmagorie"), + new(nameof(LSheets.Item), 44348, ClientLanguage.German, 2, (int)GermanArticleType.ZeroArticle, (int)GermanCases.Nominative, "ungewöhnliche Allagische Steine der Phantasmagorie"), + new(nameof(LSheets.Item), 44348, ClientLanguage.German, 2, (int)GermanArticleType.ZeroArticle, (int)GermanCases.Genitive, "ungewöhnlicher Allagischer Steine der Phantasmagorie"), + new(nameof(LSheets.Item), 44348, ClientLanguage.German, 2, (int)GermanArticleType.ZeroArticle, (int)GermanCases.Dative, "ungewöhnlichen Allagischen Steinen der Phantasmagorie"), + new(nameof(LSheets.Item), 44348, ClientLanguage.German, 2, (int)GermanArticleType.ZeroArticle, (int)GermanCases.Accusative, "ungewöhnliche Allagische Steine der Phantasmagorie"), + new(nameof(LSheets.Item), 44348, ClientLanguage.German, 2, (int)GermanArticleType.Demonstrative, (int)GermanCases.Nominative, "diese ungewöhnlichen Allagischen Steine der Phantasmagorie"), + new(nameof(LSheets.Item), 44348, ClientLanguage.German, 2, (int)GermanArticleType.Demonstrative, (int)GermanCases.Genitive, "dieser ungewöhnlichen Allagischen Steine der Phantasmagorie"), + new(nameof(LSheets.Item), 44348, ClientLanguage.German, 2, (int)GermanArticleType.Demonstrative, (int)GermanCases.Dative, "diesen ungewöhnlichen Allagischen Steinen der Phantasmagorie"), + new(nameof(LSheets.Item), 44348, ClientLanguage.German, 2, (int)GermanArticleType.Demonstrative, (int)GermanCases.Accusative, "diese ungewöhnlichen Allagischen Steine der Phantasmagorie"), + + new(nameof(LSheets.Item), 44348, ClientLanguage.French, 1, (int)FrenchArticleType.Indefinite, 1, "un mémoquartz inhabituel fantasmagorique"), + new(nameof(LSheets.Item), 44348, ClientLanguage.French, 1, (int)FrenchArticleType.Definite, 1, "le mémoquartz inhabituel fantasmagorique"), + new(nameof(LSheets.Item), 44348, ClientLanguage.French, 1, (int)FrenchArticleType.PossessiveFirstPerson, 1, "mon mémoquartz inhabituel fantasmagorique"), + new(nameof(LSheets.Item), 44348, ClientLanguage.French, 1, (int)FrenchArticleType.PossessiveSecondPerson, 1, "ton mémoquartz inhabituel fantasmagorique"), + new(nameof(LSheets.Item), 44348, ClientLanguage.French, 1, (int)FrenchArticleType.PossessiveThirdPerson, 1, "son mémoquartz inhabituel fantasmagorique"), + new(nameof(LSheets.Item), 44348, ClientLanguage.French, 2, (int)FrenchArticleType.Indefinite, 1, "des mémoquartz inhabituels fantasmagoriques"), + new(nameof(LSheets.Item), 44348, ClientLanguage.French, 2, (int)FrenchArticleType.Definite, 1, "les mémoquartz inhabituels fantasmagoriques"), + new(nameof(LSheets.Item), 44348, ClientLanguage.French, 2, (int)FrenchArticleType.PossessiveFirstPerson, 1, "mes mémoquartz inhabituels fantasmagoriques"), + new(nameof(LSheets.Item), 44348, ClientLanguage.French, 2, (int)FrenchArticleType.PossessiveSecondPerson, 1, "tes mémoquartz inhabituels fantasmagoriques"), + new(nameof(LSheets.Item), 44348, ClientLanguage.French, 2, (int)FrenchArticleType.PossessiveThirdPerson, 1, "ses mémoquartz inhabituels fantasmagoriques"), + + new(nameof(LSheets.Action), 45, ClientLanguage.German, 1, (int)FrenchArticleType.Indefinite, 1, "Blumenflüsterer IV"), + ]; + + private enum GermanCases + { + Nominative, + Genitive, + Dative, + Accusative, + } + + /// + public string Name => "Test NounProcessor"; + + /// + public unsafe SelfTestStepResult RunStep() + { + var nounProcessor = Service.Get(); + + for (var i = 0; i < this.tests.Length; i++) + { + var e = this.tests[i]; + + var nounParams = new NounParams() + { + SheetName = e.SheetName, + RowId = e.RowId, + Language = e.Language, + Quantity = e.Quantity, + ArticleType = e.ArticleType, + GrammaticalCase = e.GrammaticalCase, + }; + var output = nounProcessor.ProcessNoun(nounParams); + + if (e.ExpectedResult != output) + { + ImGui.TextUnformatted($"Mismatch detected (Test #{i}):"); + ImGui.TextUnformatted($"Got: {output}"); + ImGui.TextUnformatted($"Expected: {e.ExpectedResult}"); + + if (ImGui.Button("Continue")) + return SelfTestStepResult.Fail; + + return SelfTestStepResult.Waiting; + } + } + + return SelfTestStepResult.Pass; + } + + /// + public void CleanUp() + { + // ignored + } + + private record struct NounTestEntry( + string SheetName, + uint RowId, + ClientLanguage Language, + int Quantity, + int ArticleType, + int GrammaticalCase, + string ExpectedResult); +} diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/SeStringEvaluatorAgingStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/SeStringEvaluatorAgingStep.cs new file mode 100644 index 000000000..3a0a7d546 --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/SeStringEvaluatorAgingStep.cs @@ -0,0 +1,92 @@ +using Dalamud.Game.ClientState; +using Dalamud.Game.Text.Evaluator; + +using ImGuiNET; + +using Lumina.Text.ReadOnly; + +namespace Dalamud.Interface.Internal.Windows.SelfTest.AgingSteps; + +/// +/// Test setup for SeStringEvaluator. +/// +internal class SeStringEvaluatorAgingStep : IAgingStep +{ + private int step = 0; + + /// + public string Name => "Test SeStringEvaluator"; + + /// + public SelfTestStepResult RunStep() + { + var seStringEvaluator = Service.Get(); + + switch (this.step) + { + case 0: + ImGui.TextUnformatted("Is this the current time, and is it ticking?"); + + // This checks that EvaluateFromAddon fetches the correct Addon row, + // that MacroDecoder.GetMacroTime()->SetTime() has been called + // and that local and global parameters have been read correctly. + + ImGui.TextUnformatted(seStringEvaluator.EvaluateFromAddon(31, [(uint)DateTimeOffset.UtcNow.ToUnixTimeSeconds()]).ExtractText()); + + if (ImGui.Button("Yes")) + this.step++; + + ImGui.SameLine(); + + if (ImGui.Button("No")) + return SelfTestStepResult.Fail; + + break; + + case 1: + ImGui.TextUnformatted("Checking pcname macro using the local player name..."); + + // This makes sure that NameCache.Instance()->TryGetCharacterInfoByEntityId() has been called, + // that it returned the local players name by using its EntityId, + // and that it didn't include the world name by checking the HomeWorldId against AgentLobby.Instance()->LobbyData.HomeWorldId. + + var clientState = Service.Get(); + var localPlayer = clientState.LocalPlayer; + if (localPlayer is null) + { + ImGui.TextUnformatted("You need to be logged in for this step."); + + if (ImGui.Button("Skip")) + return SelfTestStepResult.NotRan; + + return SelfTestStepResult.Waiting; + } + + var evaluatedPlayerName = seStringEvaluator.Evaluate(ReadOnlySeString.FromMacroString(""), [localPlayer.EntityId]).ExtractText(); + var localPlayerName = localPlayer.Name.TextValue; + + if (evaluatedPlayerName != localPlayerName) + { + ImGui.TextUnformatted("The player name doesn't match:"); + ImGui.TextUnformatted($"Evaluated Player Name (got): {evaluatedPlayerName}"); + ImGui.TextUnformatted($"Local Player Name (expected): {localPlayerName}"); + + if (ImGui.Button("Continue")) + return SelfTestStepResult.Fail; + + return SelfTestStepResult.Waiting; + } + + return SelfTestStepResult.Pass; + } + + return SelfTestStepResult.Waiting; + } + + /// + public void CleanUp() + { + // ignored + this.step = 0; + } +} diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/SheetRedirectResolverAgingStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/SheetRedirectResolverAgingStep.cs new file mode 100644 index 000000000..0c9dc763f --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/SheetRedirectResolverAgingStep.cs @@ -0,0 +1,130 @@ +using System.Runtime.InteropServices; + +using Dalamud.Game; +using Dalamud.Game.Text.Evaluator.Internal; + +using FFXIVClientStructs.FFXIV.Client.System.String; +using FFXIVClientStructs.FFXIV.Client.UI.Misc; + +using ImGuiNET; + +namespace Dalamud.Interface.Internal.Windows.SelfTest.AgingSteps; + +/// +/// Test setup for SheetRedirectResolver. +/// +internal class SheetRedirectResolverAgingStep : IAgingStep +{ + private RedirectEntry[] redirects = + [ + new("Item", 10, SheetRedirectFlags.Item), + new("ItemHQ", 10, SheetRedirectFlags.Item | SheetRedirectFlags.HighQuality), + new("ItemMP", 10, SheetRedirectFlags.Item | SheetRedirectFlags.Collectible), + new("Item", 35588, SheetRedirectFlags.Item), + new("Item", 1035588, SheetRedirectFlags.Item | SheetRedirectFlags.HighQuality), + new("Item", 2000217, SheetRedirectFlags.Item | SheetRedirectFlags.EventItem), + new("ActStr", 10, SheetRedirectFlags.Action), // Trait + new("ActStr", 1000010, SheetRedirectFlags.Action | SheetRedirectFlags.ActionSheet), // Action + new("ActStr", 2000010, SheetRedirectFlags.Action), // Item + new("ActStr", 3000010, SheetRedirectFlags.Action), // EventItem + new("ActStr", 4000010, SheetRedirectFlags.Action), // EventAction + new("ActStr", 5000010, SheetRedirectFlags.Action), // GeneralAction + new("ActStr", 6000010, SheetRedirectFlags.Action), // BuddyAction + new("ActStr", 7000010, SheetRedirectFlags.Action), // MainCommand + new("ActStr", 8000010, SheetRedirectFlags.Action), // Companion + new("ActStr", 9000010, SheetRedirectFlags.Action), // CraftAction + new("ActStr", 10000010, SheetRedirectFlags.Action | SheetRedirectFlags.ActionSheet), // Action + new("ActStr", 11000010, SheetRedirectFlags.Action), // PetAction + new("ActStr", 12000010, SheetRedirectFlags.Action), // CompanyAction + new("ActStr", 13000010, SheetRedirectFlags.Action), // Mount + // new("ActStr", 14000010, RedirectFlags.Action), + // new("ActStr", 15000010, RedirectFlags.Action), + // new("ActStr", 16000010, RedirectFlags.Action), + // new("ActStr", 17000010, RedirectFlags.Action), + // new("ActStr", 18000010, RedirectFlags.Action), + new("ActStr", 19000010, SheetRedirectFlags.Action), // BgcArmyAction + new("ActStr", 20000010, SheetRedirectFlags.Action), // Ornament + new("ObjStr", 10), // BNpcName + new("ObjStr", 1000010), // ENpcResident + new("ObjStr", 2000010), // Treasure + new("ObjStr", 3000010), // Aetheryte + new("ObjStr", 4000010), // GatheringPointName + new("ObjStr", 5000010), // EObjName + new("ObjStr", 6000010), // Mount + new("ObjStr", 7000010), // Companion + // new("ObjStr", 8000010), + // new("ObjStr", 9000010), + new("ObjStr", 10000010), // Item + new("EObj", 2003479), // EObj => EObjName + new("Treasure", 1473), // Treasure (without name, falls back to rowId 0) + new("Treasure", 1474), // Treasure (with name) + new("WeatherPlaceName", 0), + new("WeatherPlaceName", 28), + new("WeatherPlaceName", 40), + new("WeatherPlaceName", 52), + new("WeatherPlaceName", 2300), + ]; + + private unsafe delegate SheetRedirectFlags ResolveSheetRedirect(RaptureTextModule* thisPtr, Utf8String* sheetName, uint* rowId, uint* flags); + + /// + public string Name => "Test SheetRedirectResolver"; + + /// + public unsafe SelfTestStepResult RunStep() + { + // Client::UI::Misc::RaptureTextModule_ResolveSheetRedirect + if (!Service.Get().TryScanText("E8 ?? ?? ?? ?? 44 8B E8 A8 10", out var addr)) + return SelfTestStepResult.Fail; + + var sheetRedirectResolver = Service.Get(); + var resolveSheetRedirect = Marshal.GetDelegateForFunctionPointer(addr); + var utf8SheetName = Utf8String.CreateEmpty(); + + try + { + for (var i = 0; i < this.redirects.Length; i++) + { + var redirect = this.redirects[i]; + + utf8SheetName->SetString(redirect.SheetName); + + var rowId1 = redirect.RowId; + uint colIndex1 = ushort.MaxValue; + var flags1 = resolveSheetRedirect(RaptureTextModule.Instance(), utf8SheetName, &rowId1, &colIndex1); + + var sheetName2 = redirect.SheetName; + var rowId2 = redirect.RowId; + uint colIndex2 = ushort.MaxValue; + var flags2 = sheetRedirectResolver.Resolve(ref sheetName2, ref rowId2, ref colIndex2); + + if (utf8SheetName->ToString() != sheetName2 || rowId1 != rowId2 || colIndex1 != colIndex2 || flags1 != flags2) + { + ImGui.TextUnformatted($"Mismatch detected (Test #{i}):"); + ImGui.TextUnformatted($"Input: {redirect.SheetName}#{redirect.RowId}"); + ImGui.TextUnformatted($"Game: {utf8SheetName->ToString()}#{rowId1}-{colIndex1} ({flags1})"); + ImGui.TextUnformatted($"Evaluated: {sheetName2}#{rowId2}-{colIndex2} ({flags2})"); + + if (ImGui.Button("Continue")) + return SelfTestStepResult.Fail; + + return SelfTestStepResult.Waiting; + } + } + + return SelfTestStepResult.Pass; + } + finally + { + utf8SheetName->Dtor(true); + } + } + + /// + public void CleanUp() + { + // ignored + } + + private record struct RedirectEntry(string SheetName, uint RowId, SheetRedirectFlags Flags = SheetRedirectFlags.None); +} diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs b/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs index 3b3670228..1be6f31a3 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs @@ -50,6 +50,9 @@ internal class SelfTestWindow : Window new DutyStateAgingStep(), new GameConfigAgingStep(), new MarketBoardAgingStep(), + new SheetRedirectResolverAgingStep(), + new NounProcessorAgingStep(), + new SeStringEvaluatorAgingStep(), new LogoutEventAgingStep(), }; diff --git a/Dalamud/Interface/Utility/ImGuiHelpers.cs b/Dalamud/Interface/Utility/ImGuiHelpers.cs index 2093d9bcb..d9056fec4 100644 --- a/Dalamud/Interface/Utility/ImGuiHelpers.cs +++ b/Dalamud/Interface/Utility/ImGuiHelpers.cs @@ -210,8 +210,6 @@ public static class ImGuiHelpers /// ImGui ID, if link functionality is desired. /// Button flags to use on link interaction. /// Interaction result of the rendered text. - /// This function is experimental. Report any issues to GitHub issues or to Discord #dalamud-dev channel. - /// The function definition is stable; only in the next API version a function may be removed. public static SeStringDrawResult SeStringWrapped( ReadOnlySpan sss, scoped in SeStringDrawParams style = default, @@ -226,8 +224,6 @@ public static class ImGuiHelpers /// ImGui ID, if link functionality is desired. /// Button flags to use on link interaction. /// Interaction result of the rendered text. - /// This function is experimental. Report any issues to GitHub issues or to Discord #dalamud-dev channel. - /// The function definition is stable; only in the next API version a function may be removed. public static SeStringDrawResult CompileSeStringWrapped( string text, scoped in SeStringDrawParams style = default, diff --git a/Dalamud/Plugin/Services/ISeStringEvaluator.cs b/Dalamud/Plugin/Services/ISeStringEvaluator.cs new file mode 100644 index 000000000..2bd423b7c --- /dev/null +++ b/Dalamud/Plugin/Services/ISeStringEvaluator.cs @@ -0,0 +1,79 @@ +using System.Diagnostics.CodeAnalysis; + +using Dalamud.Game; +using Dalamud.Game.ClientState.Objects.Enums; +using Dalamud.Game.Text.Evaluator; + +using Lumina.Text.ReadOnly; + +namespace Dalamud.Plugin.Services; + +/// +/// Defines a service for retrieving localized text for various in-game entities. +/// +[Experimental("SeStringEvaluator")] +public interface ISeStringEvaluator +{ + /// + /// Evaluates macros in a . + /// + /// The string containing macros. + /// An optional list of local parameters. + /// An optional language override. + /// An evaluated . + ReadOnlySeString Evaluate(ReadOnlySeString str, Span localParameters = default, ClientLanguage? language = null); + + /// + /// Evaluates macros in a . + /// + /// The string containing macros. + /// An optional list of local parameters. + /// An optional language override. + /// An evaluated . + ReadOnlySeString Evaluate(ReadOnlySeStringSpan str, Span localParameters = default, ClientLanguage? language = null); + + /// + /// Evaluates macros in text from the Addon sheet. + /// + /// The row id of the Addon sheet. + /// An optional list of local parameters. + /// An optional language override. + /// An evaluated . + ReadOnlySeString EvaluateFromAddon(uint addonId, Span localParameters = default, ClientLanguage? language = null); + + /// + /// Evaluates macros in text from the Lobby sheet. + /// + /// The row id of the Lobby sheet. + /// An optional list of local parameters. + /// An optional language override. + /// An evaluated . + ReadOnlySeString EvaluateFromLobby(uint lobbyId, Span localParameters = default, ClientLanguage? language = null); + + /// + /// Evaluates macros in text from the LogMessage sheet. + /// + /// The row id of the LogMessage sheet. + /// An optional list of local parameters. + /// An optional language override. + /// An evaluated . + ReadOnlySeString EvaluateFromLogMessage(uint logMessageId, Span localParameters = default, ClientLanguage? language = null); + + /// + /// Evaluates ActStr from the given ActionKind and id. + /// + /// The ActionKind. + /// The action id. + /// An optional language override. + /// The name of the action. + string EvaluateActStr(ActionKind actionKind, uint id, ClientLanguage? language = null); + + /// + /// Evaluates ObjStr from the given ObjectKind and id. + /// + /// The ObjectKind. + /// The object id. + /// An optional language override. + /// The singular name of the object. + string EvaluateObjStr(ObjectKind objectKind, uint id, ClientLanguage? language = null); +} diff --git a/Dalamud/Utility/ActionKindExtensions.cs b/Dalamud/Utility/ActionKindExtensions.cs new file mode 100644 index 000000000..21026bc31 --- /dev/null +++ b/Dalamud/Utility/ActionKindExtensions.cs @@ -0,0 +1,26 @@ +using Dalamud.Game; + +namespace Dalamud.Utility; + +/// +/// Extension methods for the enum. +/// +public static class ActionKindExtensions +{ + /// + /// Converts the id of an ActionKind to the id used in the ActStr sheet redirect. + /// + /// The ActionKind this id is for. + /// The id. + /// An id that can be used in the ActStr sheet redirect. + public static uint GetActStrId(this ActionKind actionKind, uint id) + { + // See "83 F9 0D 76 0B" + var idx = (uint)actionKind; + + if (idx is <= 13 or 19 or 20) + return id + (1000000 * idx); + + return 0; + } +} diff --git a/Dalamud/Utility/ClientLanguageExtensions.cs b/Dalamud/Utility/ClientLanguageExtensions.cs index 69c39c9b8..47f0a2082 100644 --- a/Dalamud/Utility/ClientLanguageExtensions.cs +++ b/Dalamud/Utility/ClientLanguageExtensions.cs @@ -23,4 +23,40 @@ public static class ClientLanguageExtensions _ => throw new ArgumentOutOfRangeException(nameof(language)), }; } + + /// + /// Gets the language code from a ClientLanguage. + /// + /// The ClientLanguage to convert. + /// The language code (ja, en, de, fr). + /// An exception that is thrown when no valid ClientLanguage was given. + public static string ToCode(this ClientLanguage value) + { + return value switch + { + ClientLanguage.Japanese => "ja", + ClientLanguage.English => "en", + ClientLanguage.German => "de", + ClientLanguage.French => "fr", + _ => throw new ArgumentOutOfRangeException(nameof(value)), + }; + } + + /// + /// Gets the ClientLanguage from a language code. + /// + /// The language code to convert (ja, en, de, fr). + /// The ClientLanguage. + /// An exception that is thrown when no valid language code was given. + public static ClientLanguage ToClientLanguage(this string value) + { + return value switch + { + "ja" => ClientLanguage.Japanese, + "en" => ClientLanguage.English, + "de" => ClientLanguage.German, + "fr" => ClientLanguage.French, + _ => throw new ArgumentOutOfRangeException(nameof(value)), + }; + } } diff --git a/Dalamud/Utility/ItemUtil.cs b/Dalamud/Utility/ItemUtil.cs new file mode 100644 index 000000000..de1e5a721 --- /dev/null +++ b/Dalamud/Utility/ItemUtil.cs @@ -0,0 +1,159 @@ +using System.Runtime.CompilerServices; + +using Dalamud.Data; +using Dalamud.Game; +using Dalamud.Game.Text.SeStringHandling.Payloads; +using Dalamud.Game.Text; + +using Lumina.Excel.Sheets; +using Lumina.Text.ReadOnly; + +using static Dalamud.Game.Text.SeStringHandling.Payloads.ItemPayload; +using Lumina.Text; + +namespace Dalamud.Utility; + +/// +/// Utilities related to Items. +/// +internal static class ItemUtil +{ + private static int? eventItemRowCount; + + /// Converts raw item ID to item ID with its classification. + /// Raw item ID. + /// Item ID and its classification. + internal static (uint ItemId, ItemKind Kind) GetBaseId(uint rawItemId) + { + if (IsEventItem(rawItemId)) return (rawItemId, ItemKind.EventItem); // EventItem IDs are NOT adjusted + if (IsHighQuality(rawItemId)) return (rawItemId - 1_000_000, ItemKind.Hq); + if (IsCollectible(rawItemId)) return (rawItemId - 500_000, ItemKind.Collectible); + return (rawItemId, ItemKind.Normal); + } + + /// Converts item ID with its classification to raw item ID. + /// Item ID. + /// Item classification. + /// Raw Item ID. + internal static uint GetRawId(uint itemId, ItemKind kind) + { + return kind switch + { + ItemKind.Collectible when itemId < 500_000 => itemId + 500_000, + ItemKind.Hq when itemId < 1_000_000 => itemId + 1_000_000, + ItemKind.EventItem => itemId, // EventItem IDs are not adjusted + _ => itemId, + }; + } + + /// + /// Checks if the item id belongs to a normal item. + /// + /// The item id to check. + /// true when the item id belongs to a normal item. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool IsNormalItem(uint itemId) + { + return itemId < 500_000; + } + + /// + /// Checks if the item id belongs to a collectible item. + /// + /// The item id to check. + /// true when the item id belongs to a collectible item. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool IsCollectible(uint itemId) + { + return itemId is >= 500_000 and < 1_000_000; + } + + /// + /// Checks if the item id belongs to a high quality item. + /// + /// The item id to check. + /// true when the item id belongs to a high quality item. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool IsHighQuality(uint itemId) + { + return itemId is >= 1_000_000 and < 2_000_000; + } + + /// + /// Checks if the item id belongs to an event item. + /// + /// The item id to check. + /// true when the item id belongs to an event item. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool IsEventItem(uint itemId) + { + return itemId >= 2_000_000 && itemId - 2_000_000 < (eventItemRowCount ??= Service.Get().GetExcelSheet().Count); + } + + /// + /// Gets the name of an item. + /// + /// The raw item id. + /// Whether to include the High Quality or Collectible icon. + /// An optional client language override. + /// The item name. + internal static ReadOnlySeString GetItemName(uint itemId, bool includeIcon = true, ClientLanguage? language = null) + { + var dataManager = Service.Get(); + + if (IsEventItem(itemId)) + { + return dataManager + .GetExcelSheet(language) + .TryGetRow(itemId, out var eventItem) + ? eventItem.Name + : default; + } + + var (baseId, kind) = GetBaseId(itemId); + + if (!dataManager + .GetExcelSheet(language) + .TryGetRow(baseId, out var item)) + { + return default; + } + + if (!includeIcon || kind is not (ItemKind.Hq or ItemKind.Collectible)) + return item.Name; + + var builder = SeStringBuilder.SharedPool.Get(); + + builder.Append(item.Name); + + switch (kind) + { + case ItemPayload.ItemKind.Hq: + builder.Append($" {(char)SeIconChar.HighQuality}"); + break; + case ItemPayload.ItemKind.Collectible: + builder.Append($" {(char)SeIconChar.Collectible}"); + break; + } + + var itemName = builder.ToReadOnlySeString(); + SeStringBuilder.SharedPool.Return(builder); + return itemName; + } + + /// + /// Gets the color row id for an item name. + /// + /// The raw item Id. + /// Wheather this color is used as edge color. + /// The Color row id. + internal static uint GetItemRarityColorType(uint itemId, bool isEdgeColor = false) + { + var rarity = 1u; + + if (!IsEventItem(itemId) && Service.Get().GetExcelSheet().TryGetRow(GetBaseId(itemId).ItemId, out var item)) + rarity = item.Rarity; + + return (isEdgeColor ? 548u : 547u) + (rarity * 2u); + } +} diff --git a/Dalamud/Utility/ObjectKindExtensions.cs b/Dalamud/Utility/ObjectKindExtensions.cs new file mode 100644 index 000000000..5d42dc760 --- /dev/null +++ b/Dalamud/Utility/ObjectKindExtensions.cs @@ -0,0 +1,33 @@ +using Dalamud.Game.ClientState.Objects.Enums; + +namespace Dalamud.Utility; + +/// +/// Extension methods for the enum. +/// +public static class ObjectKindExtensions +{ + /// + /// Converts the id of an ObjectKind to the id used in the ObjStr sheet redirect. + /// + /// The ObjectKind this id is for. + /// The id. + /// An id that can be used in the ObjStr sheet redirect. + public static uint GetObjStrId(this ObjectKind objectKind, uint id) + { + // See "8D 41 FE 83 F8 0C 77 4D" + return objectKind switch + { + ObjectKind.BattleNpc => id < 1000000 ? id : id - 900000, + ObjectKind.EventNpc => id, + ObjectKind.Treasure or + ObjectKind.Aetheryte or + ObjectKind.GatheringPoint or + ObjectKind.Companion or + ObjectKind.Housing => id + (1000000 * (uint)objectKind) - 2000000, + ObjectKind.EventObj => id + (1000000 * (uint)objectKind) - 4000000, + ObjectKind.CardStand => id + 3000000, + _ => 0, + }; + } +} diff --git a/Dalamud/Utility/SeStringExtensions.cs b/Dalamud/Utility/SeStringExtensions.cs index f50f19d8e..b21b9b743 100644 --- a/Dalamud/Utility/SeStringExtensions.cs +++ b/Dalamud/Utility/SeStringExtensions.cs @@ -1,3 +1,5 @@ +using System.Linq; + using Lumina.Text.Parse; using Lumina.Text.ReadOnly; @@ -74,4 +76,154 @@ public static class SeStringExtensions /// character name to validate. /// indicator if character is name is valid. public static bool IsValidCharacterName(this DSeString value) => value.ToString().IsValidCharacterName(); + + /// + /// Determines whether the contains only text payloads. + /// + /// The to check. + /// true if the string contains only text payloads; otherwise, false. + public static bool IsTextOnly(this ReadOnlySeString ross) + { + return ross.AsSpan().IsTextOnly(); + } + + /// + /// Determines whether the contains only text payloads. + /// + /// The to check. + /// true if the span contains only text payloads; otherwise, false. + public static bool IsTextOnly(this ReadOnlySeStringSpan rosss) + { + foreach (var payload in rosss) + { + if (payload.Type != ReadOnlySePayloadType.Text) + return false; + } + + return true; + } + + /// + /// Determines whether the contains the specified text. + /// + /// The to search. + /// The text to find. + /// true if the text is found; otherwise, false. + public static bool ContainsText(this ReadOnlySeString ross, ReadOnlySpan needle) + { + return ross.AsSpan().ContainsText(needle); + } + + /// + /// Determines whether the contains the specified text. + /// + /// The to search. + /// The text to find. + /// true if the text is found; otherwise, false. + public static bool ContainsText(this ReadOnlySeStringSpan rosss, ReadOnlySpan needle) + { + foreach (var payload in rosss) + { + if (payload.Type != ReadOnlySePayloadType.Text) + continue; + + if (payload.Body.IndexOf(needle) != -1) + return true; + } + + return false; + } + + /// + /// Determines whether the contains the specified text. + /// + /// The builder to search. + /// The text to find. + /// true if the text is found; otherwise, false. + public static bool ContainsText(this LSeStringBuilder builder, ReadOnlySpan needle) + { + return builder.ToReadOnlySeString().ContainsText(needle); + } + + /// + /// Replaces occurrences of a specified text in a with another text. + /// + /// The original string. + /// The text to find. + /// The replacement text. + /// A new with the replacements made. + public static ReadOnlySeString ReplaceText( + this ReadOnlySeString ross, + ReadOnlySpan toFind, + ReadOnlySpan replacement) + { + if (ross.IsEmpty) + return ross; + + var sb = LSeStringBuilder.SharedPool.Get(); + + foreach (var payload in ross) + { + if (payload.Type == ReadOnlySePayloadType.Invalid) + continue; + + if (payload.Type != ReadOnlySePayloadType.Text) + { + sb.Append(payload); + continue; + } + + var index = payload.Body.Span.IndexOf(toFind); + if (index == -1) + { + sb.Append(payload); + continue; + } + + var lastIndex = 0; + while (index != -1) + { + sb.Append(payload.Body.Span[lastIndex..index]); + + if (!replacement.IsEmpty) + { + sb.Append(replacement); + } + + lastIndex = index + toFind.Length; + index = payload.Body.Span[lastIndex..].IndexOf(toFind); + + if (index != -1) + index += lastIndex; + } + + sb.Append(payload.Body.Span[lastIndex..]); + } + + var output = sb.ToReadOnlySeString(); + LSeStringBuilder.SharedPool.Return(sb); + return output; + } + + /// + /// Replaces occurrences of a specified text in an with another text. + /// + /// The builder to modify. + /// The text to find. + /// The replacement text. + public static void ReplaceText( + this LSeStringBuilder builder, + ReadOnlySpan toFind, + ReadOnlySpan replacement) + { + if (toFind.IsEmpty) + return; + + var str = builder.ToReadOnlySeString(); + if (str.IsEmpty) + return; + + var replaced = ReplaceText(new ReadOnlySeString(builder.GetViewAsMemory()), toFind, replacement); + builder.Clear().Append(replaced); + } } diff --git a/Dalamud/Utility/StringExtensions.cs b/Dalamud/Utility/StringExtensions.cs index 24aa48446..50973e338 100644 --- a/Dalamud/Utility/StringExtensions.cs +++ b/Dalamud/Utility/StringExtensions.cs @@ -1,4 +1,5 @@ using System.Diagnostics.CodeAnalysis; +using System.Globalization; using FFXIVClientStructs.FFXIV.Client.UI; @@ -43,4 +44,48 @@ public static class StringExtensions if (!UIGlobals.IsValidPlayerCharacterName(value)) return false; return includeLegacy || value.Length <= 21; } + + /// + /// Converts the first character of the string to uppercase while leaving the rest of the string unchanged. + /// + /// The input string. + /// + /// A new string with the first character converted to uppercase. + [return: NotNullIfNotNull("input")] + public static string? FirstCharToUpper(this string? input, CultureInfo? culture = null) => + string.IsNullOrWhiteSpace(input) + ? input + : $"{char.ToUpper(input[0], culture ?? CultureInfo.CurrentCulture)}{input.AsSpan(1)}"; + + /// + /// Converts the first character of the string to lowercase while leaving the rest of the string unchanged. + /// + /// The input string. + /// + /// A new string with the first character converted to lowercase. + [return: NotNullIfNotNull("input")] + public static string? FirstCharToLower(this string? input, CultureInfo? culture = null) => + string.IsNullOrWhiteSpace(input) + ? input + : $"{char.ToLower(input[0], culture ?? CultureInfo.CurrentCulture)}{input.AsSpan(1)}"; + + /// + /// Removes soft hyphen characters (U+00AD) from the input string. + /// + /// The input string to remove soft hyphen characters from. + /// A string with all soft hyphens removed. + public static string StripSoftHyphen(this string input) => input.Replace("\u00AD", string.Empty); + + /// + /// Truncates the given string to the specified maximum number of characters, + /// appending an ellipsis if truncation occurs. + /// + /// The string to truncate. + /// The maximum allowed length of the string. + /// The string to append if truncation occurs (defaults to "..."). + /// The truncated string, or the original string if no truncation is needed. + public static string? Truncate(this string input, int maxChars, string ellipses = "...") + { + return string.IsNullOrEmpty(input) || input.Length <= maxChars ? input : input[..maxChars] + ellipses; + } } From 30cadef34b18a86f5d9d9e8693073bf1f4983ea7 Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Mon, 24 Mar 2025 09:12:13 -0700 Subject: [PATCH 23/55] fix: support XL_PLATFORM as well (hopefully) --- Dalamud.Common/Util/EnvironmentUtils.cs | 18 ++++++++++++++++++ Dalamud.Injector/EntryPoint.cs | 6 ++++++ 2 files changed, 24 insertions(+) create mode 100644 Dalamud.Common/Util/EnvironmentUtils.cs diff --git a/Dalamud.Common/Util/EnvironmentUtils.cs b/Dalamud.Common/Util/EnvironmentUtils.cs new file mode 100644 index 000000000..d6cf65e3d --- /dev/null +++ b/Dalamud.Common/Util/EnvironmentUtils.cs @@ -0,0 +1,18 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Dalamud.Common.Util; + +public static class EnvironmentUtils +{ + /// + /// Attempts to get an environment variable using the Try pattern. + /// + /// The env var to get. + /// An output containing the env var, if present. + /// A boolean indicating whether the var was present. + public static bool TryGetEnvironmentVariable(string variableName, [NotNullWhen(true)] out string? value) + { + value = Environment.GetEnvironmentVariable(variableName); + return value != null; + } +} diff --git a/Dalamud.Injector/EntryPoint.cs b/Dalamud.Injector/EntryPoint.cs index 0ee2e5507..4d26313a9 100644 --- a/Dalamud.Injector/EntryPoint.cs +++ b/Dalamud.Injector/EntryPoint.cs @@ -11,6 +11,8 @@ using System.Text.RegularExpressions; using Dalamud.Common; using Dalamud.Common.Game; +using Dalamud.Common.Util; + using Newtonsoft.Json; using Reloaded.Memory.Buffers; using Serilog; @@ -311,6 +313,10 @@ namespace Dalamud.Injector var unhandledExceptionStr = startInfo.UnhandledException.ToString().ToLowerInvariant(); var troubleshootingData = "{\"empty\": true, \"description\": \"No troubleshooting data supplied.\"}"; + // env vars are brought in prior to launch args, since args can override them. + if (EnvironmentUtils.TryGetEnvironmentVariable("XL_PLATFORM", out var xlPlatformEnv)) + platformStr = xlPlatformEnv.ToLowerInvariant(); + for (var i = 2; i < args.Count; i++) { if (args[i].StartsWith(key = "--dalamud-working-directory=")) From daeb923f6db51c4e1dbeeddecd02124580f791d1 Mon Sep 17 00:00:00 2001 From: goaaats Date: Mon, 24 Mar 2025 19:16:52 +0100 Subject: [PATCH 24/55] Add missing declaration for utils::is_running_on_wine() --- Dalamud.Boot/utils.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Dalamud.Boot/utils.h b/Dalamud.Boot/utils.h index eef405b26..2cdaf60a7 100644 --- a/Dalamud.Boot/utils.h +++ b/Dalamud.Boot/utils.h @@ -267,6 +267,8 @@ namespace utils { return get_env_list(unicode::convert(pcszName).c_str()); } + bool is_running_on_wine(); + std::filesystem::path get_module_path(HMODULE hModule); /// @brief Find the game main window. From a1305159dc393beff3c0ccd598b4bb3b17e23392 Mon Sep 17 00:00:00 2001 From: goaaats Date: Mon, 24 Mar 2025 19:17:43 +0100 Subject: [PATCH 25/55] Remove unused enumerator object pool from ObjectTable --- .../Game/ClientState/Objects/ObjectTable.cs | 30 +++---------------- 1 file changed, 4 insertions(+), 26 deletions(-) diff --git a/Dalamud/Game/ClientState/Objects/ObjectTable.cs b/Dalamud/Game/ClientState/Objects/ObjectTable.cs index 6b7f5f193..f97385fce 100644 --- a/Dalamud/Game/ClientState/Objects/ObjectTable.cs +++ b/Dalamud/Game/ClientState/Objects/ObjectTable.cs @@ -7,8 +7,6 @@ using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.IoC; using Dalamud.IoC.Internal; -using Dalamud.Logging.Internal; -using Dalamud.Plugin.Internal; using Dalamud.Plugin.Services; using Dalamud.Utility; @@ -31,20 +29,13 @@ namespace Dalamud.Game.ClientState.Objects; #pragma warning restore SA1015 internal sealed partial class ObjectTable : IServiceType, IObjectTable { - private static readonly ModuleLog Log = new("ObjectTable"); - private static int objectTableLength; private readonly ClientState clientState; private readonly CachedEntry[] cachedObjectTable; - private readonly ObjectPool multiThreadedEnumerators = - new DefaultObjectPoolProvider().Create(); - private readonly Enumerator?[] frameworkThreadEnumerators = new Enumerator?[4]; - private long nextMultithreadedUsageWarnTime; - [ServiceManager.ServiceConstructor] private unsafe ObjectTable(ClientState clientState) { @@ -228,21 +219,12 @@ internal sealed partial class ObjectTable /// IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); - private sealed class Enumerator : IEnumerator, IResettable + private sealed class Enumerator(ObjectTable owner, int slotId) : IEnumerator, IResettable { - private readonly int slotId; - private ObjectTable? owner; + private ObjectTable? owner = owner; private int index = -1; - public Enumerator() => this.slotId = -1; - - public Enumerator(ObjectTable owner, int slotId) - { - this.owner = owner; - this.slotId = slotId; - } - public IGameObject Current { get; private set; } = null!; object IEnumerator.Current => this.Current; @@ -265,8 +247,6 @@ internal sealed partial class ObjectTable return false; } - public void InitializeForPooledObjects(ObjectTable ot) => this.owner = ot; - public void Reset() => this.index = -1; public void Dispose() @@ -274,10 +254,8 @@ internal sealed partial class ObjectTable if (this.owner is not { } o) return; - if (this.slotId == -1) - o.multiThreadedEnumerators.Return(this); - else - o.frameworkThreadEnumerators[this.slotId] = this; + if (slotId != -1) + o.frameworkThreadEnumerators[slotId] = this; } public bool TryReset() From 61a4f3bcef100a4376e3976054af01771c69a40a Mon Sep 17 00:00:00 2001 From: goaaats Date: Mon, 24 Mar 2025 19:17:59 +0100 Subject: [PATCH 26/55] Re-remove obsolete ServicePointManager TLS version override --- Dalamud/EntryPoint.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/Dalamud/EntryPoint.cs b/Dalamud/EntryPoint.cs index ebfc975ff..9f9b98cd4 100644 --- a/Dalamud/EntryPoint.cs +++ b/Dalamud/EntryPoint.cs @@ -181,9 +181,6 @@ public sealed class EntryPoint // Apply common fixes for culture issues CultureFixes.Apply(); - - // This is due to GitHub not supporting TLS 1.0, so we enable all TLS versions globally - ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12 | SecurityProtocolType.Tls; // Currently VEH is not fully functional on WINE if (!Util.IsWine()) From fc562778da39c2655961d8b7529dd3dd7984e8e1 Mon Sep 17 00:00:00 2001 From: goaaats Date: Mon, 24 Mar 2025 19:19:44 +0100 Subject: [PATCH 27/55] Rename SMN enums to follow code standards --- .../Game/ClientState/JobGauge/Enums/DeliriumStep.cs | 9 ++++++--- .../ClientState/JobGauge/Enums/SummonAttunement.cs | 11 +++++++---- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/Dalamud/Game/ClientState/JobGauge/Enums/DeliriumStep.cs b/Dalamud/Game/ClientState/JobGauge/Enums/DeliriumStep.cs index cb35a83d2..d2a41b93c 100644 --- a/Dalamud/Game/ClientState/JobGauge/Enums/DeliriumStep.cs +++ b/Dalamud/Game/ClientState/JobGauge/Enums/DeliriumStep.cs @@ -1,19 +1,22 @@ namespace Dalamud.Game.ClientState.JobGauge.Enums; +/// +/// Enum representing the current step of Delirium. +/// public enum DeliriumStep { /// /// Scarlet Delirium can be used. /// - SCARLET_DELIRIUM = 0, + ScarletDelirium = 0, /// /// Comeuppance can be used. /// - COMEUPPANCE = 1, + Comeuppance = 1, /// /// Torcleaver can be used. /// - TORCLEAVER = 2, + Torcleaver = 2, } diff --git a/Dalamud/Game/ClientState/JobGauge/Enums/SummonAttunement.cs b/Dalamud/Game/ClientState/JobGauge/Enums/SummonAttunement.cs index e0b65966c..a07c0d04f 100644 --- a/Dalamud/Game/ClientState/JobGauge/Enums/SummonAttunement.cs +++ b/Dalamud/Game/ClientState/JobGauge/Enums/SummonAttunement.cs @@ -1,27 +1,30 @@ namespace Dalamud.Game.ClientState.JobGauge.Enums; +/// +/// Enum representing the current attunement of a summoner. +/// public enum SummonAttunement { /// /// No attunement. /// - NONE = 0, + None = 0, /// /// Attuned to the summon Ifrit. /// Same as . /// - IFRIT = 1, + Ifrit = 1, /// /// Attuned to the summon Titan. /// Same as . /// - TITAN = 2, + Titan = 2, /// /// Attuned to the summon Garuda. /// Same as . /// - GARUDA = 3, + Garuda = 3, } From c0c61c66bece36e3df02af5e788d3bda88a3296b Mon Sep 17 00:00:00 2001 From: goaaats Date: Mon, 24 Mar 2025 19:21:04 +0100 Subject: [PATCH 28/55] Fix warnings --- Dalamud/Game/ClientState/JobGauge/Types/SMNGauge.cs | 9 ++++++--- .../Text/Evaluator/Internal/SheetRedirectResolver.cs | 3 ++- Dalamud/Utility/ItemUtil.cs | 8 +++----- Dalamud/Utility/Util.cs | 2 -- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Dalamud/Game/ClientState/JobGauge/Types/SMNGauge.cs b/Dalamud/Game/ClientState/JobGauge/Types/SMNGauge.cs index 4c27ae83a..899ea78eb 100644 --- a/Dalamud/Game/ClientState/JobGauge/Types/SMNGauge.cs +++ b/Dalamud/Game/ClientState/JobGauge/Types/SMNGauge.cs @@ -22,6 +22,9 @@ public unsafe class SMNGauge : JobGaugeBase /// public ushort SummonTimerRemaining => this.Struct->SummonTimer; + /// + /// Gets the time remaining for the current attunement. + /// [Obsolete("Typo fixed. Use AttunementTimerRemaining instead.", true)] public ushort AttunmentTimerRemaining => this.AttunementTimerRemaining; @@ -102,19 +105,19 @@ public unsafe class SMNGauge : JobGaugeBase /// Gets a value indicating whether if Ifrit is currently attuned. /// /// true or false. - public bool IsIfritAttuned => this.AttunementType == SummonAttunement.IFRIT; + public bool IsIfritAttuned => this.AttunementType == SummonAttunement.Ifrit; /// /// Gets a value indicating whether if Titan is currently attuned. /// /// true or false. - public bool IsTitanAttuned => this.AttunementType == SummonAttunement.TITAN; + public bool IsTitanAttuned => this.AttunementType == SummonAttunement.Titan; /// /// Gets a value indicating whether if Garuda is currently attuned. /// /// true or false. - public bool IsGarudaAttuned => this.AttunementType == SummonAttunement.GARUDA; + public bool IsGarudaAttuned => this.AttunementType == SummonAttunement.Garuda; /// /// Gets a value indicating whether there are any Aetherflow stacks available. diff --git a/Dalamud/Game/Text/Evaluator/Internal/SheetRedirectResolver.cs b/Dalamud/Game/Text/Evaluator/Internal/SheetRedirectResolver.cs index 57a58c80d..49af895d9 100644 --- a/Dalamud/Game/Text/Evaluator/Internal/SheetRedirectResolver.cs +++ b/Dalamud/Game/Text/Evaluator/Internal/SheetRedirectResolver.cs @@ -85,8 +85,9 @@ internal class SheetRedirectResolver : IServiceType { flags |= SheetRedirectFlags.HighQuality; } - else if (kind == ItemKind.Collectible || sheetName == "ItemMP") // MP for Masterpiece?! + else if (kind == ItemKind.Collectible || sheetName == "ItemMP") { + // MP for Masterpiece?! flags |= SheetRedirectFlags.Collectible; } diff --git a/Dalamud/Utility/ItemUtil.cs b/Dalamud/Utility/ItemUtil.cs index de1e5a721..32160aa15 100644 --- a/Dalamud/Utility/ItemUtil.cs +++ b/Dalamud/Utility/ItemUtil.cs @@ -2,14 +2,12 @@ using System.Runtime.CompilerServices; using Dalamud.Data; using Dalamud.Game; -using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Game.Text; - using Lumina.Excel.Sheets; +using Lumina.Text; using Lumina.Text.ReadOnly; using static Dalamud.Game.Text.SeStringHandling.Payloads.ItemPayload; -using Lumina.Text; namespace Dalamud.Utility; @@ -128,10 +126,10 @@ internal static class ItemUtil switch (kind) { - case ItemPayload.ItemKind.Hq: + case ItemKind.Hq: builder.Append($" {(char)SeIconChar.HighQuality}"); break; - case ItemPayload.ItemKind.Collectible: + case ItemKind.Collectible: builder.Append($" {(char)SeIconChar.Collectible}"); break; } diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs index 2fcc4806a..87cb86e1c 100644 --- a/Dalamud/Utility/Util.cs +++ b/Dalamud/Utility/Util.cs @@ -12,14 +12,12 @@ using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; -using Dalamud.Configuration.Internal; using Dalamud.Data; using Dalamud.Game; using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Interface.Colors; using Dalamud.Interface.Utility; -using Dalamud.Logging.Internal; using Dalamud.Interface.Utility.Raii; using Dalamud.Support; using ImGuiNET; From 84f5dad0a3010b634bdafed99f9ecf7d90378737 Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Mon, 24 Mar 2025 11:31:19 -0700 Subject: [PATCH 29/55] fix: JobGauge enum casing --- .../ClientState/JobGauge/Enums/BeastChakra.cs | 8 +++---- .../ClientState/JobGauge/Enums/CardType.cs | 18 +++++++------- .../JobGauge/Enums/DismissedFairy.cs | 4 ++-- .../ClientState/JobGauge/Enums/DrawType.cs | 4 ++-- .../Game/ClientState/JobGauge/Enums/Kaeshi.cs | 10 ++++---- .../Game/ClientState/JobGauge/Enums/Mudras.cs | 6 ++--- .../Game/ClientState/JobGauge/Enums/Nadi.cs | 6 ++--- .../ClientState/JobGauge/Enums/PetGlam.cs | 16 ++++++------- .../Game/ClientState/JobGauge/Enums/Sen.cs | 8 +++---- .../JobGauge/Enums/SerpentCombo.cs | 14 +++++------ .../Game/ClientState/JobGauge/Enums/Song.cs | 8 +++---- .../ClientState/JobGauge/Enums/SummonPet.cs | 4 ++-- .../ClientState/JobGauge/Types/BRDGauge.cs | 24 +++++++++---------- .../ClientState/JobGauge/Types/MNKGauge.cs | 2 +- .../ClientState/JobGauge/Types/SAMGauge.cs | 6 ++--- 15 files changed, 69 insertions(+), 69 deletions(-) diff --git a/Dalamud/Game/ClientState/JobGauge/Enums/BeastChakra.cs b/Dalamud/Game/ClientState/JobGauge/Enums/BeastChakra.cs index bbe4ad70d..9191ca020 100644 --- a/Dalamud/Game/ClientState/JobGauge/Enums/BeastChakra.cs +++ b/Dalamud/Game/ClientState/JobGauge/Enums/BeastChakra.cs @@ -8,20 +8,20 @@ public enum BeastChakra : byte /// /// No chakra. /// - NONE = 0, + None = 0, /// /// The Opo-Opo chakra. /// - OPOOPO = 1, + OpoOpo = 1, /// /// The Raptor chakra. /// - RAPTOR = 2, + Raptor = 2, /// /// The Coeurl chakra. /// - COEURL = 3, + Coeurl = 3, } diff --git a/Dalamud/Game/ClientState/JobGauge/Enums/CardType.cs b/Dalamud/Game/ClientState/JobGauge/Enums/CardType.cs index 24ffc2b19..89fceed96 100644 --- a/Dalamud/Game/ClientState/JobGauge/Enums/CardType.cs +++ b/Dalamud/Game/ClientState/JobGauge/Enums/CardType.cs @@ -8,45 +8,45 @@ public enum CardType : byte /// /// No card. /// - NONE = 0, + None = 0, /// /// The Balance card. /// - BALANCE = 1, + Balance = 1, /// /// The Bole card. /// - BOLE = 2, + Bole = 2, /// /// The Arrow card. /// - ARROW = 3, + Arrow = 3, /// /// The Spear card. /// - SPEAR = 4, + Spear = 4, /// /// The Ewer card. /// - EWER = 5, + Ewer = 5, /// /// The Spire card. /// - SPIRE = 6, + Spire = 6, /// /// The Lord of Crowns card. /// - LORD = 7, + Lord = 7, /// /// The Lady of Crowns card. /// - LADY = 8, + Lady = 8, } diff --git a/Dalamud/Game/ClientState/JobGauge/Enums/DismissedFairy.cs b/Dalamud/Game/ClientState/JobGauge/Enums/DismissedFairy.cs index b674d11b8..446489bd1 100644 --- a/Dalamud/Game/ClientState/JobGauge/Enums/DismissedFairy.cs +++ b/Dalamud/Game/ClientState/JobGauge/Enums/DismissedFairy.cs @@ -8,10 +8,10 @@ public enum DismissedFairy : byte /// /// Dismissed fairy is Eos. /// - EOS = 6, + Eos = 6, /// /// Dismissed fairy is Selene. /// - SELENE = 7, + Selene = 7, } diff --git a/Dalamud/Game/ClientState/JobGauge/Enums/DrawType.cs b/Dalamud/Game/ClientState/JobGauge/Enums/DrawType.cs index f8833d6d0..619059daa 100644 --- a/Dalamud/Game/ClientState/JobGauge/Enums/DrawType.cs +++ b/Dalamud/Game/ClientState/JobGauge/Enums/DrawType.cs @@ -8,10 +8,10 @@ public enum DrawType : byte /// /// Astral Draw active. /// - ASTRAL = 0, + Astral = 0, /// /// Umbral Draw active. /// - UMBRAL = 1, + Umbral = 1, } diff --git a/Dalamud/Game/ClientState/JobGauge/Enums/Kaeshi.cs b/Dalamud/Game/ClientState/JobGauge/Enums/Kaeshi.cs index e35dcc7f9..86e58771b 100644 --- a/Dalamud/Game/ClientState/JobGauge/Enums/Kaeshi.cs +++ b/Dalamud/Game/ClientState/JobGauge/Enums/Kaeshi.cs @@ -8,25 +8,25 @@ public enum Kaeshi : byte /// /// No Kaeshi is active. /// - NONE = 0, + None = 0, /// /// Kaeshi: Higanbana type. /// - HIGANBANA = 1, + Higanbana = 1, /// /// Kaeshi: Goken type. /// - GOKEN = 2, + Goken = 2, /// /// Kaeshi: Setsugekka type. /// - SETSUGEKKA = 3, + Setsugekka = 3, /// /// Kaeshi: Namikiri type. /// - NAMIKIRI = 4, + Namikiri = 4, } diff --git a/Dalamud/Game/ClientState/JobGauge/Enums/Mudras.cs b/Dalamud/Game/ClientState/JobGauge/Enums/Mudras.cs index 31ee6dac1..8955c0735 100644 --- a/Dalamud/Game/ClientState/JobGauge/Enums/Mudras.cs +++ b/Dalamud/Game/ClientState/JobGauge/Enums/Mudras.cs @@ -8,15 +8,15 @@ public enum Mudras : byte /// /// Ten mudra. /// - TEN = 1, + Ten = 1, /// /// Chi mudra. /// - CHI = 2, + Chi = 2, /// /// Jin mudra. /// - JIN = 3, + Jin = 3, } diff --git a/Dalamud/Game/ClientState/JobGauge/Enums/Nadi.cs b/Dalamud/Game/ClientState/JobGauge/Enums/Nadi.cs index 8be5c739e..f61e54104 100644 --- a/Dalamud/Game/ClientState/JobGauge/Enums/Nadi.cs +++ b/Dalamud/Game/ClientState/JobGauge/Enums/Nadi.cs @@ -9,15 +9,15 @@ public enum Nadi : byte /// /// No nadi. /// - NONE = 0, + None = 0, /// /// The Lunar nadi. /// - LUNAR = 1, + Lunar = 1, /// /// The Solar nadi. /// - SOLAR = 2, + Solar = 2, } diff --git a/Dalamud/Game/ClientState/JobGauge/Enums/PetGlam.cs b/Dalamud/Game/ClientState/JobGauge/Enums/PetGlam.cs index 248caa396..c10c369d3 100644 --- a/Dalamud/Game/ClientState/JobGauge/Enums/PetGlam.cs +++ b/Dalamud/Game/ClientState/JobGauge/Enums/PetGlam.cs @@ -8,40 +8,40 @@ public enum PetGlam : byte /// /// No pet glam. /// - NONE = 0, + None = 0, /// /// Emerald carbuncle pet glam. /// - EMERALD = 1, + Emerald = 1, /// /// Topaz carbuncle pet glam. /// - TOPAZ = 2, + Topaz = 2, /// /// Ruby carbuncle pet glam. /// - RUBY = 3, + Ruby = 3, /// /// Normal carbuncle pet glam. /// - CARBUNCLE = 4, + Carbuncle = 4, /// /// Ifrit Egi pet glam. /// - IFRIT = 5, + Ifrit = 5, /// /// Titan Egi pet glam. /// - TITAN = 6, + Titan = 6, /// /// Garuda Egi pet glam. /// - GARUDA = 7, + Garuda = 7, } diff --git a/Dalamud/Game/ClientState/JobGauge/Enums/Sen.cs b/Dalamud/Game/ClientState/JobGauge/Enums/Sen.cs index a1a6035c6..0eb86b1b1 100644 --- a/Dalamud/Game/ClientState/JobGauge/Enums/Sen.cs +++ b/Dalamud/Game/ClientState/JobGauge/Enums/Sen.cs @@ -9,20 +9,20 @@ public enum Sen : byte /// /// No Sen. /// - NONE = 0, + None = 0, /// /// Setsu Sen type. /// - SETSU = 1 << 0, + Setsu = 1 << 0, /// /// Getsu Sen type. /// - GETSU = 1 << 1, + Getsu = 1 << 1, /// /// Ka Sen type. /// - KA = 1 << 2, + Ka = 1 << 2, } diff --git a/Dalamud/Game/ClientState/JobGauge/Enums/SerpentCombo.cs b/Dalamud/Game/ClientState/JobGauge/Enums/SerpentCombo.cs index 0fc50d87a..f4850cf8c 100644 --- a/Dalamud/Game/ClientState/JobGauge/Enums/SerpentCombo.cs +++ b/Dalamud/Game/ClientState/JobGauge/Enums/SerpentCombo.cs @@ -8,35 +8,35 @@ public enum SerpentCombo : byte /// /// No Serpent combo is active. /// - NONE = 0, + None = 0, /// /// Death Rattle action. /// - DEATHRATTLE = 1, + DeathRattle = 1, /// /// Last Lash action. /// - LASTLASH = 2, + LastLash = 2, /// /// First Legacy action. /// - FIRSTLEGACY = 3, + FirstLegacy = 3, /// /// Second Legacy action. /// - SECONDLEGACY = 4, + SecondLegacy = 4, /// /// Third Legacy action. /// - THIRDLEGACY = 5, + ThirdLegacy = 5, /// /// Fourth Legacy action. /// - FOURTHLEGACY = 6, + FourthLegacy = 6, } diff --git a/Dalamud/Game/ClientState/JobGauge/Enums/Song.cs b/Dalamud/Game/ClientState/JobGauge/Enums/Song.cs index c23fbbbff..7ca6b6c07 100644 --- a/Dalamud/Game/ClientState/JobGauge/Enums/Song.cs +++ b/Dalamud/Game/ClientState/JobGauge/Enums/Song.cs @@ -8,20 +8,20 @@ public enum Song : byte /// /// No song is active type. /// - NONE = 0, + None = 0, /// /// Mage's Ballad type. /// - MAGE = 1, + Mage = 1, /// /// Army's Paeon type. /// - ARMY = 2, + Army = 2, /// /// The Wanderer's Minuet type. /// - WANDERER = 3, + Wanderer = 3, } diff --git a/Dalamud/Game/ClientState/JobGauge/Enums/SummonPet.cs b/Dalamud/Game/ClientState/JobGauge/Enums/SummonPet.cs index 30cc0fff0..37569f4bf 100644 --- a/Dalamud/Game/ClientState/JobGauge/Enums/SummonPet.cs +++ b/Dalamud/Game/ClientState/JobGauge/Enums/SummonPet.cs @@ -8,10 +8,10 @@ public enum SummonPet : byte /// /// No pet. /// - NONE = 0, + None = 0, /// /// The summoned pet Carbuncle. /// - CARBUNCLE = 23, + Carbuncle = 23, } diff --git a/Dalamud/Game/ClientState/JobGauge/Types/BRDGauge.cs b/Dalamud/Game/ClientState/JobGauge/Types/BRDGauge.cs index bfcf3cc38..8880c3555 100644 --- a/Dalamud/Game/ClientState/JobGauge/Types/BRDGauge.cs +++ b/Dalamud/Game/ClientState/JobGauge/Types/BRDGauge.cs @@ -40,15 +40,15 @@ public unsafe class BRDGauge : JobGaugeBaseSongFlags.HasFlag(SongFlags.WanderersMinuet)) - return Song.WANDERER; + return Song.Wanderer; if (this.Struct->SongFlags.HasFlag(SongFlags.ArmysPaeon)) - return Song.ARMY; + return Song.Army; if (this.Struct->SongFlags.HasFlag(SongFlags.MagesBallad)) - return Song.MAGE; + return Song.Mage; - return Song.NONE; + return Song.None; } } @@ -60,15 +60,15 @@ public unsafe class BRDGauge : JobGaugeBaseSongFlags.HasFlag(SongFlags.WanderersMinuetLastPlayed)) - return Song.WANDERER; + return Song.Wanderer; if (this.Struct->SongFlags.HasFlag(SongFlags.ArmysPaeonLastPlayed)) - return Song.ARMY; + return Song.Army; if (this.Struct->SongFlags.HasFlag(SongFlags.MagesBalladLastPlayed)) - return Song.MAGE; + return Song.Mage; - return Song.NONE; + return Song.None; } } @@ -76,7 +76,7 @@ public unsafe class BRDGauge : JobGaugeBase /// - /// This will always return an array of size 3, inactive Coda are represented by . + /// This will always return an array of size 3, inactive Coda are represented by . /// public Song[] Coda { @@ -84,9 +84,9 @@ public unsafe class BRDGauge : JobGaugeBaseSongFlags.HasFlag(SongFlags.MagesBalladCoda) ? Song.MAGE : Song.NONE, - this.Struct->SongFlags.HasFlag(SongFlags.ArmysPaeonCoda) ? Song.ARMY : Song.NONE, - this.Struct->SongFlags.HasFlag(SongFlags.WanderersMinuetCoda) ? Song.WANDERER : Song.NONE, + this.Struct->SongFlags.HasFlag(SongFlags.MagesBalladCoda) ? Song.Mage : Song.None, + this.Struct->SongFlags.HasFlag(SongFlags.ArmysPaeonCoda) ? Song.Army : Song.None, + this.Struct->SongFlags.HasFlag(SongFlags.WanderersMinuetCoda) ? Song.Wanderer : Song.None, }; } } diff --git a/Dalamud/Game/ClientState/JobGauge/Types/MNKGauge.cs b/Dalamud/Game/ClientState/JobGauge/Types/MNKGauge.cs index 803740f33..31a5ceb9b 100644 --- a/Dalamud/Game/ClientState/JobGauge/Types/MNKGauge.cs +++ b/Dalamud/Game/ClientState/JobGauge/Types/MNKGauge.cs @@ -27,7 +27,7 @@ public unsafe class MNKGauge : JobGaugeBase /// - /// This will always return an array of size 3, inactive Beast Chakra are represented by . + /// This will always return an array of size 3, inactive Beast Chakra are represented by . /// public BeastChakra[] BeastChakra => this.Struct->BeastChakra.Select(c => (BeastChakra)c).ToArray(); diff --git a/Dalamud/Game/ClientState/JobGauge/Types/SAMGauge.cs b/Dalamud/Game/ClientState/JobGauge/Types/SAMGauge.cs index f3417f002..52dc0e51a 100644 --- a/Dalamud/Game/ClientState/JobGauge/Types/SAMGauge.cs +++ b/Dalamud/Game/ClientState/JobGauge/Types/SAMGauge.cs @@ -40,17 +40,17 @@ public unsafe class SAMGauge : JobGaugeBase /// true or false. - public bool HasSetsu => (this.Sen & Sen.SETSU) != 0; + public bool HasSetsu => (this.Sen & Sen.Setsu) != 0; /// /// Gets a value indicating whether the Getsu Sen is active. /// /// true or false. - public bool HasGetsu => (this.Sen & Sen.GETSU) != 0; + public bool HasGetsu => (this.Sen & Sen.Getsu) != 0; /// /// Gets a value indicating whether the Ka Sen is active. /// /// true or false. - public bool HasKa => (this.Sen & Sen.KA) != 0; + public bool HasKa => (this.Sen & Sen.Ka) != 0; } From f209ac087af3dee6d9e5b50efadfb3c9e0813ba5 Mon Sep 17 00:00:00 2001 From: goaaats Date: Mon, 24 Mar 2025 20:23:56 +0100 Subject: [PATCH 30/55] SeStringEvaluator: Throw if not on main thread --- Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs | 6 +----- Dalamud/Utility/ThreadSafety.cs | 5 +++-- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs b/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs index 723dbcb41..eb6dee290 100644 --- a/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs +++ b/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs @@ -1682,11 +1682,7 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator if (rtm is null) return false; - if (!ThreadSafety.IsMainThread) - { - Log.Error("Global parameters may only be used from the main thread."); - return false; - } + ThreadSafety.AssertMainThread("Global parameters may only be used from the main thread."); ref var gp = ref rtm->TextModule.MacroDecoder.GlobalParameters; if (parameterIndex >= gp.MySize) diff --git a/Dalamud/Utility/ThreadSafety.cs b/Dalamud/Utility/ThreadSafety.cs index ea8238d44..c31cc0005 100644 --- a/Dalamud/Utility/ThreadSafety.cs +++ b/Dalamud/Utility/ThreadSafety.cs @@ -18,13 +18,14 @@ public static class ThreadSafety /// /// Throws an exception when the current thread is not the main thread. /// + /// The message to be passed into the exception, if one is to be thrown. /// Thrown when the current thread is not the main thread. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void AssertMainThread() + public static void AssertMainThread(string? message = null) { if (!threadStaticIsMainThread) { - throw new InvalidOperationException("Not on main thread!"); + throw new InvalidOperationException(message ?? "Not on main thread!"); } } From d1e6e34f4036f9f8aff93d17f3047c6cc7b0364f Mon Sep 17 00:00:00 2001 From: goaaats Date: Mon, 24 Mar 2025 20:44:57 +0100 Subject: [PATCH 31/55] Bump to version 12 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index c26573fbd..51fc46ebe 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -6,7 +6,7 @@ XIV Launcher addon framework - 11.0.8.0 + 12.0.0.0 $(DalamudVersion) $(DalamudVersion) $(DalamudVersion) From 9e3c03d0e8e7ced5c53fae68f35969f30668a5f9 Mon Sep 17 00:00:00 2001 From: goaaats Date: Mon, 24 Mar 2025 20:45:16 +0100 Subject: [PATCH 32/55] Add deprecation warning to targets file --- targets/Dalamud.Plugin.targets | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/targets/Dalamud.Plugin.targets b/targets/Dalamud.Plugin.targets index da897c252..08d19735e 100644 --- a/targets/Dalamud.Plugin.targets +++ b/targets/Dalamud.Plugin.targets @@ -29,9 +29,7 @@ - From 2176b32219a68b744df9d488ae1ff8b5723f5dd3 Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Mon, 24 Mar 2025 13:25:13 -0700 Subject: [PATCH 33/55] chore: Bump ClientStructs and make it build again --- Dalamud/Dalamud.csproj | 1 + .../Game/Addon/Events/AddonEventListener.cs | 14 ++--- Dalamud/Game/ClientState/Statuses/Status.cs | 4 +- Dalamud/Game/Config/GameConfigSection.cs | 12 ++-- Dalamud/Game/Gui/Dtr/DtrBar.cs | 18 +++--- Dalamud/Game/Inventory/GameInventoryItem.cs | 10 ++- .../Game/Network/Internal/NetworkHandlers.cs | 8 +-- .../Game/Text/Evaluator/SeStringEvaluator.cs | 2 +- Dalamud/Interface/Internal/UiDebug.cs | 14 ++--- .../UiDebug2/Browsing/AddonTree.AtkValues.cs | 5 +- .../UiDebug2/Browsing/NodeTree.Text.cs | 2 +- .../Data/Widgets/SeStringCreatorWidget.cs | 6 +- .../Internal/Windows/TitleScreenMenuWindow.cs | 4 +- .../TextureManager.FromExistingTexture.cs | 63 +++++++++---------- Dalamud/Utility/SeStringExtensions.cs | 7 +++ lib/FFXIVClientStructs | 2 +- 16 files changed, 92 insertions(+), 80 deletions(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 51fc46ebe..9f3a9bb4a 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -90,6 +90,7 @@ + diff --git a/Dalamud/Game/Addon/Events/AddonEventListener.cs b/Dalamud/Game/Addon/Events/AddonEventListener.cs index cc6416fe8..515785d72 100644 --- a/Dalamud/Game/Addon/Events/AddonEventListener.cs +++ b/Dalamud/Game/Addon/Events/AddonEventListener.cs @@ -11,9 +11,9 @@ namespace Dalamud.Game.Addon.Events; internal unsafe class AddonEventListener : IDisposable { private ReceiveEventDelegate? receiveEventDelegate; - + private AtkEventListener* eventListener; - + /// /// Initializes a new instance of the class. /// @@ -24,7 +24,7 @@ internal unsafe class AddonEventListener : IDisposable this.eventListener = (AtkEventListener*)Marshal.AllocHGlobal(sizeof(AtkEventListener)); this.eventListener->VirtualTable = (AtkEventListener.AtkEventListenerVirtualTable*)Marshal.AllocHGlobal(sizeof(void*) * 3); - this.eventListener->VirtualTable->Dtor = (delegate* unmanaged)(delegate* unmanaged)&NullSub; + this.eventListener->VirtualTable->Dtor = (delegate* unmanaged)(delegate* unmanaged)&NullSub; this.eventListener->VirtualTable->ReceiveGlobalEvent = (delegate* unmanaged)(delegate* unmanaged)&NullSub; this.eventListener->VirtualTable->ReceiveEvent = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.receiveEventDelegate); } @@ -38,17 +38,17 @@ internal unsafe class AddonEventListener : IDisposable /// Pointer to the AtkEvent. /// Pointer to the AtkEventData. public delegate void ReceiveEventDelegate(AtkEventListener* self, AtkEventType eventType, uint eventParam, AtkEvent* eventPtr, AtkEventData* eventDataPtr); - + /// /// Gets the address of this listener. /// public nint Address => (nint)this.eventListener; - + /// public void Dispose() { if (this.eventListener is null) return; - + Marshal.FreeHGlobal((nint)this.eventListener->VirtualTable); Marshal.FreeHGlobal((nint)this.eventListener); @@ -88,7 +88,7 @@ internal unsafe class AddonEventListener : IDisposable node->RemoveEvent(eventType, param, this.eventListener, false); }); } - + [UnmanagedCallersOnly] private static void NullSub() { diff --git a/Dalamud/Game/ClientState/Statuses/Status.cs b/Dalamud/Game/ClientState/Statuses/Status.cs index f09d13fb3..c3493ce55 100644 --- a/Dalamud/Game/ClientState/Statuses/Status.cs +++ b/Dalamud/Game/ClientState/Statuses/Status.cs @@ -42,8 +42,10 @@ public unsafe class Status /// /// Gets the stack count of this status. + /// Only valid if this is a non-food status. /// - public byte StackCount => this.Struct->StackCount; + [Obsolete($"Replaced with {nameof(Param)}", true)] + public byte StackCount => (byte)this.Struct->Param; /// /// Gets the time remaining of this status. diff --git a/Dalamud/Game/Config/GameConfigSection.cs b/Dalamud/Game/Config/GameConfigSection.cs index 9cd239d84..8ebab8a60 100644 --- a/Dalamud/Game/Config/GameConfigSection.cs +++ b/Dalamud/Game/Config/GameConfigSection.cs @@ -52,7 +52,7 @@ public class GameConfigSection /// /// Event which is fired when a game config option is changed within the section. /// - internal event EventHandler? Changed; + internal event EventHandler? Changed; /// /// Gets the number of config entries contained within the section. @@ -526,8 +526,8 @@ public class GameConfigSection { if (!this.enumMap.TryGetValue(entry->Index, out var enumObject)) { - if (entry->Name == null) return null; - var name = MemoryHelper.ReadStringNullTerminated(new IntPtr(entry->Name)); + if (entry->Name.Value == null) return null; + var name = entry->Name.ToString(); if (Enum.TryParse(typeof(TEnum), name, out enumObject)) { this.enumMap.TryAdd(entry->Index, enumObject); @@ -544,7 +544,7 @@ public class GameConfigSection this.Changed?.InvokeSafely(this, eventArgs); return eventArgs; } - + private unsafe bool TryGetIndex(string name, out uint index) { if (this.indexMap.TryGetValue(name, out index)) @@ -556,12 +556,12 @@ public class GameConfigSection var e = configBase->ConfigEntry; for (var i = 0U; i < configBase->ConfigCount; i++, e++) { - if (e->Name == null) + if (e->Name.Value == null) { continue; } - var eName = MemoryHelper.ReadStringNullTerminated(new IntPtr(e->Name)); + var eName = e->Name.ToString(); if (eName.Equals(name)) { this.indexMap.TryAdd(name, i); diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs index f37b3addc..c6208fb2f 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBar.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs @@ -30,7 +30,7 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar private const uint BaseNodeId = 1000; private static readonly ModuleLog Log = new("DtrBar"); - + [ServiceManager.ServiceDependency] private readonly Framework framework = Service.Get(); @@ -58,7 +58,7 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar private ImmutableList? entriesReadOnlyCopy; private Utf8String* emptyString; - + private uint runningNodeIds = BaseNodeId; private float entryStartPos = float.NaN; @@ -72,7 +72,7 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar this.addonLifecycle.RegisterListener(this.dtrPostDrawListener); this.addonLifecycle.RegisterListener(this.dtrPostRequestedUpdateListener); this.addonLifecycle.RegisterListener(this.dtrPreFinalizeListener); - + this.framework.Update += this.Update; this.configuration.DtrOrder ??= []; @@ -522,7 +522,7 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar this.uiEventManager.AddEvent(AddonEventManager.DalamudInternalKey, (nint)dtr, (nint)node, AddonEventType.MouseOut, this.DtrEventHandler), this.uiEventManager.AddEvent(AddonEventManager.DalamudInternalKey, (nint)dtr, (nint)node, AddonEventType.MouseClick, this.DtrEventHandler), }); - + var lastChild = dtr->RootNode->ChildNode; while (lastChild->PrevSiblingNode != null) lastChild = lastChild->PrevSiblingNode; Log.Debug($"Found last sibling: {(ulong)lastChild:X}"); @@ -590,7 +590,7 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar if (this.emptyString == null) this.emptyString = Utf8String.FromString(" "); - + newTextNode->SetText(this.emptyString->StringPtr); newTextNode->TextColor = new ByteColor { R = 255, G = 255, B = 255, A = 255 }; @@ -609,7 +609,7 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar return newTextNode; } - + private void DtrEventHandler(AddonEventType atkEventType, IntPtr atkUnitBase, IntPtr atkResNode) { var addon = (AtkUnitBase*)atkUnitBase; @@ -632,7 +632,7 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar case AddonEventType.MouseOver: AtkStage.Instance()->TooltipManager.ShowTooltip(addon->Id, node, dtrBarEntry.Tooltip.Encode()); break; - + case AddonEventType.MouseOut: AtkStage.Instance()->TooltipManager.HideTooltip(addon->Id); break; @@ -646,11 +646,11 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar case AddonEventType.MouseOver: this.uiEventManager.SetCursor(AddonCursorType.Clickable); break; - + case AddonEventType.MouseOut: this.uiEventManager.ResetCursor(); break; - + case AddonEventType.MouseClick: dtrBarEntry.OnClick.Invoke(); break; diff --git a/Dalamud/Game/Inventory/GameInventoryItem.cs b/Dalamud/Game/Inventory/GameInventoryItem.cs index a9b178411..32eb9911b 100644 --- a/Dalamud/Game/Inventory/GameInventoryItem.cs +++ b/Dalamud/Game/Inventory/GameInventoryItem.cs @@ -17,7 +17,7 @@ public unsafe struct GameInventoryItem : IEquatable /// [FieldOffset(0)] internal readonly InventoryItem InternalItem; - + /// /// The view of the backing data, in . /// @@ -55,10 +55,16 @@ public unsafe struct GameInventoryItem : IEquatable /// public int Quantity => this.InternalItem.Quantity; + /// + /// Gets the spiritbond or collectability of this item. + /// + public uint SpiritbondOrCollectability => this.InternalItem.SpiritbondOrCollectability; + /// /// Gets the spiritbond of this item. /// - public uint Spiritbond => this.InternalItem.Spiritbond; + [Obsolete($"Renamed to {nameof(SpiritbondOrCollectability)}", true)] + public uint Spiritbond => this.SpiritbondOrCollectability; /// /// Gets the repair condition of this item. diff --git a/Dalamud/Game/Network/Internal/NetworkHandlers.cs b/Dalamud/Game/Network/Internal/NetworkHandlers.cs index 2ba7f2587..c0929fa84 100644 --- a/Dalamud/Game/Network/Internal/NetworkHandlers.cs +++ b/Dalamud/Game/Network/Internal/NetworkHandlers.cs @@ -565,7 +565,7 @@ internal unsafe class NetworkHandlers : IInternalDisposableService return this.configuration.IsMbCollect; } - private void MarketPurchasePacketDetour(PacketDispatcher* a1, nint packetData) + private void MarketPurchasePacketDetour(uint targetId, nint packetData) { try { @@ -576,7 +576,7 @@ internal unsafe class NetworkHandlers : IInternalDisposableService Log.Error(ex, "MarketPurchasePacketHandler threw an exception"); } - this.mbPurchaseHook.OriginalDisposeSafe(a1, packetData); + this.mbPurchaseHook.OriginalDisposeSafe(targetId, packetData); } private void MarketHistoryPacketDetour(InfoProxyItemSearch* a1, nint packetData) @@ -609,7 +609,7 @@ internal unsafe class NetworkHandlers : IInternalDisposableService this.customTalkHook.OriginalDisposeSafe(a1, eventId, responseId, args, argCount); } - private void MarketItemRequestStartDetour(PacketDispatcher* a1, nint packetRef) + private void MarketItemRequestStartDetour(uint targetId, nint packetRef) { try { @@ -620,7 +620,7 @@ internal unsafe class NetworkHandlers : IInternalDisposableService Log.Error(ex, "MarketItemRequestStartDetour threw an exception"); } - this.mbItemRequestStartHook.OriginalDisposeSafe(a1, packetRef); + this.mbItemRequestStartHook.OriginalDisposeSafe(targetId, packetRef); } private void MarketBoardOfferingsDetour(InfoProxyItemSearch* a1, nint packetRef) diff --git a/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs b/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs index eb6dee290..47c4b5899 100644 --- a/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs +++ b/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs @@ -1744,7 +1744,7 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator return false; case TextParameterType.String: - this.EvaluateAndAppendTo(builder, new(p.StringValue), null, language); + this.EvaluateAndAppendTo(builder, p.StringValue.AsReadOnlySeStringSpan(), null, language); return false; case TextParameterType.Uninitialized: diff --git a/Dalamud/Interface/Internal/UiDebug.cs b/Dalamud/Interface/Internal/UiDebug.cs index 2e8f4416b..9dfff75ec 100644 --- a/Dalamud/Interface/Internal/UiDebug.cs +++ b/Dalamud/Interface/Internal/UiDebug.cs @@ -225,7 +225,7 @@ internal unsafe class UiDebug ImGui.SameLine(); if (ImGui.Button($"Decode##{(ulong)textNode:X}")) - textNode->NodeText.SetString(new ReadOnlySeStringSpan(textNode->NodeText.StringPtr).ToString()); + textNode->NodeText.SetString(textNode->NodeText.StringPtr.AsReadOnlySeStringSpan().ToString()); ImGui.Text($"AlignmentType: {(AlignmentType)textNode->AlignmentFontType} FontSize: {textNode->FontSize}"); int b = textNode->AlignmentFontType; @@ -418,27 +418,27 @@ internal unsafe class UiDebug ImGui.Text("InputBase Text1: "); ImGui.SameLine(); Service.Get().Draw(textInputComponent->AtkComponentInputBase.UnkText1); - + ImGui.Text("InputBase Text2: "); ImGui.SameLine(); Service.Get().Draw(textInputComponent->AtkComponentInputBase.UnkText2); - + ImGui.Text("Text1: "); ImGui.SameLine(); Service.Get().Draw(textInputComponent->UnkText01); - + ImGui.Text("Text2: "); ImGui.SameLine(); Service.Get().Draw(textInputComponent->UnkText02); - + ImGui.Text("Text3: "); ImGui.SameLine(); Service.Get().Draw(textInputComponent->UnkText03); - + ImGui.Text("Text4: "); ImGui.SameLine(); Service.Get().Draw(textInputComponent->UnkText04); - + ImGui.Text("Text5: "); ImGui.SameLine(); Service.Get().Draw(textInputComponent->UnkText05); diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/AddonTree.AtkValues.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/AddonTree.AtkValues.cs index c3930821b..c3f6133dd 100644 --- a/Dalamud/Interface/Internal/UiDebug2/Browsing/AddonTree.AtkValues.cs +++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/AddonTree.AtkValues.cs @@ -76,14 +76,13 @@ public unsafe partial class AddonTree case ValueType.String8: case ValueType.String: { - if (atkValue->String == null) + if (atkValue->String.Value == null) { ImGui.TextDisabled("null"); } else { - var str = MemoryHelper.ReadSeStringNullTerminated(new nint(atkValue->String)); - Util.ShowStruct(str, (ulong)atkValue); + Util.ShowStruct(atkValue->String.ToString(), (ulong)atkValue); } break; diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Text.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Text.cs index 61e0e79b8..02bd5feca 100644 --- a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Text.cs +++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Text.cs @@ -89,7 +89,7 @@ internal unsafe partial class TextNodeTree : ResNodeTree var seStringBytes = new byte[utf8String.BufUsed]; for (var i = 0L; i < utf8String.BufUsed; i++) { - seStringBytes[i] = utf8String.StringPtr[i]; + seStringBytes[i] = utf8String.StringPtr.Value[i]; } var seString = SeString.Parse(seStringBytes); diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs index 2a56cb6c7..92e57ddac 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs @@ -187,7 +187,7 @@ internal class SeStringCreatorWidget : IDataWindowWidget // resize panels relative to the window size if (contentWidth != this.lastContentWidth) { - var originalWidth = this.lastContentWidth != 0 ? this.lastContentWidth : contentWidth; + var originalWidth = this.lastContentWidth != 0 ? this.lastContentWidth : contentWidth; this.inputsWidth = this.inputsWidth / originalWidth * contentWidth; this.lastContentWidth = contentWidth; } @@ -299,8 +299,8 @@ internal class SeStringCreatorWidget : IDataWindowWidget break; case TextParameterType.String: - if (item.StringValue != null) - WidgetUtil.DrawCopyableText(MemoryHelper.ReadStringNullTerminated((nint)item.StringValue)); + if (item.StringValue.Value != null) + WidgetUtil.DrawCopyableText(item.StringValue.ToString()); else ImGui.TextUnformatted("null"); break; diff --git a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs index 3b6140b8c..891751c1e 100644 --- a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs +++ b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs @@ -442,7 +442,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable textNode->TextFlags |= (byte)TextFlags.MultiLine; textNode->AlignmentType = AlignmentType.TopLeft; - var containsDalamudVersionString = textNode->OriginalTextPointer == textNode->NodeText.StringPtr; + var containsDalamudVersionString = textNode->OriginalTextPointer.Value == textNode->NodeText.StringPtr.Value; if (!this.configuration.ShowTsm || !this.showTsm.Value) { if (containsDalamudVersionString) @@ -460,7 +460,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable this.lastLoadedPluginCount = count; var lssb = LSeStringBuilder.SharedPool.Get(); - lssb.Append(new ReadOnlySeStringSpan(addon->AtkValues[1].String)).Append("\n\n"); + lssb.Append(new ReadOnlySeStringSpan(addon->AtkValues[1].String.Value)).Append("\n\n"); lssb.PushEdgeColorType(701).PushColorType(539) .Append(SeIconChar.BoxedLetterD.ToIconChar()) .PopColorType().PopEdgeColorType(); diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.FromExistingTexture.cs b/Dalamud/Interface/Textures/Internal/TextureManager.FromExistingTexture.cs index 2ce96e59d..829b8d0c5 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManager.FromExistingTexture.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.FromExistingTexture.cs @@ -9,7 +9,7 @@ using Dalamud.Plugin.Services; using Dalamud.Utility; using Dalamud.Utility.TerraFxCom; -using Lumina.Data.Files; +using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; using TerraFX.Interop.DirectX; using TerraFX.Interop.Windows; @@ -24,32 +24,30 @@ internal sealed partial class TextureManager (nint)this.ConvertToKernelTexture(wrap, leaveWrapOpen); /// - public unsafe FFXIVClientStructs.FFXIV.Client.Graphics.Kernel.Texture* ConvertToKernelTexture( - IDalamudTextureWrap wrap, - bool leaveWrapOpen = false) + public unsafe Texture* ConvertToKernelTexture(IDalamudTextureWrap wrap, bool leaveWrapOpen = false) { using var wrapAux = new WrapAux(wrap, leaveWrapOpen); - var flags = TexFile.Attribute.TextureType2D; + var flags = TextureFlags.TextureType2D; if (wrapAux.Desc.Usage == D3D11_USAGE.D3D11_USAGE_IMMUTABLE) - flags |= TexFile.Attribute.Immutable; + flags |= TextureFlags.Immutable; if (wrapAux.Desc.Usage == D3D11_USAGE.D3D11_USAGE_DYNAMIC) - flags |= TexFile.Attribute.ReadWrite; + flags |= TextureFlags.ReadWrite; if ((wrapAux.Desc.CPUAccessFlags & (uint)D3D11_CPU_ACCESS_FLAG.D3D11_CPU_ACCESS_READ) != 0) - flags |= TexFile.Attribute.CpuRead; + flags |= TextureFlags.CpuRead; if ((wrapAux.Desc.BindFlags & (uint)D3D11_BIND_FLAG.D3D11_BIND_RENDER_TARGET) != 0) - flags |= TexFile.Attribute.TextureRenderTarget; + flags |= TextureFlags.TextureRenderTarget; if ((wrapAux.Desc.BindFlags & (uint)D3D11_BIND_FLAG.D3D11_BIND_DEPTH_STENCIL) != 0) - flags |= TexFile.Attribute.TextureDepthStencil; + flags |= TextureFlags.TextureDepthStencil; if (wrapAux.Desc.ArraySize != 1) throw new NotSupportedException("TextureArray2D is currently not supported."); - var gtex = FFXIVClientStructs.FFXIV.Client.Graphics.Kernel.Texture.CreateTexture2D( + var gtex = Texture.CreateTexture2D( (int)wrapAux.Desc.Width, (int)wrapAux.Desc.Height, (byte)wrapAux.Desc.MipLevels, - (uint)TexFile.TextureFormat.Null, // instructs the game to skip preprocessing it seems - (uint)flags, + 0, // instructs the game to skip preprocessing it seems + flags, 0); // Kernel::Texture owns these resources. We're passing the ownership to them. @@ -57,28 +55,27 @@ internal sealed partial class TextureManager wrapAux.SrvPtr->AddRef(); // Not sure this is needed - var ltf = wrapAux.Desc.Format switch + gtex->TextureFormat = wrapAux.Desc.Format switch { - DXGI_FORMAT.DXGI_FORMAT_R32G32B32A32_FLOAT => TexFile.TextureFormat.R32G32B32A32F, - DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_FLOAT => TexFile.TextureFormat.R16G16B16A16F, - DXGI_FORMAT.DXGI_FORMAT_R32G32_FLOAT => TexFile.TextureFormat.R32G32F, - DXGI_FORMAT.DXGI_FORMAT_R16G16_FLOAT => TexFile.TextureFormat.R16G16F, - DXGI_FORMAT.DXGI_FORMAT_R32_FLOAT => TexFile.TextureFormat.R32F, - DXGI_FORMAT.DXGI_FORMAT_R24G8_TYPELESS => TexFile.TextureFormat.D24S8, - DXGI_FORMAT.DXGI_FORMAT_R16_TYPELESS => TexFile.TextureFormat.D16, - DXGI_FORMAT.DXGI_FORMAT_A8_UNORM => TexFile.TextureFormat.A8, - DXGI_FORMAT.DXGI_FORMAT_BC1_UNORM => TexFile.TextureFormat.BC1, - DXGI_FORMAT.DXGI_FORMAT_BC2_UNORM => TexFile.TextureFormat.BC2, - DXGI_FORMAT.DXGI_FORMAT_BC3_UNORM => TexFile.TextureFormat.BC3, - DXGI_FORMAT.DXGI_FORMAT_BC5_UNORM => TexFile.TextureFormat.BC5, - DXGI_FORMAT.DXGI_FORMAT_B4G4R4A4_UNORM => TexFile.TextureFormat.B4G4R4A4, - DXGI_FORMAT.DXGI_FORMAT_B5G5R5A1_UNORM => TexFile.TextureFormat.B5G5R5A1, - DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM => TexFile.TextureFormat.B8G8R8A8, - DXGI_FORMAT.DXGI_FORMAT_B8G8R8X8_UNORM => TexFile.TextureFormat.B8G8R8X8, - DXGI_FORMAT.DXGI_FORMAT_BC7_UNORM => TexFile.TextureFormat.BC7, - _ => TexFile.TextureFormat.Null, + DXGI_FORMAT.DXGI_FORMAT_R32G32B32A32_FLOAT => TextureFormat.R32G32B32A32_FLOAT, + DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_FLOAT => TextureFormat.R16G16B16A16_FLOAT, + DXGI_FORMAT.DXGI_FORMAT_R32G32_FLOAT => TextureFormat.R32G32_FLOAT, + DXGI_FORMAT.DXGI_FORMAT_R16G16_FLOAT => TextureFormat.R16G16_FLOAT, + DXGI_FORMAT.DXGI_FORMAT_R32_FLOAT => TextureFormat.R32_FLOAT, + DXGI_FORMAT.DXGI_FORMAT_R24G8_TYPELESS => TextureFormat.D24_UNORM_S8_UINT, + DXGI_FORMAT.DXGI_FORMAT_R16_TYPELESS => TextureFormat.D16_UNORM, + DXGI_FORMAT.DXGI_FORMAT_A8_UNORM => TextureFormat.A8_UNORM, + DXGI_FORMAT.DXGI_FORMAT_BC1_UNORM => TextureFormat.BC1_UNORM, + DXGI_FORMAT.DXGI_FORMAT_BC2_UNORM => TextureFormat.BC2_UNORM, + DXGI_FORMAT.DXGI_FORMAT_BC3_UNORM => TextureFormat.BC3_UNORM, + DXGI_FORMAT.DXGI_FORMAT_BC5_UNORM => TextureFormat.BC5_UNORM, + DXGI_FORMAT.DXGI_FORMAT_B4G4R4A4_UNORM => TextureFormat.B4G4R4A4_UNORM, + DXGI_FORMAT.DXGI_FORMAT_B5G5R5A1_UNORM => TextureFormat.B5G5R5A1_UNORM, + DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM => TextureFormat.B8G8R8A8_UNORM, + DXGI_FORMAT.DXGI_FORMAT_B8G8R8X8_UNORM => TextureFormat.B8G8R8X8_UNORM, + DXGI_FORMAT.DXGI_FORMAT_BC7_UNORM => TextureFormat.BC7_UNORM, + _ => 0, }; - gtex->TextureFormat = (FFXIVClientStructs.FFXIV.Client.Graphics.Kernel.TextureFormat)ltf; gtex->D3D11Texture2D = wrapAux.TexPtr; gtex->D3D11ShaderResourceView = wrapAux.SrvPtr; diff --git a/Dalamud/Utility/SeStringExtensions.cs b/Dalamud/Utility/SeStringExtensions.cs index b21b9b743..7dbffc696 100644 --- a/Dalamud/Utility/SeStringExtensions.cs +++ b/Dalamud/Utility/SeStringExtensions.cs @@ -1,5 +1,7 @@ using System.Linq; +using InteropGenerator.Runtime; + using Lumina.Text.Parse; using Lumina.Text.ReadOnly; @@ -226,4 +228,9 @@ public static class SeStringExtensions var replaced = ReplaceText(new ReadOnlySeString(builder.GetViewAsMemory()), toFind, replacement); builder.Clear().Append(replaced); } + + public static unsafe ReadOnlySeStringSpan AsReadOnlySeStringSpan(this CStringPointer ptr) + { + return new ReadOnlySeStringSpan(ptr.Value); + } } diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index 2c3e84640..3c99b4f8f 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 2c3e84640af5220b78b944a06fdca79c52144075 +Subproject commit 3c99b4f8f7f56ee4defd3ee75809c73312359f9e From 9a25946ec1cf413bf12823c6977ca8f9e01c344c Mon Sep 17 00:00:00 2001 From: goaaats Date: Mon, 24 Mar 2025 21:28:41 +0100 Subject: [PATCH 34/55] Associate Dalamud.Common and LocExporter with x64 configs --- Dalamud.sln | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/Dalamud.sln b/Dalamud.sln index 5b6f56c6e..92e28f8b7 100644 --- a/Dalamud.sln +++ b/Dalamud.sln @@ -74,8 +74,6 @@ Global Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {94E5B016-02B1-459B-97D9-E783F28764B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {94E5B016-02B1-459B-97D9-E783F28764B2}.Release|Any CPU.ActiveCfg = Release|Any CPU {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Debug|Any CPU.ActiveCfg = Debug|x64 {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Debug|Any CPU.Build.0 = Debug|x64 {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Release|Any CPU.ActiveCfg = Release|x64 @@ -124,14 +122,14 @@ Global {317A264C-920B-44A1-8A34-F3A6827B0705}.Debug|Any CPU.Build.0 = Debug|x64 {317A264C-920B-44A1-8A34-F3A6827B0705}.Release|Any CPU.ActiveCfg = Release|x64 {317A264C-920B-44A1-8A34-F3A6827B0705}.Release|Any CPU.Build.0 = Release|x64 - {F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Release|Any CPU.Build.0 = Release|Any CPU - {A568929D-6FF6-4DFA-9D14-5D7DC08FA5E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A568929D-6FF6-4DFA-9D14-5D7DC08FA5E0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A568929D-6FF6-4DFA-9D14-5D7DC08FA5E0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A568929D-6FF6-4DFA-9D14-5D7DC08FA5E0}.Release|Any CPU.Build.0 = Release|Any CPU + {F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Debug|Any CPU.ActiveCfg = Debug|x64 + {F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Debug|Any CPU.Build.0 = Debug|x64 + {F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Release|Any CPU.ActiveCfg = Release|x64 + {F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Release|Any CPU.Build.0 = Release|x64 + {A568929D-6FF6-4DFA-9D14-5D7DC08FA5E0}.Debug|Any CPU.ActiveCfg = Debug|x64 + {A568929D-6FF6-4DFA-9D14-5D7DC08FA5E0}.Debug|Any CPU.Build.0 = Debug|x64 + {A568929D-6FF6-4DFA-9D14-5D7DC08FA5E0}.Release|Any CPU.ActiveCfg = Release|x64 + {A568929D-6FF6-4DFA-9D14-5D7DC08FA5E0}.Release|Any CPU.Build.0 = Release|x64 {3620414C-7DFC-423E-929F-310E19F5D930}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3620414C-7DFC-423E-929F-310E19F5D930}.Debug|Any CPU.Build.0 = Debug|Any CPU {3620414C-7DFC-423E-929F-310E19F5D930}.Release|Any CPU.ActiveCfg = Release|Any CPU From 5dcd1cc52fbfa31beaae77afa165e491df9cab68 Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Mon, 24 Mar 2025 13:35:42 -0700 Subject: [PATCH 35/55] chore: fix angry compiler --- .../Internal/Windows/Data/Widgets/InventoryWidget.cs | 2 +- .../Windows/SelfTest/AgingSteps/ContextMenuAgingStep.cs | 4 ++-- Dalamud/Utility/SeStringExtensions.cs | 5 +++++ 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/InventoryWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/InventoryWidget.cs index 5cefc0853..efa5fa0df 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/InventoryWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/InventoryWidget.cs @@ -219,7 +219,7 @@ internal class InventoryWidget : IDataWindowWidget if (!this.IsEventItem(item.ItemId)) { - AddKeyValueRow(item.IsCollectable ? "Collectability" : "Spiritbond", item.Spiritbond.ToString()); + AddKeyValueRow(item.IsCollectable ? "Collectability" : "Spiritbond", item.SpiritbondOrCollectability.ToString()); if (item.CrafterContentId != 0) AddKeyValueRow("CrafterContentId", item.CrafterContentId.ToString()); diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/ContextMenuAgingStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/ContextMenuAgingStep.cs index f08eccd96..24faf562f 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/ContextMenuAgingStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/ContextMenuAgingStep.cs @@ -110,7 +110,7 @@ internal class ContextMenuAgingStep : IAgingStep return SelfTestStepResult.Waiting; } - + /// public void CleanUp() { @@ -244,7 +244,7 @@ internal class ContextMenuAgingStep : IAgingStep b.AppendLine($"Container: {item.ContainerType}"); b.AppendLine($"Slot: {item.InventorySlot}"); b.AppendLine($"Quantity: {item.Quantity}"); - b.AppendLine($"{(item.IsCollectable ? "Collectability" : "Spiritbond")}: {item.Spiritbond}"); + b.AppendLine($"{(item.IsCollectable ? "Collectability" : "Spiritbond")}: {item.SpiritbondOrCollectability}"); b.AppendLine($"Condition: {item.Condition / 300f:0.00}% ({item.Condition})"); b.AppendLine($"Is HQ: {item.IsHq}"); b.AppendLine($"Is Company Crest Applied: {item.IsCompanyCrestApplied}"); diff --git a/Dalamud/Utility/SeStringExtensions.cs b/Dalamud/Utility/SeStringExtensions.cs index 7dbffc696..f2c28bd7d 100644 --- a/Dalamud/Utility/SeStringExtensions.cs +++ b/Dalamud/Utility/SeStringExtensions.cs @@ -229,6 +229,11 @@ public static class SeStringExtensions builder.Clear().Append(replaced); } + /// + /// Convert a CStringPointer to a ReadOnySeStringSpan. + /// + /// The pointer to convert. + /// A span. public static unsafe ReadOnlySeStringSpan AsReadOnlySeStringSpan(this CStringPointer ptr) { return new ReadOnlySeStringSpan(ptr.Value); From 24358f5af6774d8e19c4539167dc76597393a9ba Mon Sep 17 00:00:00 2001 From: goaaats Date: Mon, 24 Mar 2025 21:41:55 +0100 Subject: [PATCH 36/55] Fix warning in injector --- Dalamud.Injector/EntryPoint.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Dalamud.Injector/EntryPoint.cs b/Dalamud.Injector/EntryPoint.cs index 4d26313a9..71c919179 100644 --- a/Dalamud.Injector/EntryPoint.cs +++ b/Dalamud.Injector/EntryPoint.cs @@ -419,7 +419,9 @@ namespace Dalamud.Injector } OSPlatform platform; - if (platformStr[0..(len = Math.Min(platformStr.Length, (key = "win").Length))] == key[0..len]) // covers both win32 and Windows + + // covers both win32 and Windows + if (platformStr[0..(len = Math.Min(platformStr.Length, (key = "win").Length))] == key[0..len]) { platform = OSPlatform.Windows; } From ddda4d477d92efacfe92609a532a694355c0736e Mon Sep 17 00:00:00 2001 From: goaaats Date: Mon, 24 Mar 2025 21:54:57 +0100 Subject: [PATCH 37/55] Make VS happy about SLN changes --- Dalamud.sln | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Dalamud.sln b/Dalamud.sln index 92e28f8b7..5b1eb9d30 100644 --- a/Dalamud.sln +++ b/Dalamud.sln @@ -74,6 +74,10 @@ Global Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution + {94E5B016-02B1-459B-97D9-E783F28764B2}.Debug|Any CPU.ActiveCfg = Debug|x64 + {94E5B016-02B1-459B-97D9-E783F28764B2}.Debug|Any CPU.Build.0 = Debug|x64 + {94E5B016-02B1-459B-97D9-E783F28764B2}.Release|Any CPU.ActiveCfg = Release|x64 + {94E5B016-02B1-459B-97D9-E783F28764B2}.Release|Any CPU.Build.0 = Release|x64 {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Debug|Any CPU.ActiveCfg = Debug|x64 {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Debug|Any CPU.Build.0 = Debug|x64 {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Release|Any CPU.ActiveCfg = Release|x64 From 314c046ec965c3ce6fa15bdb73a7825427324d27 Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Mon, 24 Mar 2025 17:43:49 -0700 Subject: [PATCH 38/55] feat: Add CStringPointer#ExtractText - Move CString extensions to their own class. - Add some words to our dictionary. --- Dalamud.sln.DotSettings | 2 ++ Dalamud/Utility/CStringExtensions.cs | 32 +++++++++++++++++++++++++++ Dalamud/Utility/SeStringExtensions.cs | 10 --------- 3 files changed, 34 insertions(+), 10 deletions(-) create mode 100644 Dalamud/Utility/CStringExtensions.cs diff --git a/Dalamud.sln.DotSettings b/Dalamud.sln.DotSettings index b0f66b736..6d0e1fdcd 100644 --- a/Dalamud.sln.DotSettings +++ b/Dalamud.sln.DotSettings @@ -54,6 +54,7 @@ True True True + True True True True @@ -66,6 +67,7 @@ True True True + True True True True diff --git a/Dalamud/Utility/CStringExtensions.cs b/Dalamud/Utility/CStringExtensions.cs new file mode 100644 index 000000000..a1848c512 --- /dev/null +++ b/Dalamud/Utility/CStringExtensions.cs @@ -0,0 +1,32 @@ +using InteropGenerator.Runtime; + +using Lumina.Text.ReadOnly; + +namespace Dalamud.Utility; + +/// +/// A set of helpful utilities for working with s from ClientStructs. +/// +public static class CStringExtensions +{ + /// + /// Convert a CStringPointer to a ReadOnlySeStringSpan. + /// + /// The pointer to convert. + /// A span. + public static unsafe ReadOnlySeStringSpan AsReadOnlySeStringSpan(this CStringPointer ptr) + { + return ptr.AsSpan(); + } + + /// + /// Extract text from this CStringPointer following 's rules. Only + /// useful for SeStrings. + /// + /// The CStringPointer to process. + /// Extracted text. + public static string ExtractText(this CStringPointer ptr) + { + return ptr.AsReadOnlySeStringSpan().ExtractText(); + } +} diff --git a/Dalamud/Utility/SeStringExtensions.cs b/Dalamud/Utility/SeStringExtensions.cs index f2c28bd7d..904375250 100644 --- a/Dalamud/Utility/SeStringExtensions.cs +++ b/Dalamud/Utility/SeStringExtensions.cs @@ -228,14 +228,4 @@ public static class SeStringExtensions var replaced = ReplaceText(new ReadOnlySeString(builder.GetViewAsMemory()), toFind, replacement); builder.Clear().Append(replaced); } - - /// - /// Convert a CStringPointer to a ReadOnySeStringSpan. - /// - /// The pointer to convert. - /// A span. - public static unsafe ReadOnlySeStringSpan AsReadOnlySeStringSpan(this CStringPointer ptr) - { - return new ReadOnlySeStringSpan(ptr.Value); - } } From a7509ef77dd0d50cbf53f7334759559190012ebe Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Mon, 24 Mar 2025 18:16:39 -0700 Subject: [PATCH 39/55] feat: More utilities! - Also document that CStringExtensions only works with bundled CS. --- .../Game/Text/Evaluator/SeStringEvaluator.cs | 2 +- Dalamud/Utility/CStringExtensions.cs | 32 +++++++++++++++++-- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs b/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs index 47c4b5899..6c768cfa1 100644 --- a/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs +++ b/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs @@ -1744,7 +1744,7 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator return false; case TextParameterType.String: - this.EvaluateAndAppendTo(builder, p.StringValue.AsReadOnlySeStringSpan(), null, language); + this.EvaluateAndAppendTo(builder, p.StringValue.AsSpan(), null, language); return false; case TextParameterType.Uninitialized: diff --git a/Dalamud/Utility/CStringExtensions.cs b/Dalamud/Utility/CStringExtensions.cs index a1848c512..f025ee85c 100644 --- a/Dalamud/Utility/CStringExtensions.cs +++ b/Dalamud/Utility/CStringExtensions.cs @@ -1,4 +1,6 @@ -using InteropGenerator.Runtime; +using Dalamud.Game.Text.SeStringHandling; + +using InteropGenerator.Runtime; using Lumina.Text.ReadOnly; @@ -7,6 +9,9 @@ namespace Dalamud.Utility; /// /// A set of helpful utilities for working with s from ClientStructs. /// +/// +/// WARNING: Will break if a custom ClientStructs is used. These are here for CONVENIENCE ONLY! +/// public static class CStringExtensions { /// @@ -14,11 +19,34 @@ public static class CStringExtensions /// /// The pointer to convert. /// A span. - public static unsafe ReadOnlySeStringSpan AsReadOnlySeStringSpan(this CStringPointer ptr) + public static ReadOnlySeStringSpan AsReadOnlySeStringSpan(this CStringPointer ptr) { return ptr.AsSpan(); } + /// + /// Convert a CStringPointer to a Dalamud SeString. + /// + /// The pointer to convert. + /// A Dalamud-flavored SeString. + public static SeString AsDalamudSeString(this CStringPointer ptr) + { + return ptr.AsReadOnlySeStringSpan().ToDalamudString(); + } + + /// + /// Get a new SeString that's a copy of the text in this CStringPointer. + /// + /// The pointer to copy. + /// A new Lumina SeString. + public static Lumina.Text.SeString AsLuminaSeString(this CStringPointer ptr) + { + var ssb = new Lumina.Text.SeStringBuilder(); + ssb.Append(ptr.AsSpan()); + + return ssb.ToSeString(); + } + /// /// Extract text from this CStringPointer following 's rules. Only /// useful for SeStrings. From 27dc6595610d461b2578271bf8ce532b40ff4fda Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Mon, 24 Mar 2025 18:32:44 -0700 Subject: [PATCH 40/55] chore: Convert AsLuminaSeString to AsReadOnlySeString - Somewhat more useful, or so one would hope. --- Dalamud/Utility/CStringExtensions.cs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/Dalamud/Utility/CStringExtensions.cs b/Dalamud/Utility/CStringExtensions.cs index f025ee85c..acc70e469 100644 --- a/Dalamud/Utility/CStringExtensions.cs +++ b/Dalamud/Utility/CStringExtensions.cs @@ -35,16 +35,17 @@ public static class CStringExtensions } /// - /// Get a new SeString that's a copy of the text in this CStringPointer. + /// Get a new ReadOnlySeString that's a copy of the text in this CStringPointer. /// + /// + /// This should be functionally identical to , but exists + /// for convenience in places that already expect ReadOnlySeString as a type (and where a copy is desired). + /// /// The pointer to copy. - /// A new Lumina SeString. - public static Lumina.Text.SeString AsLuminaSeString(this CStringPointer ptr) + /// A new Lumina ReadOnlySeString. + public static ReadOnlySeString AsReadOnlySeString(this CStringPointer ptr) { - var ssb = new Lumina.Text.SeStringBuilder(); - ssb.Append(ptr.AsSpan()); - - return ssb.ToSeString(); + return new ReadOnlySeString(ptr.AsSpan().ToArray()); } /// From 445fe09181eb190198a47c83b36ffd5222028832 Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Mon, 24 Mar 2025 18:50:39 -0700 Subject: [PATCH 41/55] fix: dalamud boot deadlock over wine --- Dalamud/EntryPoint.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/EntryPoint.cs b/Dalamud/EntryPoint.cs index 9f9b98cd4..f6ba990e6 100644 --- a/Dalamud/EntryPoint.cs +++ b/Dalamud/EntryPoint.cs @@ -183,7 +183,7 @@ public sealed class EntryPoint CultureFixes.Apply(); // Currently VEH is not fully functional on WINE - if (!Util.IsWine()) + if (info.Platform != OSPlatform.Windows) InitSymbolHandler(info); var dalamud = new Dalamud(info, fs, configuration, mainThreadContinueEvent); From 919ca87bbe1ece20a184341395ddc2eb1e0e4062 Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Mon, 24 Mar 2025 21:58:23 -0700 Subject: [PATCH 42/55] update clientstructs - fix broken gamepad sig by delegating to CS --- .../ClientState/ClientStateAddressResolver.cs | 8 -- .../Game/ClientState/GamePad/GamepadInput.cs | 75 ----------------- .../Game/ClientState/GamePad/GamepadState.cs | 83 +++++++++---------- .../Windows/Data/Widgets/GamepadWidget.cs | 14 ++-- lib/FFXIVClientStructs | 2 +- 5 files changed, 48 insertions(+), 134 deletions(-) delete mode 100644 Dalamud/Game/ClientState/GamePad/GamepadInput.cs diff --git a/Dalamud/Game/ClientState/ClientStateAddressResolver.cs b/Dalamud/Game/ClientState/ClientStateAddressResolver.cs index d44275ef8..97bc5dae1 100644 --- a/Dalamud/Game/ClientState/ClientStateAddressResolver.cs +++ b/Dalamud/Game/ClientState/ClientStateAddressResolver.cs @@ -24,12 +24,6 @@ internal sealed class ClientStateAddressResolver : BaseAddressResolver /// public IntPtr ProcessPacketPlayerSetup { get; private set; } - /// - /// Gets the address of the method which polls the gamepads for data. - /// Called every frame, even when `Enable Gamepad` is off in the settings. - /// - public IntPtr GamepadPoll { get; private set; } - /// /// Scan for and setup any configured address pointers. /// @@ -43,7 +37,5 @@ internal sealed class ClientStateAddressResolver : BaseAddressResolver // movzx edx, byte ptr [rbx+rsi+1D5E0E0h] KeyboardStateIndexArray this.KeyboardState = sig.ScanText("48 8D 0C 85 ?? ?? ?? ?? 8B 04 31 85 C2 0F 85") + 0x4; this.KeyboardStateIndexArray = sig.ScanText("0F B6 94 33 ?? ?? ?? ?? 84 D2") + 0x4; - - this.GamepadPoll = sig.ScanText("40 55 53 57 41 54 41 57 48 8D AC 24 ?? ?? ?? ?? 48 81 EC ?? ?? ?? ?? 44 0F 29 B4 24"); // unnamed in cs } } diff --git a/Dalamud/Game/ClientState/GamePad/GamepadInput.cs b/Dalamud/Game/ClientState/GamePad/GamepadInput.cs deleted file mode 100644 index d9dcea60f..000000000 --- a/Dalamud/Game/ClientState/GamePad/GamepadInput.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System.Runtime.InteropServices; - -namespace Dalamud.Game.ClientState.GamePad; - -/// -/// Struct which gets populated by polling the gamepads. -/// -/// Has an array of gamepads, among many other things (here not mapped). -/// All we really care about is the final data which the game uses to determine input. -/// -/// The size is definitely bigger than only the following fields but I do not know how big. -/// -[StructLayout(LayoutKind.Explicit)] -public struct GamepadInput -{ - /// - /// Left analogue stick's horizontal value, -99 for left, 99 for right. - /// - [FieldOffset(0x78)] - public int LeftStickX; - - /// - /// Left analogue stick's vertical value, -99 for down, 99 for up. - /// - [FieldOffset(0x7C)] - public int LeftStickY; - - /// - /// Right analogue stick's horizontal value, -99 for left, 99 for right. - /// - [FieldOffset(0x80)] - public int RightStickX; - - /// - /// Right analogue stick's vertical value, -99 for down, 99 for up. - /// - [FieldOffset(0x84)] - public int RightStickY; - - /// - /// Raw input, set the whole time while a button is held. See for the mapping. - /// - /// - /// This is a bitfield. - /// - [FieldOffset(0x88)] - public ushort ButtonsRaw; - - /// - /// Button pressed, set once when the button is pressed. See for the mapping. - /// - /// - /// This is a bitfield. - /// - [FieldOffset(0x8C)] - public ushort ButtonsPressed; - - /// - /// Button released input, set once right after the button is not hold anymore. See for the mapping. - /// - /// - /// This is a bitfield. - /// - [FieldOffset(0x90)] - public ushort ButtonsReleased; - - /// - /// Repeatedly emits the held button input in fixed intervals. See for the mapping. - /// - /// - /// This is a bitfield. - /// - [FieldOffset(0x94)] - public ushort ButtonsRepeat; -} diff --git a/Dalamud/Game/ClientState/GamePad/GamepadState.cs b/Dalamud/Game/ClientState/GamePad/GamepadState.cs index 05d691823..5237c6f0c 100644 --- a/Dalamud/Game/ClientState/GamePad/GamepadState.cs +++ b/Dalamud/Game/ClientState/GamePad/GamepadState.cs @@ -4,7 +4,8 @@ using Dalamud.Hooking; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Plugin.Services; -using Dalamud.Utility; + +using FFXIVClientStructs.FFXIV.Client.System.Input; using ImGuiNET; using Serilog; @@ -23,7 +24,7 @@ namespace Dalamud.Game.ClientState.GamePad; #pragma warning restore SA1015 internal unsafe class GamepadState : IInternalDisposableService, IGamepadState { - private readonly Hook? gamepadPoll; + private readonly Hook? gamepadPoll; private bool isDisposed; @@ -35,25 +36,21 @@ internal unsafe class GamepadState : IInternalDisposableService, IGamepadState [ServiceManager.ServiceConstructor] private GamepadState(ClientState clientState) { - var resolver = clientState.AddressResolver; - Log.Verbose($"GamepadPoll address {Util.DescribeAddress(resolver.GamepadPoll)}"); - this.gamepadPoll = Hook.FromAddress(resolver.GamepadPoll, this.GamepadPollDetour); + this.gamepadPoll = Hook.FromAddress((nint)PadDevice.StaticVirtualTablePointer->Poll, this.GamepadPollDetour); this.gamepadPoll?.Enable(); } - private delegate int ControllerPoll(IntPtr controllerInput); - /// /// Gets the pointer to the current instance of the GamepadInput struct. /// public IntPtr GamepadInputAddress { get; private set; } /// - public Vector2 LeftStick => + public Vector2 LeftStick => new(this.leftStickX, this.leftStickY); - + /// - public Vector2 RightStick => + public Vector2 RightStick => new(this.rightStickX, this.rightStickY); /// @@ -61,28 +58,28 @@ internal unsafe class GamepadState : IInternalDisposableService, IGamepadState /// /// Exposed internally for Debug Data window. /// - internal ushort ButtonsPressed { get; private set; } + internal GamepadButtons ButtonsPressed { get; private set; } /// /// Gets raw button bitmask, set the whole time while a button is held. See for the mapping. /// /// Exposed internally for Debug Data window. /// - internal ushort ButtonsRaw { get; private set; } + internal GamepadButtons ButtonsRaw { get; private set; } /// /// Gets button released bitmask, set once right after the button is not hold anymore. See for the mapping. /// /// Exposed internally for Debug Data window. /// - internal ushort ButtonsReleased { get; private set; } + internal GamepadButtons ButtonsReleased { get; private set; } /// /// Gets button repeat bitmask, emits the held button input in fixed intervals. See for the mapping. /// /// Exposed internally for Debug Data window. /// - internal ushort ButtonsRepeat { get; private set; } + internal GamepadButtons ButtonsRepeat { get; private set; } /// /// Gets or sets a value indicating whether detour should block gamepad input for game. @@ -95,16 +92,16 @@ internal unsafe class GamepadState : IInternalDisposableService, IGamepadState internal bool NavEnableGamepad { get; set; } /// - public float Pressed(GamepadButtons button) => (this.ButtonsPressed & (ushort)button) > 0 ? 1 : 0; + public float Pressed(GamepadButtons button) => (this.ButtonsPressed & button) > 0 ? 1 : 0; /// - public float Repeat(GamepadButtons button) => (this.ButtonsRepeat & (ushort)button) > 0 ? 1 : 0; + public float Repeat(GamepadButtons button) => (this.ButtonsRepeat & button) > 0 ? 1 : 0; /// - public float Released(GamepadButtons button) => (this.ButtonsReleased & (ushort)button) > 0 ? 1 : 0; + public float Released(GamepadButtons button) => (this.ButtonsReleased & button) > 0 ? 1 : 0; /// - public float Raw(GamepadButtons button) => (this.ButtonsRaw & (ushort)button) > 0 ? 1 : 0; + public float Raw(GamepadButtons button) => (this.ButtonsRaw & button) > 0 ? 1 : 0; /// /// Disposes this instance, alongside its hooks. @@ -115,28 +112,28 @@ internal unsafe class GamepadState : IInternalDisposableService, IGamepadState GC.SuppressFinalize(this); } - private int GamepadPollDetour(IntPtr gamepadInput) + private nint GamepadPollDetour(PadDevice* gamepadInput) { var original = this.gamepadPoll!.Original(gamepadInput); try { - this.GamepadInputAddress = gamepadInput; - var input = (GamepadInput*)gamepadInput; - this.leftStickX = input->LeftStickX; - this.leftStickY = input->LeftStickY; - this.rightStickX = input->RightStickX; - this.rightStickY = input->RightStickY; - this.ButtonsRaw = input->ButtonsRaw; - this.ButtonsPressed = input->ButtonsPressed; - this.ButtonsReleased = input->ButtonsReleased; - this.ButtonsRepeat = input->ButtonsRepeat; + this.GamepadInputAddress = (nint)gamepadInput; + + this.leftStickX = gamepadInput->GamepadInputData.LeftStickX; + this.leftStickY = gamepadInput->GamepadInputData.LeftStickY; + this.rightStickX = gamepadInput->GamepadInputData.RightStickX; + this.rightStickY = gamepadInput->GamepadInputData.RightStickY; + this.ButtonsRaw = (GamepadButtons)gamepadInput->GamepadInputData.Buttons; + this.ButtonsPressed = (GamepadButtons)gamepadInput->GamepadInputData.ButtonsPressed; + this.ButtonsReleased = (GamepadButtons)gamepadInput->GamepadInputData.ButtonsReleased; + this.ButtonsRepeat = (GamepadButtons)gamepadInput->GamepadInputData.ButtonsRepeat; if (this.NavEnableGamepad) { - input->LeftStickX = 0; - input->LeftStickY = 0; - input->RightStickX = 0; - input->RightStickY = 0; + gamepadInput->GamepadInputData.LeftStickX = 0; + gamepadInput->GamepadInputData.LeftStickY = 0; + gamepadInput->GamepadInputData.RightStickX = 0; + gamepadInput->GamepadInputData.RightStickY = 0; // NOTE (Chiv) Zeroing `ButtonsRaw` destroys `ButtonPressed`, `ButtonReleased` // and `ButtonRepeat` as the game uses the RAW input to determine those (apparently). @@ -153,16 +150,16 @@ internal unsafe class GamepadState : IInternalDisposableService, IGamepadState // `ButtonPressed` while ImGuiConfigFlags.NavEnableGamepad is set. // This is debatable. // ImGui itself does not care either way as it uses the Raw values and does its own state handling. - const ushort deletionMask = (ushort)(~GamepadButtons.L2 - & ~GamepadButtons.R2 - & ~GamepadButtons.DpadDown - & ~GamepadButtons.DpadLeft - & ~GamepadButtons.DpadUp - & ~GamepadButtons.DpadRight); - input->ButtonsRaw &= deletionMask; - input->ButtonsPressed = 0; - input->ButtonsReleased = 0; - input->ButtonsRepeat = 0; + const GamepadButtonsFlags deletionMask = ~GamepadButtonsFlags.L2 + & ~GamepadButtonsFlags.R2 + & ~GamepadButtonsFlags.DPadDown + & ~GamepadButtonsFlags.DPadLeft + & ~GamepadButtonsFlags.DPadUp + & ~GamepadButtonsFlags.DPadRight; + gamepadInput->GamepadInputData.Buttons &= deletionMask; + gamepadInput->GamepadInputData.ButtonsPressed = 0; + gamepadInput->GamepadInputData.ButtonsReleased = 0; + gamepadInput->GamepadInputData.ButtonsRepeat = 0; return 0; } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/GamepadWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/GamepadWidget.cs index 5121d82e6..610fa90cc 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/GamepadWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/GamepadWidget.cs @@ -12,9 +12,9 @@ internal class GamepadWidget : IDataWindowWidget { /// public string[]? CommandShortcuts { get; init; } = { "gamepad", "controller" }; - + /// - public string DisplayName { get; init; } = "Gamepad"; + public string DisplayName { get; init; } = "Gamepad"; /// public bool Ready { get; set; } @@ -42,24 +42,24 @@ internal class GamepadWidget : IDataWindowWidget this.DrawHelper( "Buttons Raw", - gamepadState.ButtonsRaw, + (uint)gamepadState.ButtonsRaw, gamepadState.Raw); this.DrawHelper( "Buttons Pressed", - gamepadState.ButtonsPressed, + (uint)gamepadState.ButtonsPressed, gamepadState.Pressed); this.DrawHelper( "Buttons Repeat", - gamepadState.ButtonsRepeat, + (uint)gamepadState.ButtonsRepeat, gamepadState.Repeat); this.DrawHelper( "Buttons Released", - gamepadState.ButtonsReleased, + (uint)gamepadState.ButtonsReleased, gamepadState.Released); ImGui.Text($"LeftStick {gamepadState.LeftStick}"); ImGui.Text($"RightStick {gamepadState.RightStick}"); } - + private void DrawHelper(string text, uint mask, Func resolve) { ImGui.Text($"{text} {mask:X4}"); diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index 3c99b4f8f..48076a4ce 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 3c99b4f8f7f56ee4defd3ee75809c73312359f9e +Subproject commit 48076a4ce750c8f008f3bedd04d0bced03147f56 From 12ce09870fb7f8f9ec9eead5d6d4fccde40cc7b6 Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Tue, 25 Mar 2025 09:10:00 -0700 Subject: [PATCH 43/55] deps: bump deps - Lumina.Excel to 7.2.0 - ClientStructs to latest Co-authored-by: Asriel Camora --- Directory.Build.props | 2 +- lib/FFXIVClientStructs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 6fd42faa1..2905b80b1 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -11,7 +11,7 @@ 5.6.1 - 7.1.3 + 7.2.0 13.0.3 diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index 48076a4ce..794671e6c 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 48076a4ce750c8f008f3bedd04d0bced03147f56 +Subproject commit 794671e6c66d12c975dc67cf95691c52bc5f6feb From 67561af32f6e28a97a075a7d4c5c7182a0bb94d4 Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Tue, 25 Mar 2025 09:28:57 -0700 Subject: [PATCH 44/55] fix: Lumina updates --- Dalamud/Game/Internal/DalamudAtkTweaks.cs | 6 ++--- .../Internal/SheetRedirectResolver.cs | 2 +- .../Game/Text/Evaluator/SeStringEvaluator.cs | 4 ++-- Dalamud/Game/Text/SeStringHandling/Payload.cs | 4 ++-- .../Payloads/UIForegroundPayload.cs | 4 ++-- .../Payloads/UIGlowPayload.cs | 4 ++-- .../Internal/SeStringColorStackSet.cs | 8 +++---- .../Windows/Data/Widgets/InventoryWidget.cs | 2 +- .../Windows/Data/Widgets/UIColorWidget.cs | 24 +++++++++---------- Dalamud/Utility/CStringExtensions.cs | 2 +- 10 files changed, 30 insertions(+), 30 deletions(-) diff --git a/Dalamud/Game/Internal/DalamudAtkTweaks.cs b/Dalamud/Game/Internal/DalamudAtkTweaks.cs index 7834ab58f..83a2f3525 100644 --- a/Dalamud/Game/Internal/DalamudAtkTweaks.cs +++ b/Dalamud/Game/Internal/DalamudAtkTweaks.cs @@ -53,7 +53,7 @@ internal sealed unsafe class DalamudAtkTweaks : IInternalDisposableService this.locDalamudSettings = Loc.Localize("SystemMenuSettings", "Dalamud Settings"); // this.contextMenu.ContextMenuOpened += this.ContextMenuOnContextMenuOpened; - + this.hookAgentHudOpenSystemMenu.Enable(); this.hookUiModuleExecuteMainCommand.Enable(); this.hookAtkUnitBaseReceiveGlobalEvent.Enable(); @@ -180,7 +180,7 @@ internal sealed unsafe class DalamudAtkTweaks : IInternalDisposableService // about hooking the exd reader, thank god var firstStringEntry = &atkValueArgs[5 + 18]; firstStringEntry->ChangeType(ValueType.String); - + var secondStringEntry = &atkValueArgs[6 + 18]; secondStringEntry->ChangeType(ValueType.String); @@ -193,7 +193,7 @@ internal sealed unsafe class DalamudAtkTweaks : IInternalDisposableService .Append($"{SeIconChar.BoxedLetterD.ToIconString()} ") .Append(new UIForegroundPayload(0)) .Append(this.locDalamudSettings).Encode(); - + firstStringEntry->SetManagedString(strPlugins); secondStringEntry->SetManagedString(strSettings); diff --git a/Dalamud/Game/Text/Evaluator/Internal/SheetRedirectResolver.cs b/Dalamud/Game/Text/Evaluator/Internal/SheetRedirectResolver.cs index 49af895d9..1be0f8866 100644 --- a/Dalamud/Game/Text/Evaluator/Internal/SheetRedirectResolver.cs +++ b/Dalamud/Game/Text/Evaluator/Internal/SheetRedirectResolver.cs @@ -193,7 +193,7 @@ internal class SheetRedirectResolver : IServiceType colIndex = 43; if (this.dataManager.GetExcelSheet().TryGetRow(rowId, out var row)) - rowId = row.Order; + rowId = row.SortKey; break; } diff --git a/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs b/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs index 6c768cfa1..d9b2b2e01 100644 --- a/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs +++ b/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs @@ -1595,7 +1595,7 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator if (eColorTypeVal == 0) context.Builder.PopColor(); else if (this.dataManager.GetExcelSheet().TryGetRow(eColorTypeVal, out var row)) - context.Builder.PushColorBgra((row.UIForeground >> 8) | (row.UIForeground << 24)); + context.Builder.PushColorBgra((row.Light >> 8) | (row.Light << 24)); return true; } @@ -1609,7 +1609,7 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator if (eColorTypeVal == 0) context.Builder.PopEdgeColor(); else if (this.dataManager.GetExcelSheet().TryGetRow(eColorTypeVal, out var row)) - context.Builder.PushEdgeColorBgra((row.UIForeground >> 8) | (row.UIForeground << 24)); + context.Builder.PushEdgeColorBgra((row.Light >> 8) | (row.Light << 24)); return true; } diff --git a/Dalamud/Game/Text/SeStringHandling/Payload.cs b/Dalamud/Game/Text/SeStringHandling/Payload.cs index a38a8271d..7131a88a7 100644 --- a/Dalamud/Game/Text/SeStringHandling/Payload.cs +++ b/Dalamud/Game/Text/SeStringHandling/Payload.cs @@ -201,7 +201,7 @@ public abstract partial class Payload case SeStringChunkType.Icon: payload = new IconPayload(); break; - + default: // Log.Verbose("Unhandled SeStringChunkType: {0}", chunkType); break; @@ -306,7 +306,7 @@ public abstract partial class Payload /// See the . /// NewLine = 0x10, - + /// /// See the class. /// diff --git a/Dalamud/Game/Text/SeStringHandling/Payloads/UIForegroundPayload.cs b/Dalamud/Game/Text/SeStringHandling/Payloads/UIForegroundPayload.cs index 995c20211..4e8de8f8f 100644 --- a/Dalamud/Game/Text/SeStringHandling/Payloads/UIForegroundPayload.cs +++ b/Dalamud/Game/Text/SeStringHandling/Payloads/UIForegroundPayload.cs @@ -74,13 +74,13 @@ public class UIForegroundPayload : Payload /// Gets the Red/Green/Blue/Alpha values for this foreground color, encoded as a typical hex color. /// [JsonIgnore] - public uint RGBA => this.UIColor.Value.UIForeground; + public uint RGBA => this.UIColor.Value.Dark; /// /// Gets the ABGR value for this foreground color, as ImGui requires it in PushColor. /// [JsonIgnore] - public uint ABGR => Interface.ColorHelpers.SwapEndianness(this.UIColor.Value.UIForeground); + public uint ABGR => Interface.ColorHelpers.SwapEndianness(this.UIColor.Value.Dark); /// public override string ToString() diff --git a/Dalamud/Game/Text/SeStringHandling/Payloads/UIGlowPayload.cs b/Dalamud/Game/Text/SeStringHandling/Payloads/UIGlowPayload.cs index 3049ccac3..840d6c85c 100644 --- a/Dalamud/Game/Text/SeStringHandling/Payloads/UIGlowPayload.cs +++ b/Dalamud/Game/Text/SeStringHandling/Payloads/UIGlowPayload.cs @@ -71,13 +71,13 @@ public class UIGlowPayload : Payload /// Gets the Red/Green/Blue/Alpha values for this glow color, encoded as a typical hex color. /// [JsonIgnore] - public uint RGBA => this.UIColor.Value.UIGlow; + public uint RGBA => this.UIColor.Value.Light; /// /// Gets the ABGR value for this glow color, as ImGui requires it in PushColor. /// [JsonIgnore] - public uint ABGR => Interface.ColorHelpers.SwapEndianness(this.UIColor.Value.UIGlow); + public uint ABGR => Interface.ColorHelpers.SwapEndianness(this.UIColor.Value.Light); /// /// Gets a Lumina UIColor object representing this payload. The actual color data is at UIColor.UIGlow. diff --git a/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringColorStackSet.cs b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringColorStackSet.cs index ddff55923..ad60d405e 100644 --- a/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringColorStackSet.cs +++ b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringColorStackSet.cs @@ -43,10 +43,10 @@ internal sealed class SeStringColorStackSet foreach (var row in uiColor) { // Contains ABGR. - this.colorTypes[row.RowId, 0] = row.UIForeground; - this.colorTypes[row.RowId, 1] = row.UIGlow; - this.colorTypes[row.RowId, 2] = row.Unknown0; - this.colorTypes[row.RowId, 3] = row.Unknown1; + 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) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/InventoryWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/InventoryWidget.cs index efa5fa0df..f455f2e66 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/InventoryWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/InventoryWidget.cs @@ -380,7 +380,7 @@ internal class InventoryWidget : IDataWindowWidget var rowId = this.GetItemRarityColorType(item, isEdgeColor); return this.dataManager.Excel.GetSheet().TryGetRow(rowId, out var color) - ? BinaryPrimitives.ReverseEndianness(color.UIForeground) | 0xFF000000 + ? BinaryPrimitives.ReverseEndianness(color.Light) | 0xFF000000 : 0xFFFFFFFF; } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs index d43ae50a3..45f1ad715 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs @@ -93,34 +93,34 @@ internal class UiColorWidget : IDataWindowWidget ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); - ImGui.PushID($"row{id}_col1"); - if (this.DrawColorColumn(row.UIForeground) && + ImGui.PushID($"row{id}_dark"); + if (this.DrawColorColumn(row.Dark) && adjacentRow.HasValue) - DrawEdgePreview(id, row.UIForeground, adjacentRow.Value.UIForeground); + DrawEdgePreview(id, row.Dark, adjacentRow.Value.Dark); ImGui.PopID(); ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); - ImGui.PushID($"row{id}_col2"); - if (this.DrawColorColumn(row.UIGlow) && + ImGui.PushID($"row{id}_light"); + if (this.DrawColorColumn(row.Light) && adjacentRow.HasValue) - DrawEdgePreview(id, row.UIGlow, adjacentRow.Value.UIGlow); + DrawEdgePreview(id, row.Light, adjacentRow.Value.Light); ImGui.PopID(); ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); - ImGui.PushID($"row{id}_col3"); - if (this.DrawColorColumn(row.Unknown0) && + ImGui.PushID($"row{id}_classic"); + if (this.DrawColorColumn(row.ClassicFF) && adjacentRow.HasValue) - DrawEdgePreview(id, row.Unknown0, adjacentRow.Value.Unknown0); + DrawEdgePreview(id, row.ClassicFF, adjacentRow.Value.ClassicFF); ImGui.PopID(); ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); - ImGui.PushID($"row{id}_col4"); - if (this.DrawColorColumn(row.Unknown1) && + ImGui.PushID($"row{id}_blue"); + if (this.DrawColorColumn(row.ClearBlue) && adjacentRow.HasValue) - DrawEdgePreview(id, row.Unknown1, adjacentRow.Value.Unknown1); + DrawEdgePreview(id, row.ClearBlue, adjacentRow.Value.ClearBlue); ImGui.PopID(); } } diff --git a/Dalamud/Utility/CStringExtensions.cs b/Dalamud/Utility/CStringExtensions.cs index acc70e469..83ebb186f 100644 --- a/Dalamud/Utility/CStringExtensions.cs +++ b/Dalamud/Utility/CStringExtensions.cs @@ -10,7 +10,7 @@ namespace Dalamud.Utility; /// A set of helpful utilities for working with s from ClientStructs. /// /// -/// WARNING: Will break if a custom ClientStructs is used. These are here for CONVENIENCE ONLY! +/// WARNING: Will break if a custom ClientStructs is used. These are here for CONVENIENCE ONLY!. /// public static class CStringExtensions { From c5af536032c291c16071685ddf466b57bf77f86e Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Tue, 25 Mar 2025 09:35:21 -0700 Subject: [PATCH 45/55] docs: Explain what the UI*Payloads actually are. --- .../Game/Text/SeStringHandling/Payloads/UIForegroundPayload.cs | 3 ++- Dalamud/Game/Text/SeStringHandling/Payloads/UIGlowPayload.cs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Dalamud/Game/Text/SeStringHandling/Payloads/UIForegroundPayload.cs b/Dalamud/Game/Text/SeStringHandling/Payloads/UIForegroundPayload.cs index 4e8de8f8f..8443e06ce 100644 --- a/Dalamud/Game/Text/SeStringHandling/Payloads/UIForegroundPayload.cs +++ b/Dalamud/Game/Text/SeStringHandling/Payloads/UIForegroundPayload.cs @@ -10,7 +10,8 @@ using Newtonsoft.Json; namespace Dalamud.Game.Text.SeStringHandling.Payloads; /// -/// An SeString Payload representing a UI foreground color applied to following text payloads. +/// An SeString Payload that allows text to have a specific color. The color selected will be determined by the +/// theme's coloring, regardless of the active theme. /// public class UIForegroundPayload : Payload { diff --git a/Dalamud/Game/Text/SeStringHandling/Payloads/UIGlowPayload.cs b/Dalamud/Game/Text/SeStringHandling/Payloads/UIGlowPayload.cs index 840d6c85c..d22318378 100644 --- a/Dalamud/Game/Text/SeStringHandling/Payloads/UIGlowPayload.cs +++ b/Dalamud/Game/Text/SeStringHandling/Payloads/UIGlowPayload.cs @@ -10,7 +10,8 @@ using Newtonsoft.Json; namespace Dalamud.Game.Text.SeStringHandling.Payloads; /// -/// An SeString Payload representing a UI glow color applied to following text payloads. +/// An SeString Payload that allows text to have a specific edge glow. The color selected will be determined by the +/// theme's coloring, regardless of the active theme. /// public class UIGlowPayload : Payload { From 0377e847658d3f65b4b7acf6f610ab20b9a8bbad Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Tue, 25 Mar 2025 17:37:18 +0100 Subject: [PATCH 46/55] Config Options for 7.2 (#2205) --- Dalamud/Game/Config/SystemConfigOption.cs | 84 +++++++++++----------- Dalamud/Game/Config/UiConfigOption.cs | 85 +++++++++++++---------- 2 files changed, 91 insertions(+), 78 deletions(-) diff --git a/Dalamud/Game/Config/SystemConfigOption.cs b/Dalamud/Game/Config/SystemConfigOption.cs index f7e3bd3f8..154992637 100644 --- a/Dalamud/Game/Config/SystemConfigOption.cs +++ b/Dalamud/Game/Config/SystemConfigOption.cs @@ -597,6 +597,20 @@ public enum SystemConfigOption [GameConfigOption("EnablePsFunction", ConfigType.UInt)] EnablePsFunction, + /// + /// System option with the internal name ActiveInstanceGuid. + /// This option is a String. + /// + [GameConfigOption("ActiveInstanceGuid", ConfigType.String)] + ActiveInstanceGuid, + + /// + /// System option with the internal name ActiveProductGuid. + /// This option is a String. + /// + [GameConfigOption("ActiveProductGuid", ConfigType.String)] + ActiveProductGuid, + /// /// System option with the internal name WaterWet. /// This option is a UInt. @@ -996,6 +1010,27 @@ public enum SystemConfigOption [GameConfigOption("AutoChangeCameraMode", ConfigType.UInt)] AutoChangeCameraMode, + /// + /// System option with the internal name MsqProgress. + /// This option is a UInt. + /// + [GameConfigOption("MsqProgress", ConfigType.UInt)] + MsqProgress, + + /// + /// System option with the internal name PromptConfigUpdate. + /// This option is a UInt. + /// + [GameConfigOption("PromptConfigUpdate", ConfigType.UInt)] + PromptConfigUpdate, + + /// + /// System option with the internal name TitleScreenType. + /// This option is a UInt. + /// + [GameConfigOption("TitleScreenType", ConfigType.UInt)] + TitleScreenType, + /// /// System option with the internal name AccessibilitySoundVisualEnable. /// This option is a UInt. @@ -1059,6 +1094,13 @@ public enum SystemConfigOption [GameConfigOption("IdlingCameraAFK", ConfigType.UInt)] IdlingCameraAFK, + /// + /// System option with the internal name FirstConfigBackup. + /// This option is a UInt. + /// + [GameConfigOption("FirstConfigBackup", ConfigType.UInt)] + FirstConfigBackup, + /// /// System option with the internal name MouseSpeed. /// This option is a Float. @@ -1436,46 +1478,4 @@ public enum SystemConfigOption /// [GameConfigOption("PadButton_R3", ConfigType.String)] PadButton_R3, - - /// - /// System option with the internal name ActiveInstanceGuid. - /// This option is a String. - /// - [GameConfigOption("ActiveInstanceGuid", ConfigType.String)] - ActiveInstanceGuid, - - /// - /// System option with the internal name ActiveProductGuid. - /// This option is a String. - /// - [GameConfigOption("ActiveProductGuid", ConfigType.String)] - ActiveProductGuid, - - /// - /// System option with the internal name MsqProgress. - /// This option is a UInt. - /// - [GameConfigOption("MsqProgress", ConfigType.UInt)] - MsqProgress, - - /// - /// System option with the internal name PromptConfigUpdate. - /// This option is a UInt. - /// - [GameConfigOption("PromptConfigUpdate", ConfigType.UInt)] - PromptConfigUpdate, - - /// - /// System option with the internal name TitleScreenType. - /// This option is a UInt. - /// - [GameConfigOption("TitleScreenType", ConfigType.UInt)] - TitleScreenType, - - /// - /// System option with the internal name FirstConfigBackup. - /// This option is a UInt. - /// - [GameConfigOption("FirstConfigBackup", ConfigType.UInt)] - FirstConfigBackup, } diff --git a/Dalamud/Game/Config/UiConfigOption.cs b/Dalamud/Game/Config/UiConfigOption.cs index 53e64c89f..1a59b8945 100644 --- a/Dalamud/Game/Config/UiConfigOption.cs +++ b/Dalamud/Game/Config/UiConfigOption.cs @@ -37,6 +37,13 @@ public enum UiConfigOption [GameConfigOption("BattleEffectPvPEnemyPc", ConfigType.UInt)] BattleEffectPvPEnemyPc, + /// + /// UiConfig option with the internal name PadMode. + /// This option is a UInt. + /// + [GameConfigOption("PadMode", ConfigType.UInt)] + PadMode, + /// /// UiConfig option with the internal name WeaponAutoPutAway. /// This option is a UInt. @@ -114,14 +121,6 @@ public enum UiConfigOption [GameConfigOption("LockonDefaultZoom", ConfigType.Float)] LockonDefaultZoom, - /// - /// UiConfig option with the internal name LockonDefaultZoom_179. - /// This option is a Float. - /// - [Obsolete("This option won't work. Use LockonDefaultZoom.", true)] - [GameConfigOption("LockonDefaultZoom_179", ConfigType.Float)] - LockonDefaultZoom_179, - /// /// UiConfig option with the internal name CameraProductionOfAction. /// This option is a UInt. @@ -311,6 +310,27 @@ public enum UiConfigOption [GameConfigOption("RightClickExclusionMinion", ConfigType.UInt)] RightClickExclusionMinion, + /// + /// UiConfig option with the internal name EnableMoveTiltCharacter. + /// This option is a UInt. + /// + [GameConfigOption("EnableMoveTiltCharacter", ConfigType.UInt)] + EnableMoveTiltCharacter, + + /// + /// UiConfig option with the internal name EnableMoveTiltMountGround. + /// This option is a UInt. + /// + [GameConfigOption("EnableMoveTiltMountGround", ConfigType.UInt)] + EnableMoveTiltMountGround, + + /// + /// UiConfig option with the internal name EnableMoveTiltMountFly. + /// This option is a UInt. + /// + [GameConfigOption("EnableMoveTiltMountFly", ConfigType.UInt)] + EnableMoveTiltMountFly, + /// /// UiConfig option with the internal name TurnSpeed. /// This option is a UInt. @@ -1130,6 +1150,27 @@ public enum UiConfigOption [GameConfigOption("HotbarXHBEditEnable", ConfigType.UInt)] HotbarXHBEditEnable, + /// + /// UiConfig option with the internal name HotbarContentsAction2ReverseOperation. + /// This option is a UInt. + /// + [GameConfigOption("HotbarContentsAction2ReverseOperation", ConfigType.UInt)] + HotbarContentsAction2ReverseOperation, + + /// + /// UiConfig option with the internal name HotbarContentsAction2ReturnInitialSlot. + /// This option is a UInt. + /// + [GameConfigOption("HotbarContentsAction2ReturnInitialSlot", ConfigType.UInt)] + HotbarContentsAction2ReturnInitialSlot, + + /// + /// UiConfig option with the internal name HotbarContentsAction2ReverseRotate. + /// This option is a UInt. + /// + [GameConfigOption("HotbarContentsAction2ReverseRotate", ConfigType.UInt)] + HotbarContentsAction2ReverseRotate, + /// /// UiConfig option with the internal name PlateType. /// This option is a UInt. @@ -3572,32 +3613,4 @@ public enum UiConfigOption /// [GameConfigOption("PvPFrontlinesGCFree", ConfigType.UInt)] PvPFrontlinesGCFree, - - /// - /// UiConfig option with the internal name PadMode. - /// This option is a UInt. - /// - [GameConfigOption("PadMode", ConfigType.UInt)] - PadMode, - - /// - /// UiConfig option with the internal name EnableMoveTiltCharacter. - /// This option is a UInt. - /// - [GameConfigOption("EnableMoveTiltCharacter", ConfigType.UInt)] - EnableMoveTiltCharacter, - - /// - /// UiConfig option with the internal name EnableMoveTiltMountGround. - /// This option is a UInt. - /// - [GameConfigOption("EnableMoveTiltMountGround", ConfigType.UInt)] - EnableMoveTiltMountGround, - - /// - /// UiConfig option with the internal name EnableMoveTiltMountFly. - /// This option is a UInt. - /// - [GameConfigOption("EnableMoveTiltMountFly", ConfigType.UInt)] - EnableMoveTiltMountFly, } From fe5ce40a973677a914d6ccb86c9b85d8975438a5 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Tue, 25 Mar 2025 17:38:21 +0100 Subject: [PATCH 47/55] Fix ISeStringEvaluator service not resolving (#2204) --- Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs b/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs index d9b2b2e01..d0b182f04 100644 --- a/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs +++ b/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs @@ -11,6 +11,8 @@ using Dalamud.Game.Config; using Dalamud.Game.Text.Evaluator.Internal; using Dalamud.Game.Text.Noun; using Dalamud.Game.Text.Noun.Enums; +using Dalamud.IoC; +using Dalamud.IoC.Internal; using Dalamud.Logging.Internal; using Dalamud.Plugin.Services; using Dalamud.Utility; @@ -41,7 +43,9 @@ namespace Dalamud.Game.Text.Evaluator; /// /// Evaluator for SeStrings. /// +[PluginInterface] [ServiceManager.EarlyLoadedService] +[ResolveVia] internal class SeStringEvaluator : IServiceType, ISeStringEvaluator { private static readonly ModuleLog Log = new("SeStringEvaluator"); From abb5cf5dd1034a8f140d19fa1d2aab5e1fa29bbe Mon Sep 17 00:00:00 2001 From: marzent Date: Tue, 25 Mar 2025 18:43:18 +0100 Subject: [PATCH 48/55] fix wine_get_host_version calling convention (#2206) --- Dalamud.Injector/EntryPoint.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud.Injector/EntryPoint.cs b/Dalamud.Injector/EntryPoint.cs index 71c919179..f927cb164 100644 --- a/Dalamud.Injector/EntryPoint.cs +++ b/Dalamud.Injector/EntryPoint.cs @@ -277,7 +277,7 @@ namespace Dalamud.Injector { if (wineGetHostVersionPtr == nint.Zero) return null; - var methodDelegate = (delegate* unmanaged[Fastcall])wineGetHostVersionPtr; + var methodDelegate = (delegate* unmanaged[Cdecl])wineGetHostVersionPtr; methodDelegate(out var platformPtr, out var _); if (platformPtr == null) return null; From dc30651fb67335cb030f2d7879fc5de02d8f5d18 Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Tue, 25 Mar 2025 13:02:20 -0700 Subject: [PATCH 49/55] deps: bump cs --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index 794671e6c..937d4fd01 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 794671e6c66d12c975dc67cf95691c52bc5f6feb +Subproject commit 937d4fd012ad378132f26b09690e685b1096dfc4 From cba1a7d18cf68edd6be62a08c7802f18da459edb Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Tue, 25 Mar 2025 16:17:21 -0700 Subject: [PATCH 50/55] fix: AddonLifecycle sig, cs bump --- .../Addon/Lifecycle/AddonLifecycleAddressResolver.cs | 10 +++++----- lib/FFXIVClientStructs | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs index baf8bb86c..854d666fd 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs @@ -13,19 +13,19 @@ internal unsafe class AddonLifecycleAddressResolver : BaseAddressResolver /// This is called for a majority of all addon OnSetup's. /// public nint AddonSetup { get; private set; } - + /// /// Gets the address of the other addon setup hook invoked by the AtkUnitManager. /// There are two callsites for this vFunc, we need to hook both of them to catch both normal UI and special UI cases like dialogue. /// This seems to be called rarely for specific addons. /// public nint AddonSetup2 { get; private set; } - + /// /// Gets the address of the addon finalize hook invoked by the AtkUnitManager. /// public nint AddonFinalize { get; private set; } - + /// /// Gets the address of the addon draw hook invoked by virtual function call. /// @@ -35,7 +35,7 @@ internal unsafe class AddonLifecycleAddressResolver : BaseAddressResolver /// Gets the address of the addon update hook invoked by virtual function call. /// public nint AddonUpdate { get; private set; } - + /// /// Gets the address of the addon onRequestedUpdate hook invoked by virtual function call. /// @@ -51,6 +51,6 @@ internal unsafe class AddonLifecycleAddressResolver : BaseAddressResolver this.AddonFinalize = sig.ScanText("E8 ?? ?? ?? ?? 48 83 EF 01 75 D5"); this.AddonDraw = sig.ScanText("FF 90 ?? ?? ?? ?? 83 EB 01 79 C4 48 81 EF ?? ?? ?? ?? 48 83 ED 01"); this.AddonUpdate = sig.ScanText("FF 90 ?? ?? ?? ?? 40 88 AF ?? ?? ?? ?? 45 33 D2"); - this.AddonOnRequestedUpdate = sig.ScanText("FF 90 98 01 00 00 48 8B 5C 24 30 48 83 C4 20"); + this.AddonOnRequestedUpdate = sig.ScanText("FF 90 A0 01 00 00 48 8B 5C 24 30"); } } diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index 937d4fd01..c3a15633b 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 937d4fd012ad378132f26b09690e685b1096dfc4 +Subproject commit c3a15633bbfa7e03831129d1bf0ef4cbf956bf58 From 2f029567e4beaee24240a5fadcb5d933eefb7ea7 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Wed, 26 Mar 2025 01:32:24 +0100 Subject: [PATCH 51/55] Fixing colors and NounResolver (#2208) * Fixing colors and NounResolver * Remove failing special case NounProcessor selftest --- Dalamud/Game/Text/Evaluator/Internal/SheetRedirectResolver.cs | 2 +- Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs | 4 ++-- .../Internal/Windows/Data/Widgets/InventoryWidget.cs | 2 +- .../Windows/SelfTest/AgingSteps/NounProcessorAgingStep.cs | 2 -- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/Dalamud/Game/Text/Evaluator/Internal/SheetRedirectResolver.cs b/Dalamud/Game/Text/Evaluator/Internal/SheetRedirectResolver.cs index 1be0f8866..f851e7686 100644 --- a/Dalamud/Game/Text/Evaluator/Internal/SheetRedirectResolver.cs +++ b/Dalamud/Game/Text/Evaluator/Internal/SheetRedirectResolver.cs @@ -193,7 +193,7 @@ internal class SheetRedirectResolver : IServiceType colIndex = 43; if (this.dataManager.GetExcelSheet().TryGetRow(rowId, out var row)) - rowId = row.SortKey; + rowId = row.ContentFinderCondition.RowId; break; } diff --git a/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs b/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs index d0b182f04..83f8e241a 100644 --- a/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs +++ b/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs @@ -1599,7 +1599,7 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator if (eColorTypeVal == 0) context.Builder.PopColor(); else if (this.dataManager.GetExcelSheet().TryGetRow(eColorTypeVal, out var row)) - context.Builder.PushColorBgra((row.Light >> 8) | (row.Light << 24)); + context.Builder.PushColorBgra((row.Dark >> 8) | (row.Dark << 24)); return true; } @@ -1613,7 +1613,7 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator if (eColorTypeVal == 0) context.Builder.PopEdgeColor(); else if (this.dataManager.GetExcelSheet().TryGetRow(eColorTypeVal, out var row)) - context.Builder.PushEdgeColorBgra((row.Light >> 8) | (row.Light << 24)); + context.Builder.PushEdgeColorBgra((row.Dark >> 8) | (row.Dark << 24)); return true; } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/InventoryWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/InventoryWidget.cs index f455f2e66..d532c2cdc 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/InventoryWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/InventoryWidget.cs @@ -380,7 +380,7 @@ internal class InventoryWidget : IDataWindowWidget var rowId = this.GetItemRarityColorType(item, isEdgeColor); return this.dataManager.Excel.GetSheet().TryGetRow(rowId, out var color) - ? BinaryPrimitives.ReverseEndianness(color.Light) | 0xFF000000 + ? BinaryPrimitives.ReverseEndianness(color.Dark) | 0xFF000000 : 0xFFFFFFFF; } diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/NounProcessorAgingStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/NounProcessorAgingStep.cs index 4073616b2..4bea503d9 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/NounProcessorAgingStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/NounProcessorAgingStep.cs @@ -191,8 +191,6 @@ internal class NounProcessorAgingStep : IAgingStep new(nameof(LSheets.Item), 44348, ClientLanguage.French, 2, (int)FrenchArticleType.PossessiveFirstPerson, 1, "mes mémoquartz inhabituels fantasmagoriques"), new(nameof(LSheets.Item), 44348, ClientLanguage.French, 2, (int)FrenchArticleType.PossessiveSecondPerson, 1, "tes mémoquartz inhabituels fantasmagoriques"), new(nameof(LSheets.Item), 44348, ClientLanguage.French, 2, (int)FrenchArticleType.PossessiveThirdPerson, 1, "ses mémoquartz inhabituels fantasmagoriques"), - - new(nameof(LSheets.Action), 45, ClientLanguage.German, 1, (int)FrenchArticleType.Indefinite, 1, "Blumenflüsterer IV"), ]; private enum GermanCases From 43e380493f70fb60da08f6fb63a0d1d9ac5b635e Mon Sep 17 00:00:00 2001 From: Aireil <33433913+Aireil@users.noreply.github.com> Date: Wed, 26 Mar 2025 04:25:10 +0100 Subject: [PATCH 52/55] Fix FlyTextKind enum (#2207) * Fix FlyTextKind enum * Remove the experimental tags --- Dalamud/Game/Gui/FlyText/FlyTextKind.cs | 96 +++++++++++++++---------- 1 file changed, 57 insertions(+), 39 deletions(-) diff --git a/Dalamud/Game/Gui/FlyText/FlyTextKind.cs b/Dalamud/Game/Gui/FlyText/FlyTextKind.cs index 3727fd0f8..0edbd09ee 100644 --- a/Dalamud/Game/Gui/FlyText/FlyTextKind.cs +++ b/Dalamud/Game/Gui/FlyText/FlyTextKind.cs @@ -92,214 +92,232 @@ public enum FlyTextKind : int /// IslandExp = 15, + /// + /// Val1 in serif font next to all caps condensed font Text1 with Text2 in sans-serif as subtitle. + /// Added in 7.2, usage currently unknown. + /// + Unknown16 = 16, + + /// + /// Val1 in serif font, Text2 in sans-serif as subtitle. + /// Added in 7.2, usage currently unknown. + /// + Unknown17 = 17, + + /// + /// Val1 in serif font, Text2 in sans-serif as subtitle. + /// Added in 7.2, usage currently unknown. + /// + Unknown18 = 18, + /// /// Sans-serif Text1 next to serif Val1 with all caps condensed font MP with Text2 in sans-serif as subtitle. /// - MpDrain = 16, + MpDrain = 19, /// /// Currently not used by the game. /// Sans-serif Text1 next to serif Val1 with all caps condensed font TP with Text2 in sans-serif as subtitle. /// - NamedTp = 17, + NamedTp = 20, /// /// Val1 in serif font, Text2 in sans-serif as subtitle with sans-serif Text1 to the left of the Val1. /// - Healing = 18, + Healing = 21, /// /// Sans-serif Text1 next to serif Val1 with all caps condensed font MP with Text2 in sans-serif as subtitle. /// - MpRegen = 19, + MpRegen = 22, /// /// Currently not used by the game. /// Sans-serif Text1 next to serif Val1 with all caps condensed font TP with Text2 in sans-serif as subtitle. /// - NamedTp2 = 20, + NamedTp2 = 23, /// /// Sans-serif Text1 next to serif Val1 with all caps condensed font EP with Text2 in sans-serif as subtitle. /// - EpRegen = 21, + EpRegen = 24, /// /// Sans-serif Text1 next to serif Val1 with all caps condensed font CP with Text2 in sans-serif as subtitle. /// - CpRegen = 22, + CpRegen = 25, /// /// Sans-serif Text1 next to serif Val1 with all caps condensed font GP with Text2 in sans-serif as subtitle. /// - GpRegen = 23, + GpRegen = 26, /// /// Displays nothing. /// - None = 24, + None = 27, /// /// All caps serif INVULNERABLE. /// - Invulnerable = 25, + Invulnerable = 28, /// /// All caps sans-serif condensed font INTERRUPTED! /// Does a large bounce effect on appearance. /// Does not scroll up or down the screen. /// - Interrupted = 26, + Interrupted = 29, /// /// Val1 in serif font. /// - CraftingProgress = 27, + CraftingProgress = 30, /// /// Val1 in serif font. /// - CraftingQuality = 28, + CraftingQuality = 31, /// /// Val1 in larger serif font with exclamation, with Text2 in sans-serif as subtitle. Does a bigger bounce effect on appearance. /// - CraftingQualityCrit = 29, + CraftingQualityCrit = 32, /// /// Currently not used by the game. /// Val1 in serif font. /// - AutoAttackNoText3 = 30, + AutoAttackNoText3 = 33, /// /// CriticalHit with sans-serif Text1 to the left of the Val1 (2). /// - HealingCrit = 31, + HealingCrit = 34, /// /// Currently not used by the game. /// Same as DamageCrit with a MP in condensed font to the right of Val1. /// Does a jiggle effect to the right on appearance. /// - NamedCriticalHitWithMp = 32, + NamedCriticalHitWithMp = 35, /// /// Currently not used by the game. /// Same as DamageCrit with a TP in condensed font to the right of Val1. /// Does a jiggle effect to the right on appearance. /// - NamedCriticalHitWithTp = 33, + NamedCriticalHitWithTp = 36, /// /// Icon next to sans-serif Text1 with sans-serif "has no effect!" to the right. /// - DebuffNoEffect = 34, + DebuffNoEffect = 37, /// /// Icon next to sans-serif slightly faded Text1. /// - BuffFading = 35, + BuffFading = 38, /// /// Icon next to sans-serif slightly faded Text1. /// - DebuffFading = 36, + DebuffFading = 39, /// /// Text1 in sans-serif font. /// - Named = 37, + Named = 40, /// /// Icon next to sans-serif Text1 with sans-serif "(fully resisted)" to the right. /// - DebuffResisted = 38, + DebuffResisted = 41, /// /// All caps serif 'INCAPACITATED!'. /// - Incapacitated = 39, + Incapacitated = 42, /// /// Text1 with sans-serif "(fully resisted)" to the right. /// - FullyResisted = 40, + FullyResisted = 43, /// /// Text1 with sans-serif "has no effect!" to the right. /// - HasNoEffect = 41, + HasNoEffect = 44, /// /// Val1 in serif font, Text2 in sans-serif as subtitle with sans-serif Text1 to the left of the Val1. /// - HpDrain = 42, + HpDrain = 45, /// /// Currently not used by the game. /// Sans-serif Text1 next to serif Val1 with all caps condensed font MP with Text2 in sans-serif as subtitle. /// - NamedMp3 = 43, + NamedMp3 = 46, /// /// Currently not used by the game. /// Sans-serif Text1 next to serif Val1 with all caps condensed font TP with Text2 in sans-serif as subtitle. /// - NamedTp3 = 44, + NamedTp3 = 47, /// /// Icon next to sans-serif Text1 with serif "INVULNERABLE!" beneath the Text1. /// - DebuffInvulnerable = 45, + DebuffInvulnerable = 48, /// /// All caps serif RESIST. /// - Resist = 46, + Resist = 49, /// /// Icon with an item icon outline next to sans-serif Text1. /// - LootedItem = 47, + LootedItem = 50, /// /// Val1 in serif font. /// - Collectability = 48, + Collectability = 51, /// /// Val1 in larger serif font with exclamation, with Text2 in sans-serif as subtitle. /// Does a bigger bounce effect on appearance. /// - CollectabilityCrit = 49, + CollectabilityCrit = 52, /// /// All caps serif REFLECT. /// - Reflect = 50, + Reflect = 53, /// /// All caps serif REFLECTED. /// - Reflected = 51, + Reflected = 54, /// /// Val1 in serif font, Text2 in sans-serif as subtitle. /// Does a bounce effect on appearance. /// - CraftingQualityDh = 52, + CraftingQualityDh = 55, /// /// Currently not used by the game. /// Val1 in larger serif font with exclamation, with Text2 in sans-serif as subtitle. /// Does a bigger bounce effect on appearance. /// - CriticalHit4 = 53, + CriticalHit4 = 56, /// /// Val1 in even larger serif font with 2 exclamations, Text2 in sans-serif as subtitle. /// Does a large bounce effect on appearance. Does not scroll up or down the screen. /// - CraftingQualityCritDh = 54, + CraftingQualityCritDh = 57, } From 494291511ca7d226fc7c76a9e5a5984a390bcb30 Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Wed, 26 Mar 2025 09:52:07 -0700 Subject: [PATCH 53/55] *CS bumping noises* --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index c3a15633b..5279df085 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit c3a15633bbfa7e03831129d1bf0ef4cbf956bf58 +Subproject commit 5279df085ea13df1743aa99684ee3eb67583500c From 81bc0012ed3d0b438c3a28e34b578a6f9a3d77c7 Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Wed, 26 Mar 2025 12:12:36 -0700 Subject: [PATCH 54/55] more cs bumpies uwu - boing - boing - boing --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index 5279df085..dd25bdcd5 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 5279df085ea13df1743aa99684ee3eb67583500c +Subproject commit dd25bdcd57d0eccf5d0615e9c052192a788eca75 From 8320a824cefb0ac18964d61d3b603f72c8a95227 Mon Sep 17 00:00:00 2001 From: marzent Date: Wed, 26 Mar 2025 20:38:27 +0100 Subject: [PATCH 55/55] return an error HRESULT when dotnet runtime can't be found (#2209) --- lib/CoreCLR/boot.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/CoreCLR/boot.cpp b/lib/CoreCLR/boot.cpp index f5e626c3f..84d3d15cf 100644 --- a/lib/CoreCLR/boot.cpp +++ b/lib/CoreCLR/boot.cpp @@ -126,7 +126,7 @@ HRESULT InitializeClrAndGetEntryPoint( if (!dotnet_path || !std::filesystem::exists(dotnet_path)) { logging::E("Error: Unable to find .NET runtime path"); - return 1; + return HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND); } // =========================================================================== //