diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f1267baca..be44afacc 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -23,7 +23,7 @@ jobs: uses: microsoft/setup-msbuild@v1.0.2 - uses: actions/setup-dotnet@v3 with: - dotnet-version: '8.0.100' + dotnet-version: '9.0.200' - name: Define VERSION run: | $env:COMMIT = $env:GITHUB_SHA.Substring(0, 7) diff --git a/Dalamud.Boot/DalamudStartInfo.cpp b/Dalamud.Boot/DalamudStartInfo.cpp index 985332966..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); @@ -123,6 +124,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 +150,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..64450e290 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, @@ -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; @@ -61,6 +62,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..68f04b84b 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, + false, // !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.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.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. 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.Common/DalamudStartInfo.cs b/Dalamud.Common/DalamudStartInfo.cs index c3cc33a12..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. /// @@ -120,10 +127,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.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.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.CorePlugin/Dalamud.CorePlugin.csproj b/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj index ca78f09ad..f2c8cbc5a 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.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/Dalamud.Injector.csproj b/Dalamud.Injector/Dalamud.Injector.csproj index 9cc063916..c8b648f6d 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.Injector/EntryPoint.cs b/Dalamud.Injector/EntryPoint.cs index 6507436c2..f927cb164 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; @@ -94,6 +96,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"); @@ -262,6 +265,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[Cdecl])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; @@ -277,9 +309,14 @@ 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.\"}"; + // 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=")) @@ -306,6 +343,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..])); @@ -377,11 +418,37 @@ namespace Dalamud.Injector throw new CommandLineException($"\"{languageStr}\" is not a valid supported language."); } + OSPlatform platform; + + // covers both win32 and Windows + if (platformStr[0..(len = Math.Min(platformStr.Length, (key = "win").Length))] == key[0..len]) + { + 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; @@ -399,6 +466,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() { @@ -463,13 +531,14 @@ 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)]"); 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]"); @@ -729,15 +798,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; } @@ -792,20 +888,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.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.sln b/Dalamud.sln index 5b6f56c6e..5b1eb9d30 100644 --- a/Dalamud.sln +++ b/Dalamud.sln @@ -74,8 +74,10 @@ 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 + {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 @@ -124,14 +126,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 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/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/Dalamud.csproj b/Dalamud/Dalamud.csproj index b24eb07ed..9f3a9bb4a 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -1,16 +1,12 @@ - net8.0-windows - x64 - x64 - 12.0 True XIV Launcher addon framework - 11.0.8.0 + 12.0.0.0 $(DalamudVersion) $(DalamudVersion) $(DalamudVersion) @@ -94,6 +90,7 @@ + diff --git a/Dalamud/EntryPoint.cs b/Dalamud/EntryPoint.cs index 6d5c219dd..f6ba990e6 100644 --- a/Dalamud/EntryPoint.cs +++ b/Dalamud/EntryPoint.cs @@ -178,20 +178,18 @@ 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()) + // Currently VEH is not fully functional on WINE + if (info.Platform != OSPlatform.Windows) 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(); 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/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/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/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/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/DeliriumStep.cs b/Dalamud/Game/ClientState/JobGauge/Enums/DeliriumStep.cs new file mode 100644 index 000000000..d2a41b93c --- /dev/null +++ b/Dalamud/Game/ClientState/JobGauge/Enums/DeliriumStep.cs @@ -0,0 +1,22 @@ +namespace Dalamud.Game.ClientState.JobGauge.Enums; + +/// +/// Enum representing the current step of Delirium. +/// +public enum DeliriumStep +{ + /// + /// Scarlet Delirium can be used. + /// + ScarletDelirium = 0, + + /// + /// Comeuppance can be used. + /// + Comeuppance = 1, + + /// + /// Torcleaver can be used. + /// + Torcleaver = 2, +} 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/SummonAttunement.cs b/Dalamud/Game/ClientState/JobGauge/Enums/SummonAttunement.cs new file mode 100644 index 000000000..a07c0d04f --- /dev/null +++ b/Dalamud/Game/ClientState/JobGauge/Enums/SummonAttunement.cs @@ -0,0 +1,30 @@ +namespace Dalamud.Game.ClientState.JobGauge.Enums; + +/// +/// Enum representing the current attunement of a summoner. +/// +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/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/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; } 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; } diff --git a/Dalamud/Game/ClientState/JobGauge/Types/SMNGauge.cs b/Dalamud/Game/ClientState/JobGauge/Types/SMNGauge.cs index 81be0e762..899ea78eb 100644 --- a/Dalamud/Game/ClientState/JobGauge/Types/SMNGauge.cs +++ b/Dalamud/Game/ClientState/JobGauge/Types/SMNGauge.cs @@ -25,7 +25,13 @@ public unsafe class SMNGauge : JobGaugeBase /// /// Gets the time remaining for the current attunement. /// - public ushort AttunmentTimerRemaining => this.Struct->AttunementTimer; + [Obsolete("Typo fixed. Use AttunementTimerRemaining instead.", true)] + public ushort AttunmentTimerRemaining => this.AttunementTimerRemaining; + + /// + /// Gets the time remaining for the current attunement. + /// + public ushort AttunementTimerRemaining => this.Struct->AttunementTimer; /// /// Gets the summon that will return after the current summon expires. @@ -40,10 +46,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 +105,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. diff --git a/Dalamud/Game/ClientState/Objects/ObjectTable.cs b/Dalamud/Game/ClientState/Objects/ObjectTable.cs index 6aa7fd8ad..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) { @@ -66,7 +57,7 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable { get { - _ = this.WarnMultithreadedUsage(); + ThreadSafety.AssertMainThread(); return (nint)(&CSGameObjectManager.Instance()->Objects); } @@ -80,7 +71,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 +80,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 +97,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 +114,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 +122,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 +146,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 +198,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()) @@ -256,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; @@ -293,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() @@ -302,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() 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/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, } 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/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, } 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/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/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/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..f851e7686 --- /dev/null +++ b/Dalamud/Game/Text/Evaluator/Internal/SheetRedirectResolver.cs @@ -0,0 +1,233 @@ +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.ContentFinderCondition.RowId; + 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..83f8e241a --- /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.IoC; +using Dalamud.IoC.Internal; +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. +/// +[PluginInterface] +[ServiceManager.EarlyLoadedService] +[ResolveVia] +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.Dark >> 8) | (row.Dark << 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.Dark >> 8) | (row.Dark << 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; + + ThreadSafety.AssertMainThread("Global parameters may only be used from the main thread."); + + 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, p.StringValue.AsSpan(), 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/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/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/Payloads/UIForegroundPayload.cs b/Dalamud/Game/Text/SeStringHandling/Payloads/UIForegroundPayload.cs index 995c20211..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 { @@ -74,13 +75,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..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 { @@ -71,13 +72,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/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/Animation/Easing.cs b/Dalamud/Interface/Animation/Easing.cs index c6d6149af..8191487f4 100644 --- a/Dalamud/Interface/Animation/Easing.cs +++ b/Dalamud/Interface/Animation/Easing.cs @@ -8,7 +8,6 @@ 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 +45,22 @@ 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, following unclamped logic. /// - public double Value + [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 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. + /// 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/ImGuiNotification/Internal/ActiveNotification.ImGui.cs b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs index e8d6c5cc0..c672dd3b3 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 6587b6c32..c3135853d 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs @@ -200,7 +200,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/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/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/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/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/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/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/Dalamud/Interface/Internal/Windows/Data/Widgets/InventoryWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/InventoryWidget.cs index 5cefc0853..d532c2cdc 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()); @@ -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.Dark) | 0xFF000000 : 0xFFFFFFFF; } 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..92e57ddac --- /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.Value != null) + WidgetUtil.DrawCopyableText(item.StringValue.ToString()); + 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/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/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/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..4bea503d9 --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/NounProcessorAgingStep.cs @@ -0,0 +1,257 @@ +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"), + ]; + + 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/Internal/Windows/TitleScreenMenuWindow.cs b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs index c9ca65e0c..cbb6998ac 100644 --- a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs +++ b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs @@ -223,7 +223,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) @@ -270,7 +270,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.ValueClamped)) { var i = 0; foreach (var entry in entries) @@ -353,7 +353,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); @@ -403,7 +403,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) { @@ -430,7 +430,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.ValueClamped : 0f); } // Drop shadow @@ -480,7 +480,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) @@ -498,7 +498,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/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/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); } diff --git a/Dalamud/Plugin/PluginLoadReason.cs b/Dalamud/Plugin/PluginLoadReason.cs index d4c1a3b26..2b494c549 100644 --- a/Dalamud/Plugin/PluginLoadReason.cs +++ b/Dalamud/Plugin/PluginLoadReason.cs @@ -3,32 +3,31 @@ namespace Dalamud.Plugin; /// /// This enum reflects reasons for loading a plugin. /// +[Flags] 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, } - -// TODO(api9): This should be a mask, so that we can combine Installer | ProfileLoaded 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/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; + } +} diff --git a/Dalamud/Utility/CStringExtensions.cs b/Dalamud/Utility/CStringExtensions.cs new file mode 100644 index 000000000..83ebb186f --- /dev/null +++ b/Dalamud/Utility/CStringExtensions.cs @@ -0,0 +1,61 @@ +using Dalamud.Game.Text.SeStringHandling; + +using InteropGenerator.Runtime; + +using Lumina.Text.ReadOnly; + +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 +{ + /// + /// Convert a CStringPointer to a ReadOnlySeStringSpan. + /// + /// The pointer to convert. + /// A span. + 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 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 ReadOnlySeString. + public static ReadOnlySeString AsReadOnlySeString(this CStringPointer ptr) + { + return new ReadOnlySeString(ptr.AsSpan().ToArray()); + } + + /// + /// 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/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..32160aa15 --- /dev/null +++ b/Dalamud/Utility/ItemUtil.cs @@ -0,0 +1,157 @@ +using System.Runtime.CompilerServices; + +using Dalamud.Data; +using Dalamud.Game; +using Dalamud.Game.Text; +using Lumina.Excel.Sheets; +using Lumina.Text; +using Lumina.Text.ReadOnly; + +using static Dalamud.Game.Text.SeStringHandling.Payloads.ItemPayload; + +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 ItemKind.Hq: + builder.Append($" {(char)SeIconChar.HighQuality}"); + break; + case 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 057759e1e..904375250 100644 --- a/Dalamud/Utility/SeStringExtensions.cs +++ b/Dalamud/Utility/SeStringExtensions.cs @@ -1,3 +1,7 @@ +using System.Linq; + +using InteropGenerator.Runtime; + using Lumina.Text.Parse; using Lumina.Text.ReadOnly; @@ -38,24 +42,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 . @@ -92,4 +78,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; + } } 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!"); } } diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs index 7724c68e0..87cb86e1c 100644 --- a/Dalamud/Utility/Util.cs +++ b/Dalamud/Utility/Util.cs @@ -12,7 +12,6 @@ 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; @@ -500,55 +499,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/Directory.Build.props b/Directory.Build.props index 354dedd60..2905b80b1 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,8 +1,17 @@ + + + + net9.0-windows + x64 + x64 + 13.0 + + 5.6.1 - 7.1.3 + 7.2.0 13.0.3 diff --git a/build/build.csproj b/build/build.csproj index 219b668bd..37a4d3252 100644 --- a/build/build.csproj +++ b/build/build.csproj @@ -1,7 +1,6 @@  Exe - net8.0 disable IDE0002;IDE0051;IDE1006;CS0649;CS0169 @@ -12,5 +11,6 @@ + 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/lib/CoreCLR/boot.cpp b/lib/CoreCLR/boot.cpp index 54276aad1..84d3d15cf 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,9 +28,68 @@ 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, + bool enable_legacy_corrupted_state_exception_policy, std::wstring runtimeconfig_path, std::wstring module_path, std::wstring entrypoint_assembly_name, @@ -41,8 +101,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"); @@ -56,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 HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND); } // =========================================================================== // @@ -89,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), 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, diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index 2c3e84640..dd25bdcd5 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 2c3e84640af5220b78b944a06fdca79c52144075 +Subproject commit dd25bdcd57d0eccf5d0615e9c052192a788eca75 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 @@ - 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