diff --git a/Dalamud/Game/ActionKind.cs b/Dalamud/Game/ActionKind.cs
new file mode 100644
index 000000000..9a574f9a8
--- /dev/null
+++ b/Dalamud/Game/ActionKind.cs
@@ -0,0 +1,89 @@
+namespace Dalamud.Game;
+
+///
+/// Enum describing possible action kinds.
+///
+public enum ActionKind
+{
+ ///
+ /// A Trait.
+ ///
+ Trait = 0,
+
+ ///
+ /// An Action.
+ ///
+ Action = 1,
+
+ ///
+ /// A usable Item.
+ ///
+ Item = 2, // does not work?
+
+ ///
+ /// A usable EventItem.
+ ///
+ EventItem = 3, // does not work?
+
+ ///
+ /// An EventAction.
+ ///
+ EventAction = 4,
+
+ ///
+ /// A GeneralAction.
+ ///
+ GeneralAction = 5,
+
+ ///
+ /// A BuddyAction.
+ ///
+ BuddyAction = 6,
+
+ ///
+ /// A MainCommand.
+ ///
+ MainCommand = 7,
+
+ ///
+ /// A Companion.
+ ///
+ Companion = 8, // unresolved?!
+
+ ///
+ /// A CraftAction.
+ ///
+ CraftAction = 9,
+
+ ///
+ /// An Action (again).
+ ///
+ Action2 = 10, // what's the difference?
+
+ ///
+ /// A PetAction.
+ ///
+ PetAction = 11,
+
+ ///
+ /// A CompanyAction.
+ ///
+ CompanyAction = 12,
+
+ ///
+ /// A Mount.
+ ///
+ Mount = 13,
+
+ // 14-18 unused
+
+ ///
+ /// A BgcArmyAction.
+ ///
+ BgcArmyAction = 19,
+
+ ///
+ /// An Ornament.
+ ///
+ Ornament = 20,
+}
diff --git a/Dalamud/Game/Gui/GameGui.cs b/Dalamud/Game/Gui/GameGui.cs
index 7cd6d7360..1041464a7 100644
--- a/Dalamud/Game/Gui/GameGui.cs
+++ b/Dalamud/Game/Gui/GameGui.cs
@@ -323,7 +323,7 @@ internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui
return ret;
}
- private void HandleActionHoverDetour(AgentActionDetail* hoverState, ActionKind actionKind, uint actionId, int a4, byte a5)
+ private void HandleActionHoverDetour(AgentActionDetail* hoverState, FFXIVClientStructs.FFXIV.Client.UI.Agent.ActionKind actionKind, uint actionId, int a4, byte a5)
{
this.handleActionHoverHook.Original(hoverState, actionKind, actionId, a4, a5);
this.HoveredAction.ActionKind = (HoverActionKind)actionKind;
diff --git a/Dalamud/Game/Text/Evaluator/Internal/SeStringBuilderIconWrap.cs b/Dalamud/Game/Text/Evaluator/Internal/SeStringBuilderIconWrap.cs
new file mode 100644
index 000000000..65567d240
--- /dev/null
+++ b/Dalamud/Game/Text/Evaluator/Internal/SeStringBuilderIconWrap.cs
@@ -0,0 +1,30 @@
+using Lumina.Text;
+
+namespace Dalamud.Game.Text.Evaluator.Internal;
+
+///
+/// Wraps payloads in an open and close icon, for example the Auto Translation open/close brackets.
+///
+internal readonly struct SeStringBuilderIconWrap : IDisposable
+{
+ private readonly SeStringBuilder builder;
+ private readonly uint iconClose;
+
+ ///
+ /// Initializes a new instance of the struct.
+ /// Appends an icon macro with on creation, and an icon macro with
+ /// on disposal.
+ ///
+ /// The builder to use.
+ /// The open icon id.
+ /// The close icon id.
+ public SeStringBuilderIconWrap(SeStringBuilder builder, uint iconOpen, uint iconClose)
+ {
+ this.builder = builder;
+ this.iconClose = iconClose;
+ this.builder.AppendIcon(iconOpen);
+ }
+
+ ///
+ public void Dispose() => this.builder.AppendIcon(this.iconClose);
+}
diff --git a/Dalamud/Game/Text/Evaluator/Internal/SeStringContext.cs b/Dalamud/Game/Text/Evaluator/Internal/SeStringContext.cs
new file mode 100644
index 000000000..a32702f6c
--- /dev/null
+++ b/Dalamud/Game/Text/Evaluator/Internal/SeStringContext.cs
@@ -0,0 +1,83 @@
+using System.Globalization;
+
+using Dalamud.Utility;
+
+using Lumina.Text;
+using Lumina.Text.ReadOnly;
+
+namespace Dalamud.Game.Text.Evaluator.Internal;
+
+///
+/// A context wrapper used in .
+///
+internal readonly ref struct SeStringContext
+{
+ ///
+ /// The to append text and macros to.
+ ///
+ internal readonly SeStringBuilder Builder;
+
+ ///
+ /// A list of local parameters.
+ ///
+ internal readonly Span LocalParameters;
+
+ ///
+ /// The target language, used for sheet lookups.
+ ///
+ internal readonly ClientLanguage Language;
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The to append text and macros to.
+ /// A list of local parameters.
+ /// The target language, used for sheet lookups.
+ internal SeStringContext(SeStringBuilder builder, Span localParameters, ClientLanguage language)
+ {
+ this.Builder = builder;
+ this.LocalParameters = localParameters;
+ this.Language = language;
+ }
+
+ ///
+ /// Gets the of the current target .
+ ///
+ internal CultureInfo CultureInfo => Localization.GetCultureInfoFromLangCode(this.Language.ToCode());
+
+ ///
+ /// Tries to get a number from the local parameters at the specified index.
+ ///
+ /// The index in the list.
+ /// The local parameter number.
+ /// true if the local parameters list contained a parameter at given index, false otherwise.
+ internal bool TryGetLNum(int index, out uint value)
+ {
+ if (index >= 0 && this.LocalParameters.Length > index)
+ {
+ value = this.LocalParameters[index].UIntValue;
+ return true;
+ }
+
+ value = 0;
+ return false;
+ }
+
+ ///
+ /// Tries to get a string from the local parameters at the specified index.
+ ///
+ /// The index in the list.
+ /// The local parameter string.
+ /// true if the local parameters list contained a parameter at given index, false otherwise.
+ internal bool TryGetLStr(int index, out ReadOnlySeString value)
+ {
+ if (index >= 0 && this.LocalParameters.Length > index)
+ {
+ value = this.LocalParameters[index].StringValue;
+ return true;
+ }
+
+ value = default;
+ return false;
+ }
+}
diff --git a/Dalamud/Game/Text/Evaluator/Internal/SheetRedirectFlags.cs b/Dalamud/Game/Text/Evaluator/Internal/SheetRedirectFlags.cs
new file mode 100644
index 000000000..1c1171873
--- /dev/null
+++ b/Dalamud/Game/Text/Evaluator/Internal/SheetRedirectFlags.cs
@@ -0,0 +1,49 @@
+namespace Dalamud.Game.Text.Evaluator.Internal;
+
+///
+/// An enum providing additional information about the sheet redirect.
+///
+[Flags]
+internal enum SheetRedirectFlags
+{
+ ///
+ /// No flags.
+ ///
+ None = 0,
+
+ ///
+ /// Resolved to a sheet related with items.
+ ///
+ Item = 1,
+
+ ///
+ /// Resolved to the EventItem sheet.
+ ///
+ EventItem = 2,
+
+ ///
+ /// Resolved to a high quality item.
+ ///
+ ///
+ /// Append Addon#9.
+ ///
+ HighQuality = 4,
+
+ ///
+ /// Resolved to a collectible item.
+ ///
+ ///
+ /// Append Addon#150.
+ ///
+ Collectible = 8,
+
+ ///
+ /// Resolved to a sheet related with actions.
+ ///
+ Action = 16,
+
+ ///
+ /// Resolved to the Action sheet.
+ ///
+ ActionSheet = 32,
+}
diff --git a/Dalamud/Game/Text/Evaluator/Internal/SheetRedirectResolver.cs b/Dalamud/Game/Text/Evaluator/Internal/SheetRedirectResolver.cs
new file mode 100644
index 000000000..57a58c80d
--- /dev/null
+++ b/Dalamud/Game/Text/Evaluator/Internal/SheetRedirectResolver.cs
@@ -0,0 +1,232 @@
+using Dalamud.Data;
+using Dalamud.Utility;
+
+using Lumina.Extensions;
+
+using ItemKind = Dalamud.Game.Text.SeStringHandling.Payloads.ItemPayload.ItemKind;
+using LSheets = Lumina.Excel.Sheets;
+
+namespace Dalamud.Game.Text.Evaluator.Internal;
+
+///
+/// A service to resolve sheet redirects in expressions.
+///
+[ServiceManager.EarlyLoadedService]
+internal class SheetRedirectResolver : IServiceType
+{
+ private static readonly (string SheetName, uint ColumnIndex, bool ReturnActionSheetFlag)[] ActStrSheets =
+ [
+ (nameof(LSheets.Trait), 0, false),
+ (nameof(LSheets.Action), 0, true),
+ (nameof(LSheets.Item), 0, false),
+ (nameof(LSheets.EventItem), 0, false),
+ (nameof(LSheets.EventAction), 0, false),
+ (nameof(LSheets.GeneralAction), 0, false),
+ (nameof(LSheets.BuddyAction), 0, false),
+ (nameof(LSheets.MainCommand), 5, false),
+ (nameof(LSheets.Companion), 0, false),
+ (nameof(LSheets.CraftAction), 0, false),
+ (nameof(LSheets.Action), 0, true),
+ (nameof(LSheets.PetAction), 0, false),
+ (nameof(LSheets.CompanyAction), 0, false),
+ (nameof(LSheets.Mount), 0, false),
+ (string.Empty, 0, false),
+ (string.Empty, 0, false),
+ (string.Empty, 0, false),
+ (string.Empty, 0, false),
+ (string.Empty, 0, false),
+ (nameof(LSheets.BgcArmyAction), 1, false),
+ (nameof(LSheets.Ornament), 8, false),
+ ];
+
+ private static readonly string[] ObjStrSheetNames =
+ [
+ nameof(LSheets.BNpcName),
+ nameof(LSheets.ENpcResident),
+ nameof(LSheets.Treasure),
+ nameof(LSheets.Aetheryte),
+ nameof(LSheets.GatheringPointName),
+ nameof(LSheets.EObjName),
+ nameof(LSheets.Mount),
+ nameof(LSheets.Companion),
+ string.Empty,
+ string.Empty,
+ nameof(LSheets.Item),
+ ];
+
+ [ServiceManager.ServiceDependency]
+ private readonly DataManager dataManager = Service.Get();
+
+ [ServiceManager.ServiceConstructor]
+ private SheetRedirectResolver()
+ {
+ }
+
+ ///
+ /// Resolves the sheet redirect, if any is present.
+ ///
+ /// The sheet name.
+ /// The row id.
+ /// The column index. Use ushort.MaxValue as default.
+ /// Flags giving additional information about the redirect.
+ internal SheetRedirectFlags Resolve(ref string sheetName, ref uint rowId, ref uint colIndex)
+ {
+ var flags = SheetRedirectFlags.None;
+
+ switch (sheetName)
+ {
+ case nameof(LSheets.Item) or "ItemHQ" or "ItemMP":
+ {
+ flags |= SheetRedirectFlags.Item;
+
+ var (itemId, kind) = ItemUtil.GetBaseId(rowId);
+
+ if (kind == ItemKind.Hq || sheetName == "ItemHQ")
+ {
+ flags |= SheetRedirectFlags.HighQuality;
+ }
+ else if (kind == ItemKind.Collectible || sheetName == "ItemMP") // MP for Masterpiece?!
+ {
+ flags |= SheetRedirectFlags.Collectible;
+ }
+
+ if (kind == ItemKind.EventItem &&
+ rowId - 2_000_000 <= this.dataManager.GetExcelSheet().Count)
+ {
+ flags |= SheetRedirectFlags.EventItem;
+ sheetName = nameof(LSheets.EventItem);
+ }
+ else
+ {
+ sheetName = nameof(LSheets.Item);
+ rowId = itemId;
+ }
+
+ if (colIndex is >= 4 and <= 7)
+ return SheetRedirectFlags.None;
+
+ break;
+ }
+
+ case "ActStr":
+ {
+ var returnActionSheetFlag = false;
+ (var index, rowId) = uint.DivRem(rowId, 1000000);
+ if (index < ActStrSheets.Length)
+ (sheetName, colIndex, returnActionSheetFlag) = ActStrSheets[index];
+
+ if (sheetName != nameof(LSheets.Companion) && colIndex != 13)
+ flags |= SheetRedirectFlags.Action;
+
+ if (returnActionSheetFlag)
+ flags |= SheetRedirectFlags.ActionSheet;
+
+ break;
+ }
+
+ case "ObjStr":
+ {
+ (var index, rowId) = uint.DivRem(rowId, 1000000);
+ if (index < ObjStrSheetNames.Length)
+ sheetName = ObjStrSheetNames[index];
+
+ colIndex = 0;
+
+ switch (index)
+ {
+ case 0: // BNpcName
+ if (rowId >= 100000)
+ rowId += 900000;
+ break;
+
+ case 1: // ENpcResident
+ rowId += 1000000;
+ break;
+
+ case 2: // Treasure
+ if (this.dataManager.GetExcelSheet().TryGetRow(rowId, out var treasureRow) &&
+ treasureRow.Unknown0.IsEmpty)
+ rowId = 0; // defaulting to "Treasure Coffer"
+ break;
+
+ case 3: // Aetheryte
+ rowId = this.dataManager.GetExcelSheet()
+ .TryGetRow(rowId, out var aetheryteRow) && aetheryteRow.IsAetheryte
+ ? 0u // "Aetheryte"
+ : 1; // "Aethernet Shard"
+ break;
+
+ case 5: // EObjName
+ rowId += 2000000;
+ break;
+ }
+
+ break;
+ }
+
+ case nameof(LSheets.EObj) when colIndex is <= 7 or ushort.MaxValue:
+ sheetName = nameof(LSheets.EObjName);
+ break;
+
+ case nameof(LSheets.Treasure)
+ when this.dataManager.GetExcelSheet().TryGetRow(rowId, out var treasureRow) &&
+ treasureRow.Unknown0.IsEmpty:
+ rowId = 0; // defaulting to "Treasure Coffer"
+ break;
+
+ case "WeatherPlaceName":
+ {
+ sheetName = nameof(LSheets.PlaceName);
+
+ var placeNameSubId = rowId;
+ if (this.dataManager.GetExcelSheet().TryGetFirst(
+ r => r.PlaceNameSub.RowId == placeNameSubId,
+ out var row))
+ rowId = row.PlaceNameParent.RowId;
+ break;
+ }
+
+ case nameof(LSheets.InstanceContent) when colIndex == 3:
+ {
+ sheetName = nameof(LSheets.ContentFinderCondition);
+ colIndex = 43;
+
+ if (this.dataManager.GetExcelSheet().TryGetRow(rowId, out var row))
+ rowId = row.Order;
+ break;
+ }
+
+ case nameof(LSheets.PartyContent) when colIndex == 2:
+ {
+ sheetName = nameof(LSheets.ContentFinderCondition);
+ colIndex = 43;
+
+ if (this.dataManager.GetExcelSheet().TryGetRow(rowId, out var row))
+ rowId = row.ContentFinderCondition.RowId;
+ break;
+ }
+
+ case nameof(LSheets.PublicContent) when colIndex == 3:
+ {
+ sheetName = nameof(LSheets.ContentFinderCondition);
+ colIndex = 43;
+
+ if (this.dataManager.GetExcelSheet().TryGetRow(rowId, out var row))
+ rowId = row.ContentFinderCondition.RowId;
+ break;
+ }
+
+ case nameof(LSheets.AkatsukiNote):
+ {
+ sheetName = nameof(LSheets.AkatsukiNoteString);
+ colIndex = 0;
+
+ if (this.dataManager.Excel.GetSubrowSheet().TryGetRow(rowId, out var row))
+ rowId = (uint)row[0].Unknown2;
+ break;
+ }
+ }
+
+ return flags;
+ }
+}
diff --git a/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs b/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs
new file mode 100644
index 000000000..723dbcb41
--- /dev/null
+++ b/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs
@@ -0,0 +1,1995 @@
+using System.Collections.Concurrent;
+using System.Globalization;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Text;
+
+using Dalamud.Configuration.Internal;
+using Dalamud.Data;
+using Dalamud.Game.ClientState.Objects.Enums;
+using Dalamud.Game.Config;
+using Dalamud.Game.Text.Evaluator.Internal;
+using Dalamud.Game.Text.Noun;
+using Dalamud.Game.Text.Noun.Enums;
+using Dalamud.Logging.Internal;
+using Dalamud.Plugin.Services;
+using Dalamud.Utility;
+
+using FFXIVClientStructs.FFXIV.Client.Game;
+using FFXIVClientStructs.FFXIV.Client.Game.UI;
+using FFXIVClientStructs.FFXIV.Client.UI;
+using FFXIVClientStructs.FFXIV.Client.UI.Agent;
+using FFXIVClientStructs.FFXIV.Client.UI.Info;
+using FFXIVClientStructs.FFXIV.Client.UI.Misc;
+using FFXIVClientStructs.FFXIV.Component.Text;
+
+using Lumina.Data.Structs.Excel;
+using Lumina.Excel;
+using Lumina.Excel.Sheets;
+using Lumina.Extensions;
+using Lumina.Text;
+using Lumina.Text.Expressions;
+using Lumina.Text.Payloads;
+using Lumina.Text.ReadOnly;
+
+using AddonSheet = Lumina.Excel.Sheets.Addon;
+
+namespace Dalamud.Game.Text.Evaluator;
+
+#pragma warning disable SeStringEvaluator
+
+///
+/// Evaluator for SeStrings.
+///
+[ServiceManager.EarlyLoadedService]
+internal class SeStringEvaluator : IServiceType, ISeStringEvaluator
+{
+ private static readonly ModuleLog Log = new("SeStringEvaluator");
+
+ [ServiceManager.ServiceDependency]
+ private readonly DataManager dataManager = Service.Get();
+
+ [ServiceManager.ServiceDependency]
+ private readonly GameConfig gameConfig = Service.Get();
+
+ [ServiceManager.ServiceDependency]
+ private readonly DalamudConfiguration dalamudConfiguration = Service.Get();
+
+ [ServiceManager.ServiceDependency]
+ private readonly NounProcessor nounProcessor = Service.Get();
+
+ [ServiceManager.ServiceDependency]
+ private readonly SheetRedirectResolver sheetRedirectResolver = Service.Get();
+
+ private readonly ConcurrentDictionary, string> actStrCache = [];
+ private readonly ConcurrentDictionary, string> objStrCache = [];
+
+ [ServiceManager.ServiceConstructor]
+ private SeStringEvaluator()
+ {
+ }
+
+ ///
+ public ReadOnlySeString Evaluate(
+ ReadOnlySeString str,
+ Span localParameters = default,
+ ClientLanguage? language = null)
+ {
+ return this.Evaluate(str.AsSpan(), localParameters, language);
+ }
+
+ ///
+ public ReadOnlySeString Evaluate(
+ ReadOnlySeStringSpan str,
+ Span localParameters = default,
+ ClientLanguage? language = null)
+ {
+ if (str.IsTextOnly())
+ return new(str);
+
+ var lang = language ?? this.dalamudConfiguration.EffectiveLanguage.ToClientLanguage();
+
+ // TODO: remove culture info toggling after supporting CultureInfo for SeStringBuilder.Append,
+ // and then remove try...finally block (discard builder from the pool on exception)
+ var previousCulture = CultureInfo.CurrentCulture;
+ var builder = SeStringBuilder.SharedPool.Get();
+ try
+ {
+ CultureInfo.CurrentCulture = Localization.GetCultureInfoFromLangCode(lang.ToCode());
+ return this.EvaluateAndAppendTo(builder, str, localParameters, lang).ToReadOnlySeString();
+ }
+ finally
+ {
+ CultureInfo.CurrentCulture = previousCulture;
+ SeStringBuilder.SharedPool.Return(builder);
+ }
+ }
+
+ ///
+ public ReadOnlySeString EvaluateFromAddon(
+ uint addonId,
+ Span localParameters = default,
+ ClientLanguage? language = null)
+ {
+ var lang = language ?? this.dalamudConfiguration.EffectiveLanguage.ToClientLanguage();
+
+ if (!this.dataManager.GetExcelSheet(lang).TryGetRow(addonId, out var addonRow))
+ return default;
+
+ return this.Evaluate(addonRow.Text.AsSpan(), localParameters, lang);
+ }
+
+ ///
+ public ReadOnlySeString EvaluateFromLobby(
+ uint lobbyId,
+ Span localParameters = default,
+ ClientLanguage? language = null)
+ {
+ var lang = language ?? this.dalamudConfiguration.EffectiveLanguage.ToClientLanguage();
+
+ if (!this.dataManager.GetExcelSheet(lang).TryGetRow(lobbyId, out var lobbyRow))
+ return default;
+
+ return this.Evaluate(lobbyRow.Text.AsSpan(), localParameters, lang);
+ }
+
+ ///
+ public ReadOnlySeString EvaluateFromLogMessage(
+ uint logMessageId,
+ Span localParameters = default,
+ ClientLanguage? language = null)
+ {
+ var lang = language ?? this.dalamudConfiguration.EffectiveLanguage.ToClientLanguage();
+
+ if (!this.dataManager.GetExcelSheet(lang).TryGetRow(logMessageId, out var logMessageRow))
+ return default;
+
+ return this.Evaluate(logMessageRow.Text.AsSpan(), localParameters, lang);
+ }
+
+ ///
+ public string EvaluateActStr(ActionKind actionKind, uint id, ClientLanguage? language = null) =>
+ this.actStrCache.GetOrAdd(
+ new(actionKind, id, language ?? this.dalamudConfiguration.EffectiveLanguage.ToClientLanguage()),
+ static (key, t) => t.EvaluateFromAddon(2026, [key.Kind.GetActStrId(key.Id)], key.Language)
+ .ExtractText()
+ .StripSoftHyphen(),
+ this);
+
+ ///
+ public string EvaluateObjStr(ObjectKind objectKind, uint id, ClientLanguage? language = null) =>
+ this.objStrCache.GetOrAdd(
+ new(objectKind, id, language ?? this.dalamudConfiguration.EffectiveLanguage.ToClientLanguage()),
+ static (key, t) => t.EvaluateFromAddon(2025, [key.Kind.GetObjStrId(key.Id)], key.Language)
+ .ExtractText()
+ .StripSoftHyphen(),
+ this);
+
+ // TODO: move this to MapUtil?
+ private static uint ConvertRawToMapPos(Lumina.Excel.Sheets.Map map, short offset, float value)
+ {
+ var scale = map.SizeFactor / 100.0f;
+ return (uint)(10 - (int)(((((value + offset) * scale) + 1024f) * -0.2f) / scale));
+ }
+
+ private static uint ConvertRawToMapPosX(Lumina.Excel.Sheets.Map map, float x)
+ => ConvertRawToMapPos(map, map.OffsetX, x);
+
+ private static uint ConvertRawToMapPosY(Lumina.Excel.Sheets.Map map, float y)
+ => ConvertRawToMapPos(map, map.OffsetY, y);
+
+ private SeStringBuilder EvaluateAndAppendTo(
+ SeStringBuilder builder,
+ ReadOnlySeStringSpan str,
+ Span localParameters,
+ ClientLanguage language)
+ {
+ var context = new SeStringContext(builder, localParameters, language);
+
+ foreach (var payload in str)
+ {
+ if (!this.ResolvePayload(in context, payload))
+ {
+ context.Builder.Append(payload);
+ }
+ }
+
+ return builder;
+ }
+
+ private bool ResolvePayload(in SeStringContext context, ReadOnlySePayloadSpan payload)
+ {
+ if (payload.Type != ReadOnlySePayloadType.Macro)
+ return false;
+
+ // if (context.HandlePayload(payload, in context))
+ // return true;
+
+ switch (payload.MacroCode)
+ {
+ case MacroCode.SetResetTime:
+ return this.TryResolveSetResetTime(in context, payload);
+
+ case MacroCode.SetTime:
+ return this.TryResolveSetTime(in context, payload);
+
+ case MacroCode.If:
+ return this.TryResolveIf(in context, payload);
+
+ case MacroCode.Switch:
+ return this.TryResolveSwitch(in context, payload);
+
+ case MacroCode.PcName:
+ return this.TryResolvePcName(in context, payload);
+
+ case MacroCode.IfPcGender:
+ return this.TryResolveIfPcGender(in context, payload);
+
+ case MacroCode.IfPcName:
+ return this.TryResolveIfPcName(in context, payload);
+
+ // case MacroCode.Josa:
+ // case MacroCode.Josaro:
+
+ case MacroCode.IfSelf:
+ return this.TryResolveIfSelf(in context, payload);
+
+ // case MacroCode.NewLine: // pass through
+ // case MacroCode.Wait: // pass through
+ // case MacroCode.Icon: // pass through
+
+ case MacroCode.Color:
+ return this.TryResolveColor(in context, payload);
+
+ case MacroCode.EdgeColor:
+ return this.TryResolveEdgeColor(in context, payload);
+
+ case MacroCode.ShadowColor:
+ return this.TryResolveShadowColor(in context, payload);
+
+ // case MacroCode.SoftHyphen: // pass through
+ // case MacroCode.Key:
+ // case MacroCode.Scale:
+
+ case MacroCode.Bold:
+ return this.TryResolveBold(in context, payload);
+
+ case MacroCode.Italic:
+ return this.TryResolveItalic(in context, payload);
+
+ // case MacroCode.Edge:
+ // case MacroCode.Shadow:
+ // case MacroCode.NonBreakingSpace: // pass through
+ // case MacroCode.Icon2: // pass through
+ // case MacroCode.Hyphen: // pass through
+
+ case MacroCode.Num:
+ return this.TryResolveNum(in context, payload);
+
+ case MacroCode.Hex:
+ return this.TryResolveHex(in context, payload);
+
+ case MacroCode.Kilo:
+ return this.TryResolveKilo(in context, payload);
+
+ // case MacroCode.Byte:
+
+ case MacroCode.Sec:
+ return this.TryResolveSec(in context, payload);
+
+ // case MacroCode.Time:
+
+ case MacroCode.Float:
+ return this.TryResolveFloat(in context, payload);
+
+ // case MacroCode.Link: // pass through
+
+ case MacroCode.Sheet:
+ return this.TryResolveSheet(in context, payload);
+
+ case MacroCode.String:
+ return this.TryResolveString(in context, payload);
+
+ case MacroCode.Caps:
+ return this.TryResolveCaps(in context, payload);
+
+ case MacroCode.Head:
+ return this.TryResolveHead(in context, payload);
+
+ case MacroCode.Split:
+ return this.TryResolveSplit(in context, payload);
+
+ case MacroCode.HeadAll:
+ return this.TryResolveHeadAll(in context, payload);
+
+ case MacroCode.Fixed:
+ return this.TryResolveFixed(in context, payload);
+
+ case MacroCode.Lower:
+ return this.TryResolveLower(in context, payload);
+
+ case MacroCode.JaNoun:
+ return this.TryResolveNoun(ClientLanguage.Japanese, in context, payload);
+
+ case MacroCode.EnNoun:
+ return this.TryResolveNoun(ClientLanguage.English, in context, payload);
+
+ case MacroCode.DeNoun:
+ return this.TryResolveNoun(ClientLanguage.German, in context, payload);
+
+ case MacroCode.FrNoun:
+ return this.TryResolveNoun(ClientLanguage.French, in context, payload);
+
+ // case MacroCode.ChNoun:
+
+ case MacroCode.LowerHead:
+ return this.TryResolveLowerHead(in context, payload);
+
+ case MacroCode.ColorType:
+ return this.TryResolveColorType(in context, payload);
+
+ case MacroCode.EdgeColorType:
+ return this.TryResolveEdgeColorType(in context, payload);
+
+ // case MacroCode.Ruby:
+
+ case MacroCode.Digit:
+ return this.TryResolveDigit(in context, payload);
+
+ case MacroCode.Ordinal:
+ return this.TryResolveOrdinal(in context, payload);
+
+ // case MacroCode.Sound: // pass through
+
+ case MacroCode.LevelPos:
+ return this.TryResolveLevelPos(in context, payload);
+
+ default:
+ return false;
+ }
+ }
+
+ private unsafe bool TryResolveSetResetTime(in SeStringContext context, in ReadOnlySePayloadSpan payload)
+ {
+ DateTime date;
+
+ if (payload.TryGetExpression(out var eHour, out var eWeekday)
+ && this.TryResolveInt(in context, eHour, out var eHourVal)
+ && this.TryResolveInt(in context, eWeekday, out var eWeekdayVal))
+ {
+ var t = DateTime.UtcNow.AddDays(((eWeekdayVal - (int)DateTime.UtcNow.DayOfWeek) + 7) % 7);
+ date = new DateTime(t.Year, t.Month, t.Day, eHourVal, 0, 0, DateTimeKind.Utc).ToLocalTime();
+ }
+ else if (payload.TryGetExpression(out eHour)
+ && this.TryResolveInt(in context, eHour, out eHourVal))
+ {
+ var t = DateTime.UtcNow;
+ date = new DateTime(t.Year, t.Month, t.Day, eHourVal, 0, 0, DateTimeKind.Utc).ToLocalTime();
+ }
+ else
+ {
+ return false;
+ }
+
+ MacroDecoder.GetMacroTime()->SetTime(date);
+
+ return true;
+ }
+
+ private unsafe bool TryResolveSetTime(in SeStringContext context, in ReadOnlySePayloadSpan payload)
+ {
+ if (!payload.TryGetExpression(out var eTime) || !this.TryResolveUInt(in context, eTime, out var eTimeVal))
+ return false;
+
+ var date = DateTimeOffset.FromUnixTimeSeconds(eTimeVal).LocalDateTime;
+ MacroDecoder.GetMacroTime()->SetTime(date);
+
+ return true;
+ }
+
+ private bool TryResolveIf(in SeStringContext context, in ReadOnlySePayloadSpan payload)
+ {
+ return
+ payload.TryGetExpression(out var eCond, out var eTrue, out var eFalse)
+ && this.ResolveStringExpression(
+ context,
+ this.TryResolveBool(in context, eCond, out var eCondVal) && eCondVal
+ ? eTrue
+ : eFalse);
+ }
+
+ private bool TryResolveSwitch(in SeStringContext context, in ReadOnlySePayloadSpan payload)
+ {
+ var cond = -1;
+ foreach (var e in payload)
+ {
+ switch (cond)
+ {
+ case -1:
+ cond = this.TryResolveUInt(in context, e, out var eVal) ? (int)eVal : 0;
+ break;
+ case > 1:
+ cond--;
+ break;
+ default:
+ return this.ResolveStringExpression(in context, e);
+ }
+ }
+
+ return false;
+ }
+
+ private unsafe bool TryResolvePcName(in SeStringContext context, in ReadOnlySePayloadSpan payload)
+ {
+ if (!payload.TryGetExpression(out var eEntityId))
+ return false;
+
+ if (!this.TryResolveUInt(in context, eEntityId, out var entityId))
+ return false;
+
+ // TODO: handle LogNameType
+
+ NameCache.CharacterInfo characterInfo = default;
+ if (NameCache.Instance()->TryGetCharacterInfoByEntityId(entityId, &characterInfo))
+ {
+ context.Builder.Append((ReadOnlySeStringSpan)characterInfo.Name.AsSpan());
+
+ if (characterInfo.HomeWorldId != AgentLobby.Instance()->LobbyData.HomeWorldId &&
+ WorldHelper.Instance()->AllWorlds.TryGetValue((ushort)characterInfo.HomeWorldId, out var world, false))
+ {
+ context.Builder.AppendIcon(88);
+
+ if (this.gameConfig.UiConfig.TryGetUInt("LogCrossWorldName", out var logCrossWorldName) &&
+ logCrossWorldName == 1)
+ context.Builder.Append((ReadOnlySeStringSpan)world.Name);
+ }
+
+ return true;
+ }
+
+ // TODO: lookup via InstanceContentCrystallineConflictDirector
+ // TODO: lookup via MJIManager
+
+ return false;
+ }
+
+ private unsafe bool TryResolveIfPcGender(in SeStringContext context, in ReadOnlySePayloadSpan payload)
+ {
+ if (!payload.TryGetExpression(out var eEntityId, out var eMale, out var eFemale))
+ return false;
+
+ if (!this.TryResolveUInt(in context, eEntityId, out var entityId))
+ return false;
+
+ NameCache.CharacterInfo characterInfo = default;
+ if (NameCache.Instance()->TryGetCharacterInfoByEntityId(entityId, &characterInfo))
+ return this.ResolveStringExpression(in context, characterInfo.Sex == 0 ? eMale : eFemale);
+
+ // TODO: lookup via InstanceContentCrystallineConflictDirector
+
+ return false;
+ }
+
+ private unsafe bool TryResolveIfPcName(in SeStringContext context, in ReadOnlySePayloadSpan payload)
+ {
+ if (!payload.TryGetExpression(out var eEntityId, out var eName, out var eTrue, out var eFalse))
+ return false;
+
+ if (!this.TryResolveUInt(in context, eEntityId, out var entityId) || !eName.TryGetString(out var name))
+ return false;
+
+ name = this.Evaluate(name, context.LocalParameters, context.Language).AsSpan();
+
+ NameCache.CharacterInfo characterInfo = default;
+ return NameCache.Instance()->TryGetCharacterInfoByEntityId(entityId, &characterInfo) &&
+ this.ResolveStringExpression(
+ context,
+ name.Equals(characterInfo.Name.AsSpan())
+ ? eTrue
+ : eFalse);
+ }
+
+ private unsafe bool TryResolveIfSelf(in SeStringContext context, in ReadOnlySePayloadSpan payload)
+ {
+ if (!payload.TryGetExpression(out var eEntityId, out var eTrue, out var eFalse))
+ return false;
+
+ if (!this.TryResolveUInt(in context, eEntityId, out var entityId))
+ return false;
+
+ // the game uses LocalPlayer here, but using PlayerState seems more safe.
+ return this.ResolveStringExpression(in context, PlayerState.Instance()->EntityId == entityId ? eTrue : eFalse);
+ }
+
+ private bool TryResolveColor(in SeStringContext context, in ReadOnlySePayloadSpan payload)
+ {
+ if (!payload.TryGetExpression(out var eColor))
+ return false;
+
+ if (eColor.TryGetPlaceholderExpression(out var ph) && ph == (int)ExpressionType.StackColor)
+ context.Builder.PopColor();
+ else if (this.TryResolveUInt(in context, eColor, out var eColorVal))
+ context.Builder.PushColorBgra(eColorVal);
+
+ return true;
+ }
+
+ private bool TryResolveEdgeColor(in SeStringContext context, in ReadOnlySePayloadSpan payload)
+ {
+ if (!payload.TryGetExpression(out var eColor))
+ return false;
+
+ if (eColor.TryGetPlaceholderExpression(out var ph) && ph == (int)ExpressionType.StackColor)
+ context.Builder.PopEdgeColor();
+ else if (this.TryResolveUInt(in context, eColor, out var eColorVal))
+ context.Builder.PushEdgeColorBgra(eColorVal);
+
+ return true;
+ }
+
+ private bool TryResolveShadowColor(in SeStringContext context, in ReadOnlySePayloadSpan payload)
+ {
+ if (!payload.TryGetExpression(out var eColor))
+ return false;
+
+ if (eColor.TryGetPlaceholderExpression(out var ph) && ph == (int)ExpressionType.StackColor)
+ context.Builder.PopShadowColor();
+ else if (this.TryResolveUInt(in context, eColor, out var eColorVal))
+ context.Builder.PushShadowColorBgra(eColorVal);
+
+ return true;
+ }
+
+ private bool TryResolveBold(in SeStringContext context, in ReadOnlySePayloadSpan payload)
+ {
+ if (!payload.TryGetExpression(out var eEnable) ||
+ !this.TryResolveBool(in context, eEnable, out var eEnableVal))
+ return false;
+
+ context.Builder.AppendSetBold(eEnableVal);
+
+ return true;
+ }
+
+ private bool TryResolveItalic(in SeStringContext context, in ReadOnlySePayloadSpan payload)
+ {
+ if (!payload.TryGetExpression(out var eEnable) ||
+ !this.TryResolveBool(in context, eEnable, out var eEnableVal))
+ return false;
+
+ context.Builder.AppendSetItalic(eEnableVal);
+
+ return true;
+ }
+
+ private bool TryResolveNum(in SeStringContext context, in ReadOnlySePayloadSpan payload)
+ {
+ if (!payload.TryGetExpression(out var eInt) || !this.TryResolveInt(in context, eInt, out var eIntVal))
+ {
+ context.Builder.Append('0');
+ return true;
+ }
+
+ context.Builder.Append(eIntVal.ToString());
+
+ return true;
+ }
+
+ private bool TryResolveHex(in SeStringContext context, in ReadOnlySePayloadSpan payload)
+ {
+ if (!payload.TryGetExpression(out var eUInt) || !this.TryResolveUInt(in context, eUInt, out var eUIntVal))
+ {
+ // TODO: throw?
+ // ERROR: mismatch parameter type ('' is not numeric)
+ return false;
+ }
+
+ context.Builder.Append("0x{0:X08}".Format(eUIntVal));
+
+ return true;
+ }
+
+ private bool TryResolveKilo(in SeStringContext context, in ReadOnlySePayloadSpan payload)
+ {
+ if (!payload.TryGetExpression(out var eInt, out var eSep) ||
+ !this.TryResolveInt(in context, eInt, out var eIntVal))
+ {
+ context.Builder.Append('0');
+ return true;
+ }
+
+ if (eIntVal == int.MinValue)
+ {
+ // -2147483648
+ context.Builder.Append("-2"u8);
+ this.ResolveStringExpression(in context, eSep);
+ context.Builder.Append("147"u8);
+ this.ResolveStringExpression(in context, eSep);
+ context.Builder.Append("483"u8);
+ this.ResolveStringExpression(in context, eSep);
+ context.Builder.Append("648"u8);
+ return true;
+ }
+
+ if (eIntVal < 0)
+ {
+ context.Builder.Append('-');
+ eIntVal = -eIntVal;
+ }
+
+ if (eIntVal == 0)
+ {
+ context.Builder.Append('0');
+ return true;
+ }
+
+ var anyDigitPrinted = false;
+ for (var i = 1_000_000_000; i > 0; i /= 10)
+ {
+ var digit = (eIntVal / i) % 10;
+ switch (anyDigitPrinted)
+ {
+ case false when digit == 0:
+ continue;
+ case true when i % 3 == 0:
+ this.ResolveStringExpression(in context, eSep);
+ break;
+ }
+
+ anyDigitPrinted = true;
+ context.Builder.Append((char)('0' + digit));
+ }
+
+ return true;
+ }
+
+ private bool TryResolveSec(in SeStringContext context, in ReadOnlySePayloadSpan payload)
+ {
+ if (!payload.TryGetExpression(out var eInt) || !this.TryResolveUInt(in context, eInt, out var eIntVal))
+ {
+ // TODO: throw?
+ // ERROR: mismatch parameter type ('' is not numeric)
+ return false;
+ }
+
+ context.Builder.Append("{0:00}".Format(eIntVal));
+ return true;
+ }
+
+ private bool TryResolveFloat(in SeStringContext context, in ReadOnlySePayloadSpan payload)
+ {
+ if (!payload.TryGetExpression(out var eValue, out var eRadix, out var eSeparator)
+ || !this.TryResolveInt(in context, eValue, out var eValueVal)
+ || !this.TryResolveInt(in context, eRadix, out var eRadixVal))
+ {
+ return false;
+ }
+
+ var (integerPart, fractionalPart) = int.DivRem(eValueVal, eRadixVal);
+ if (fractionalPart < 0)
+ {
+ integerPart--;
+ fractionalPart += eRadixVal;
+ }
+
+ context.Builder.Append(integerPart.ToString());
+ this.ResolveStringExpression(in context, eSeparator);
+
+ // brain fried code
+ Span fractionalDigits = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
+ var pos = fractionalDigits.Length - 1;
+ for (var r = eRadixVal; r > 1; r /= 10)
+ {
+ fractionalDigits[pos--] = (byte)('0' + (fractionalPart % 10));
+ fractionalPart /= 10;
+ }
+
+ context.Builder.Append(fractionalDigits[(pos + 1)..]);
+
+ return true;
+ }
+
+ private bool TryResolveSheet(in SeStringContext context, in ReadOnlySePayloadSpan payload)
+ {
+ var enu = payload.GetEnumerator();
+
+ if (!enu.MoveNext() || !enu.Current.TryGetString(out var eSheetNameStr))
+ return false;
+
+ if (!enu.MoveNext() || !this.TryResolveUInt(in context, enu.Current, out var eRowIdValue))
+ return false;
+
+ if (!enu.MoveNext() || !this.TryResolveUInt(in context, enu.Current, out var eColIndexValue))
+ return false;
+
+ var eColParamValue = 0u;
+ if (enu.MoveNext())
+ this.TryResolveUInt(in context, enu.Current, out eColParamValue);
+
+ var resolvedSheetName = this.Evaluate(eSheetNameStr, context.LocalParameters, context.Language).ExtractText();
+
+ this.sheetRedirectResolver.Resolve(ref resolvedSheetName, ref eRowIdValue, ref eColIndexValue);
+
+ if (string.IsNullOrEmpty(resolvedSheetName))
+ return false;
+
+ if (!this.dataManager.Excel.SheetNames.Contains(resolvedSheetName))
+ return false;
+
+ if (!this.dataManager.GetExcelSheet(context.Language, resolvedSheetName)
+ .TryGetRow(eRowIdValue, out var row))
+ return false;
+
+ if (eColIndexValue >= row.Columns.Count)
+ return false;
+
+ var column = row.Columns[(int)eColIndexValue];
+ switch (column.Type)
+ {
+ case ExcelColumnDataType.String:
+ context.Builder.Append(this.Evaluate(row.ReadString(column.Offset), [eColParamValue], context.Language));
+ return true;
+ case ExcelColumnDataType.Bool:
+ context.Builder.Append((row.ReadBool(column.Offset) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture));
+ return true;
+ case ExcelColumnDataType.Int8:
+ context.Builder.Append(row.ReadInt8(column.Offset).ToString("D", CultureInfo.InvariantCulture));
+ return true;
+ case ExcelColumnDataType.UInt8:
+ context.Builder.Append(row.ReadUInt8(column.Offset).ToString("D", CultureInfo.InvariantCulture));
+ return true;
+ case ExcelColumnDataType.Int16:
+ context.Builder.Append(row.ReadInt16(column.Offset).ToString("D", CultureInfo.InvariantCulture));
+ return true;
+ case ExcelColumnDataType.UInt16:
+ context.Builder.Append(row.ReadUInt16(column.Offset).ToString("D", CultureInfo.InvariantCulture));
+ return true;
+ case ExcelColumnDataType.Int32:
+ context.Builder.Append(row.ReadInt32(column.Offset).ToString("D", CultureInfo.InvariantCulture));
+ return true;
+ case ExcelColumnDataType.UInt32:
+ context.Builder.Append(row.ReadUInt32(column.Offset).ToString("D", CultureInfo.InvariantCulture));
+ return true;
+ case ExcelColumnDataType.Float32:
+ context.Builder.Append(row.ReadFloat32(column.Offset).ToString("D", CultureInfo.InvariantCulture));
+ return true;
+ case ExcelColumnDataType.Int64:
+ context.Builder.Append(row.ReadInt64(column.Offset).ToString("D", CultureInfo.InvariantCulture));
+ return true;
+ case ExcelColumnDataType.UInt64:
+ context.Builder.Append(row.ReadUInt64(column.Offset).ToString("D", CultureInfo.InvariantCulture));
+ return true;
+ case ExcelColumnDataType.PackedBool0:
+ context.Builder.Append((row.ReadPackedBool(column.Offset, 0) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture));
+ return true;
+ case ExcelColumnDataType.PackedBool1:
+ context.Builder.Append((row.ReadPackedBool(column.Offset, 1) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture));
+ return true;
+ case ExcelColumnDataType.PackedBool2:
+ context.Builder.Append((row.ReadPackedBool(column.Offset, 2) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture));
+ return true;
+ case ExcelColumnDataType.PackedBool3:
+ context.Builder.Append((row.ReadPackedBool(column.Offset, 3) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture));
+ return true;
+ case ExcelColumnDataType.PackedBool4:
+ context.Builder.Append((row.ReadPackedBool(column.Offset, 4) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture));
+ return true;
+ case ExcelColumnDataType.PackedBool5:
+ context.Builder.Append((row.ReadPackedBool(column.Offset, 5) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture));
+ return true;
+ case ExcelColumnDataType.PackedBool6:
+ context.Builder.Append((row.ReadPackedBool(column.Offset, 6) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture));
+ return true;
+ case ExcelColumnDataType.PackedBool7:
+ context.Builder.Append((row.ReadPackedBool(column.Offset, 7) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture));
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ private bool TryResolveString(in SeStringContext context, in ReadOnlySePayloadSpan payload)
+ {
+ return payload.TryGetExpression(out var eStr) && this.ResolveStringExpression(in context, eStr);
+ }
+
+ private bool TryResolveCaps(in SeStringContext context, in ReadOnlySePayloadSpan payload)
+ {
+ if (!payload.TryGetExpression(out var eStr))
+ return false;
+
+ var builder = SeStringBuilder.SharedPool.Get();
+
+ try
+ {
+ var headContext = new SeStringContext(builder, context.LocalParameters, context.Language);
+
+ if (!this.ResolveStringExpression(headContext, eStr))
+ return false;
+
+ var str = builder.ToReadOnlySeString();
+ var pIdx = 0;
+
+ foreach (var p in str)
+ {
+ pIdx++;
+
+ if (p.Type == ReadOnlySePayloadType.Invalid)
+ continue;
+
+ if (pIdx == 1 && p.Type == ReadOnlySePayloadType.Text)
+ {
+ context.Builder.Append(Encoding.UTF8.GetString(p.Body.ToArray()).ToUpper(context.CultureInfo));
+ continue;
+ }
+
+ context.Builder.Append(p);
+ }
+
+ return true;
+ }
+ finally
+ {
+ SeStringBuilder.SharedPool.Return(builder);
+ }
+ }
+
+ private bool TryResolveHead(in SeStringContext context, in ReadOnlySePayloadSpan payload)
+ {
+ if (!payload.TryGetExpression(out var eStr))
+ return false;
+
+ var builder = SeStringBuilder.SharedPool.Get();
+
+ try
+ {
+ var headContext = new SeStringContext(builder, context.LocalParameters, context.Language);
+
+ if (!this.ResolveStringExpression(headContext, eStr))
+ return false;
+
+ var str = builder.ToReadOnlySeString();
+ var pIdx = 0;
+
+ foreach (var p in str)
+ {
+ pIdx++;
+
+ if (p.Type == ReadOnlySePayloadType.Invalid)
+ continue;
+
+ if (pIdx == 1 && p.Type == ReadOnlySePayloadType.Text)
+ {
+ context.Builder.Append(Encoding.UTF8.GetString(p.Body.Span).FirstCharToUpper(context.CultureInfo));
+ continue;
+ }
+
+ context.Builder.Append(p);
+ }
+
+ return true;
+ }
+ finally
+ {
+ SeStringBuilder.SharedPool.Return(builder);
+ }
+ }
+
+ private bool TryResolveSplit(in SeStringContext context, in ReadOnlySePayloadSpan payload)
+ {
+ if (!payload.TryGetExpression(out var eText, out var eSeparator, out var eIndex))
+ return false;
+
+ if (!eSeparator.TryGetString(out var eSeparatorVal) || !eIndex.TryGetUInt(out var eIndexVal) || eIndexVal <= 0)
+ return false;
+
+ var builder = SeStringBuilder.SharedPool.Get();
+
+ try
+ {
+ var headContext = new SeStringContext(builder, context.LocalParameters, context.Language);
+
+ if (!this.ResolveStringExpression(headContext, eText))
+ return false;
+
+ var separator = eSeparatorVal.ExtractText();
+ if (separator.Length < 1)
+ return false;
+
+ var splitted = builder.ToReadOnlySeString().ExtractText().Split(separator[0]);
+ if (eIndexVal <= splitted.Length)
+ {
+ context.Builder.Append(splitted[eIndexVal - 1]);
+ return true;
+ }
+
+ return false;
+ }
+ finally
+ {
+ SeStringBuilder.SharedPool.Return(builder);
+ }
+ }
+
+ private bool TryResolveHeadAll(in SeStringContext context, in ReadOnlySePayloadSpan payload)
+ {
+ if (!payload.TryGetExpression(out var eStr))
+ return false;
+
+ var builder = SeStringBuilder.SharedPool.Get();
+
+ try
+ {
+ var headContext = new SeStringContext(builder, context.LocalParameters, context.Language);
+
+ if (!this.ResolveStringExpression(headContext, eStr))
+ return false;
+
+ var str = builder.ToReadOnlySeString();
+
+ foreach (var p in str)
+ {
+ if (p.Type == ReadOnlySePayloadType.Invalid)
+ continue;
+
+ if (p.Type == ReadOnlySePayloadType.Text)
+ {
+ context.Builder.Append(
+ context.CultureInfo.TextInfo.ToTitleCase(Encoding.UTF8.GetString(p.Body.Span)));
+
+ continue;
+ }
+
+ context.Builder.Append(p);
+ }
+
+ return true;
+ }
+ finally
+ {
+ SeStringBuilder.SharedPool.Return(builder);
+ }
+ }
+
+ private bool TryResolveFixed(in SeStringContext context, in ReadOnlySePayloadSpan payload)
+ {
+ // This is handled by the second function in Client::UI::Misc::PronounModule_ProcessString
+
+ var enu = payload.GetEnumerator();
+
+ if (!enu.MoveNext() || !this.TryResolveInt(in context, enu.Current, out var e0Val))
+ return false;
+
+ if (!enu.MoveNext() || !this.TryResolveInt(in context, enu.Current, out var e1Val))
+ return false;
+
+ return e0Val switch
+ {
+ 100 or 200 => e1Val switch
+ {
+ 1 => this.TryResolveFixedPlayerLink(in context, ref enu),
+ 2 => this.TryResolveFixedClassJobLevel(in context, ref enu),
+ 3 => this.TryResolveFixedMapLink(in context, ref enu),
+ 4 => this.TryResolveFixedItemLink(in context, ref enu),
+ 5 => this.TryResolveFixedChatSoundEffect(in context, ref enu),
+ 6 => this.TryResolveFixedObjStr(in context, ref enu),
+ 7 => this.TryResolveFixedString(in context, ref enu),
+ 8 => this.TryResolveFixedTimeRemaining(in context, ref enu),
+ // Reads a uint and saves it to PronounModule+0x3AC
+ // TODO: handle this? looks like it's for the mentor/beginner icon of the player link in novice network
+ // see "FF 50 50 8B B0"
+ 9 => true,
+ 10 => this.TryResolveFixedStatusLink(in context, ref enu),
+ 11 => this.TryResolveFixedPartyFinderLink(in context, ref enu),
+ 12 => this.TryResolveFixedQuestLink(in context, ref enu),
+ _ => false,
+ },
+ _ => this.TryResolveFixedAutoTranslation(in context, payload, e0Val, e1Val),
+ };
+ }
+
+ private unsafe bool TryResolveFixedPlayerLink(in SeStringContext context, ref ReadOnlySePayloadSpan.Enumerator enu)
+ {
+ if (!enu.MoveNext() || !this.TryResolveUInt(in context, enu.Current, out var worldId))
+ return false;
+
+ if (!enu.MoveNext() || !enu.Current.TryGetString(out var playerName))
+ return false;
+
+ if (UIGlobals.IsValidPlayerCharacterName(playerName.ExtractText()))
+ {
+ var flags = 0u;
+ if (InfoModule.Instance()->IsInCrossWorldDuty())
+ flags |= 0x10;
+
+ context.Builder.PushLink(LinkMacroPayloadType.Character, flags, worldId, 0u, playerName);
+ context.Builder.Append(playerName);
+ context.Builder.PopLink();
+ }
+ else
+ {
+ context.Builder.Append(playerName);
+ }
+
+ if (worldId == AgentLobby.Instance()->LobbyData.HomeWorldId)
+ return true;
+
+ if (!this.dataManager.GetExcelSheet(context.Language).TryGetRow(worldId, out var worldRow))
+ return false;
+
+ context.Builder.AppendIcon(88);
+ context.Builder.Append(worldRow.Name);
+
+ return true;
+ }
+
+ private bool TryResolveFixedClassJobLevel(in SeStringContext context, ref ReadOnlySePayloadSpan.Enumerator enu)
+ {
+ if (!enu.MoveNext() || !this.TryResolveInt(in context, enu.Current, out var classJobId) || classJobId <= 0)
+ return false;
+
+ if (!enu.MoveNext() || !this.TryResolveInt(in context, enu.Current, out var level))
+ return false;
+
+ if (!this.dataManager.GetExcelSheet(context.Language)
+ .TryGetRow((uint)classJobId, out var classJobRow))
+ return false;
+
+ context.Builder.Append(classJobRow.Name);
+
+ if (level != 0)
+ context.Builder.Append(context.CultureInfo, $"({level:D})");
+
+ return true;
+ }
+
+ private bool TryResolveFixedMapLink(in SeStringContext context, ref ReadOnlySePayloadSpan.Enumerator enu)
+ {
+ if (!enu.MoveNext() || !this.TryResolveUInt(in context, enu.Current, out var territoryTypeId))
+ return false;
+
+ if (!enu.MoveNext() || !this.TryResolveUInt(in context, enu.Current, out var packedIds))
+ return false;
+
+ if (!enu.MoveNext() || !this.TryResolveInt(in context, enu.Current, out var rawX))
+ return false;
+
+ if (!enu.MoveNext() || !this.TryResolveInt(in context, enu.Current, out var rawY))
+ return false;
+
+ if (!enu.MoveNext() || !this.TryResolveInt(in context, enu.Current, out var rawZ))
+ return false;
+
+ if (!enu.MoveNext() || !this.TryResolveUInt(in context, enu.Current, out var placeNameIdInt))
+ return false;
+
+ var instance = packedIds >> 0x10;
+ var mapId = packedIds & 0xFF;
+
+ if (this.dataManager.GetExcelSheet(context.Language)
+ .TryGetRow(territoryTypeId, out var territoryTypeRow))
+ {
+ if (!this.dataManager.GetExcelSheet(context.Language)
+ .TryGetRow(
+ placeNameIdInt == 0 ? territoryTypeRow.PlaceName.RowId : placeNameIdInt,
+ out var placeNameRow))
+ return false;
+
+ if (!this.dataManager.GetExcelSheet().TryGetRow(mapId, out var mapRow))
+ return false;
+
+ var sb = SeStringBuilder.SharedPool.Get();
+
+ sb.Append(placeNameRow.Name);
+ if (instance is > 0 and <= 9)
+ sb.Append((char)((char)0xE0B0 + (char)instance));
+
+ var placeNameWithInstance = sb.ToReadOnlySeString();
+ SeStringBuilder.SharedPool.Return(sb);
+
+ var mapPosX = ConvertRawToMapPosX(mapRow, rawX / 1000f);
+ var mapPosY = ConvertRawToMapPosY(mapRow, rawY / 1000f);
+
+ var linkText = rawZ == -30000
+ ? this.EvaluateFromAddon(
+ 1635,
+ [placeNameWithInstance, mapPosX, mapPosY],
+ context.Language)
+ : this.EvaluateFromAddon(
+ 1636,
+ [placeNameWithInstance, mapPosX, mapPosY, rawZ / (rawZ >= 0 ? 10 : -10), rawZ],
+ context.Language);
+
+ context.Builder.PushLinkMapPosition(territoryTypeId, mapId, rawX, rawY);
+ context.Builder.Append(this.EvaluateFromAddon(371, [linkText], context.Language));
+ context.Builder.PopLink();
+
+ return true;
+ }
+
+ var rowId = mapId switch
+ {
+ 0 => 875u, // "(No location set for map link)"
+ 1 => 874u, // "(Map link unavailable in this area)"
+ 2 => 13743u, // "(Unable to set map link)"
+ _ => 0u,
+ };
+ if (rowId == 0u)
+ return false;
+ if (this.dataManager.GetExcelSheet(context.Language).TryGetRow(rowId, out var addonRow))
+ context.Builder.Append(addonRow.Text);
+ return true;
+ }
+
+ private bool TryResolveFixedItemLink(in SeStringContext context, ref ReadOnlySePayloadSpan.Enumerator enu)
+ {
+ if (!enu.MoveNext() || !this.TryResolveUInt(in context, enu.Current, out var itemId))
+ return false;
+
+ if (!enu.MoveNext() || !this.TryResolveUInt(in context, enu.Current, out var rarity))
+ return false;
+
+ if (!enu.MoveNext() || !this.TryResolveInt(in context, enu.Current, out var unk2))
+ return false;
+
+ if (!enu.MoveNext() || !this.TryResolveInt(in context, enu.Current, out var unk3))
+ return false;
+
+ if (!enu.MoveNext() || !enu.Current.TryGetString(out var itemName)) // TODO: unescape??
+ return false;
+
+ // rarity color start
+ context.Builder.Append(this.EvaluateFromAddon(6, [rarity], context.Language));
+
+ var v2 = (ushort)((unk2 & 0xFF) + (unk3 << 0x10)); // TODO: find out what this does
+
+ context.Builder.PushLink(LinkMacroPayloadType.Item, itemId, rarity, v2);
+
+ // arrow and item name
+ context.Builder.Append(this.EvaluateFromAddon(371, [itemName], context.Language));
+
+ context.Builder.PopLink();
+ context.Builder.PopColor();
+
+ return true;
+ }
+
+ private bool TryResolveFixedChatSoundEffect(in SeStringContext context, ref ReadOnlySePayloadSpan.Enumerator enu)
+ {
+ if (!enu.MoveNext() || !this.TryResolveUInt(in context, enu.Current, out var soundEffectId))
+ return false;
+
+ context.Builder.Append($"");
+
+ // the game would play it here
+
+ return true;
+ }
+
+ private bool TryResolveFixedObjStr(in SeStringContext context, ref ReadOnlySePayloadSpan.Enumerator enu)
+ {
+ if (!enu.MoveNext() || !this.TryResolveUInt(in context, enu.Current, out var objStrId))
+ return false;
+
+ context.Builder.Append(this.EvaluateFromAddon(2025, [objStrId], context.Language));
+
+ return true;
+ }
+
+ private bool TryResolveFixedString(in SeStringContext context, ref ReadOnlySePayloadSpan.Enumerator enu)
+ {
+ if (!enu.MoveNext() || !enu.Current.TryGetString(out var text))
+ return false;
+
+ // formats it through vsprintf using "%s"??
+ context.Builder.Append(text.ExtractText());
+
+ return true;
+ }
+
+ private bool TryResolveFixedTimeRemaining(in SeStringContext context, ref ReadOnlySePayloadSpan.Enumerator enu)
+ {
+ if (!enu.MoveNext() || !this.TryResolveUInt(in context, enu.Current, out var seconds))
+ return false;
+
+ if (seconds != 0)
+ {
+ context.Builder.Append(this.EvaluateFromAddon(33, [seconds / 60, seconds % 60], context.Language));
+ }
+ else
+ {
+ if (this.dataManager.GetExcelSheet(context.Language).TryGetRow(48, out var addonRow))
+ context.Builder.Append(addonRow.Text);
+ }
+
+ return true;
+ }
+
+ private bool TryResolveFixedStatusLink(in SeStringContext context, ref ReadOnlySePayloadSpan.Enumerator enu)
+ {
+ if (!enu.MoveNext() || !this.TryResolveUInt(in context, enu.Current, out var statusId))
+ return false;
+
+ if (!enu.MoveNext() || !this.TryResolveBool(in context, enu.Current, out var hasOverride))
+ return false;
+
+ if (!this.dataManager.GetExcelSheet(context.Language)
+ .TryGetRow(statusId, out var statusRow))
+ return false;
+
+ ReadOnlySeStringSpan statusName;
+ ReadOnlySeStringSpan statusDescription;
+
+ if (hasOverride)
+ {
+ if (!enu.MoveNext() || !enu.Current.TryGetString(out statusName))
+ return false;
+
+ if (!enu.MoveNext() || !enu.Current.TryGetString(out statusDescription))
+ return false;
+ }
+ else
+ {
+ statusName = statusRow.Name.AsSpan();
+ statusDescription = statusRow.Description.AsSpan();
+ }
+
+ var sb = SeStringBuilder.SharedPool.Get();
+
+ switch (statusRow.StatusCategory)
+ {
+ case 1:
+ sb.Append(this.EvaluateFromAddon(376, default, context.Language));
+ break;
+
+ case 2:
+ sb.Append(this.EvaluateFromAddon(377, default, context.Language));
+ break;
+ }
+
+ sb.Append(statusName);
+
+ var linkText = sb.ToReadOnlySeString();
+ SeStringBuilder.SharedPool.Return(sb);
+
+ context.Builder
+ .BeginMacro(MacroCode.Link)
+ .AppendUIntExpression((uint)LinkMacroPayloadType.Status)
+ .AppendUIntExpression(statusId)
+ .AppendUIntExpression(0)
+ .AppendUIntExpression(0)
+ .AppendStringExpression(statusName)
+ .AppendStringExpression(statusDescription)
+ .EndMacro();
+
+ context.Builder.Append(this.EvaluateFromAddon(371, [linkText], context.Language));
+
+ context.Builder.PopLink();
+
+ return true;
+ }
+
+ private bool TryResolveFixedPartyFinderLink(in SeStringContext context, ref ReadOnlySePayloadSpan.Enumerator enu)
+ {
+ if (!enu.MoveNext() || !this.TryResolveUInt(in context, enu.Current, out var listingId))
+ return false;
+
+ if (!enu.MoveNext() || !this.TryResolveUInt(in context, enu.Current, out var unk1))
+ return false;
+
+ if (!enu.MoveNext() || !this.TryResolveUInt(in context, enu.Current, out var worldId))
+ return false;
+
+ if (!enu.MoveNext() || !this.TryResolveInt(
+ context,
+ enu.Current,
+ out var crossWorldFlag)) // 0 = cross world, 1 = not cross world
+ return false;
+
+ if (!enu.MoveNext() || !enu.Current.TryGetString(out var playerName))
+ return false;
+
+ context.Builder
+ .BeginMacro(MacroCode.Link)
+ .AppendUIntExpression((uint)LinkMacroPayloadType.PartyFinder)
+ .AppendUIntExpression(listingId)
+ .AppendUIntExpression(unk1)
+ .AppendUIntExpression((uint)(crossWorldFlag << 0x10) + worldId)
+ .EndMacro();
+
+ context.Builder.Append(
+ this.EvaluateFromAddon(
+ 371,
+ [this.EvaluateFromAddon(2265, [playerName, crossWorldFlag], context.Language)],
+ context.Language));
+
+ context.Builder.PopLink();
+
+ return true;
+ }
+
+ private bool TryResolveFixedQuestLink(in SeStringContext context, ref ReadOnlySePayloadSpan.Enumerator enu)
+ {
+ if (!enu.MoveNext() || !this.TryResolveUInt(in context, enu.Current, out var questId))
+ return false;
+
+ if (!enu.MoveNext() || !enu.MoveNext() || !enu.MoveNext()) // unused
+ return false;
+
+ if (!enu.MoveNext() || !enu.Current.TryGetString(out var questName))
+ return false;
+
+ /* TODO: hide incomplete, repeatable special event quest names
+ if (!QuestManager.IsQuestComplete(questId) && !QuestManager.Instance()->IsQuestAccepted(questId))
+ {
+ var questRecompleteManager = QuestRecompleteManager.Instance();
+ if (questRecompleteManager == null || !questRecompleteManager->"E8 ?? ?? ?? ?? 0F B6 57 FF"(questId)) {
+ if (_excelService.TryGetRow(5497, context.Language, out var addonRow))
+ questName = addonRow.Text.AsSpan();
+ }
+ }
+ */
+
+ context.Builder
+ .BeginMacro(MacroCode.Link)
+ .AppendUIntExpression((uint)LinkMacroPayloadType.Quest)
+ .AppendUIntExpression(questId)
+ .AppendUIntExpression(0)
+ .AppendUIntExpression(0)
+ .EndMacro();
+
+ context.Builder.Append(this.EvaluateFromAddon(371, [questName], context.Language));
+
+ context.Builder.PopLink();
+
+ return true;
+ }
+
+ private bool TryResolveFixedAutoTranslation(
+ in SeStringContext context, in ReadOnlySePayloadSpan payload, int e0Val, int e1Val)
+ {
+ // Auto-Translation / Completion
+ var group = (uint)(e0Val + 1);
+ var rowId = (uint)e1Val;
+
+ using var icons = new SeStringBuilderIconWrap(context.Builder, 54, 55);
+
+ if (!this.dataManager.GetExcelSheet(context.Language).TryGetFirst(
+ row => row.Group == group && !row.LookupTable.IsEmpty,
+ out var groupRow))
+ return false;
+
+ var lookupTable = (
+ groupRow.LookupTable.IsTextOnly()
+ ? groupRow.LookupTable
+ : this.Evaluate(
+ groupRow.LookupTable.AsSpan(),
+ context.LocalParameters,
+ context.Language)).ExtractText();
+
+ // Completion sheet
+ if (lookupTable.Equals("@"))
+ {
+ if (this.dataManager.GetExcelSheet(context.Language).TryGetRow(rowId, out var completionRow))
+ {
+ context.Builder.Append(completionRow.Text);
+ }
+
+ return true;
+ }
+
+ // CategoryDataCache
+ if (lookupTable.Equals("#"))
+ {
+ // couldn't find any, so we don't handle them :p
+ context.Builder.Append(payload);
+ return false;
+ }
+
+ // All other sheets
+ var rangesStart = lookupTable.IndexOf('[');
+ // Sheet without ranges
+ if (rangesStart == -1)
+ {
+ if (this.dataManager.GetExcelSheet(context.Language, lookupTable).TryGetRow(rowId, out var row))
+ {
+ context.Builder.Append(row.ReadStringColumn(0));
+ return true;
+ }
+ }
+
+ var sheetName = lookupTable[..rangesStart];
+ var ranges = lookupTable[(rangesStart + 1)..^1];
+ if (ranges.Length == 0)
+ return true;
+
+ var isNoun = false;
+ var col = 0;
+
+ if (ranges.StartsWith("noun"))
+ {
+ isNoun = true;
+ }
+ else if (ranges.StartsWith("col"))
+ {
+ var colRangeEnd = ranges.IndexOf(',');
+ if (colRangeEnd == -1)
+ colRangeEnd = ranges.Length;
+
+ col = int.Parse(ranges[4..colRangeEnd]);
+ }
+ else if (ranges.StartsWith("tail"))
+ {
+ // couldn't find any, so we don't handle them :p
+ context.Builder.Append(payload);
+ return false;
+ }
+
+ if (isNoun && context.Language == ClientLanguage.German && sheetName == "Companion")
+ {
+ context.Builder.Append(this.nounProcessor.ProcessNoun(new NounParams()
+ {
+ Language = ClientLanguage.German,
+ SheetName = sheetName,
+ RowId = rowId,
+ Quantity = 1,
+ ArticleType = (int)GermanArticleType.ZeroArticle,
+ }));
+ }
+ else if (this.dataManager.GetExcelSheet(context.Language, sheetName).TryGetRow(rowId, out var row))
+ {
+ context.Builder.Append(row.ReadStringColumn(col));
+ }
+
+ return true;
+ }
+
+ private bool TryResolveLower(in SeStringContext context, in ReadOnlySePayloadSpan payload)
+ {
+ if (!payload.TryGetExpression(out var eStr))
+ return false;
+
+ var builder = SeStringBuilder.SharedPool.Get();
+
+ try
+ {
+ var headContext = new SeStringContext(builder, context.LocalParameters, context.Language);
+
+ if (!this.ResolveStringExpression(headContext, eStr))
+ return false;
+
+ var str = builder.ToReadOnlySeString();
+
+ foreach (var p in str)
+ {
+ if (p.Type == ReadOnlySePayloadType.Invalid)
+ continue;
+
+ if (p.Type == ReadOnlySePayloadType.Text)
+ {
+ context.Builder.Append(Encoding.UTF8.GetString(p.Body.ToArray()).ToLower(context.CultureInfo));
+
+ continue;
+ }
+
+ context.Builder.Append(p);
+ }
+
+ return true;
+ }
+ finally
+ {
+ SeStringBuilder.SharedPool.Return(builder);
+ }
+ }
+
+ private bool TryResolveNoun(ClientLanguage language, in SeStringContext context, in ReadOnlySePayloadSpan payload)
+ {
+ var eAmountVal = 1;
+ var eCaseVal = 1;
+
+ var enu = payload.GetEnumerator();
+
+ if (!enu.MoveNext() || !enu.Current.TryGetString(out var eSheetNameStr))
+ return false;
+
+ var sheetName = this.Evaluate(eSheetNameStr, context.LocalParameters, context.Language).ExtractText();
+
+ if (!enu.MoveNext() || !this.TryResolveInt(in context, enu.Current, out var eArticleTypeVal))
+ return false;
+
+ if (!enu.MoveNext() || !this.TryResolveUInt(in context, enu.Current, out var eRowIdVal))
+ return false;
+
+ uint colIndex = ushort.MaxValue;
+ var flags = this.sheetRedirectResolver.Resolve(ref sheetName, ref eRowIdVal, ref colIndex);
+
+ if (string.IsNullOrEmpty(sheetName))
+ return false;
+
+ // optional arguments
+ if (enu.MoveNext())
+ {
+ if (!this.TryResolveInt(in context, enu.Current, out eAmountVal))
+ return false;
+
+ if (enu.MoveNext())
+ {
+ if (!this.TryResolveInt(in context, enu.Current, out eCaseVal))
+ return false;
+
+ // For Chinese texts?
+ /*
+ if (enu.MoveNext())
+ {
+ var eUnkInt5 = enu.Current;
+ if (!TryResolveInt(context,eUnkInt5, out eUnkInt5Val))
+ return false;
+ }
+ */
+ }
+ }
+
+ context.Builder.Append(
+ this.nounProcessor.ProcessNoun(new NounParams()
+ {
+ Language = language,
+ SheetName = sheetName,
+ RowId = eRowIdVal,
+ Quantity = eAmountVal,
+ ArticleType = eArticleTypeVal,
+ GrammaticalCase = eCaseVal - 1,
+ IsActionSheet = flags.HasFlag(SheetRedirectFlags.Action),
+ }));
+
+ return true;
+ }
+
+ private bool TryResolveLowerHead(in SeStringContext context, in ReadOnlySePayloadSpan payload)
+ {
+ if (!payload.TryGetExpression(out var eStr))
+ return false;
+
+ var builder = SeStringBuilder.SharedPool.Get();
+
+ try
+ {
+ var headContext = new SeStringContext(builder, context.LocalParameters, context.Language);
+
+ if (!this.ResolveStringExpression(headContext, eStr))
+ return false;
+
+ var str = builder.ToReadOnlySeString();
+ var pIdx = 0;
+
+ foreach (var p in str)
+ {
+ pIdx++;
+
+ if (p.Type == ReadOnlySePayloadType.Invalid)
+ continue;
+
+ if (pIdx == 1 && p.Type == ReadOnlySePayloadType.Text)
+ {
+ context.Builder.Append(Encoding.UTF8.GetString(p.Body.Span).FirstCharToLower(context.CultureInfo));
+ continue;
+ }
+
+ context.Builder.Append(p);
+ }
+
+ return true;
+ }
+ finally
+ {
+ SeStringBuilder.SharedPool.Return(builder);
+ }
+ }
+
+ private bool TryResolveColorType(in SeStringContext context, in ReadOnlySePayloadSpan payload)
+ {
+ if (!payload.TryGetExpression(out var eColorType) ||
+ !this.TryResolveUInt(in context, eColorType, out var eColorTypeVal))
+ return false;
+
+ if (eColorTypeVal == 0)
+ context.Builder.PopColor();
+ else if (this.dataManager.GetExcelSheet().TryGetRow(eColorTypeVal, out var row))
+ context.Builder.PushColorBgra((row.UIForeground >> 8) | (row.UIForeground << 24));
+
+ return true;
+ }
+
+ private bool TryResolveEdgeColorType(in SeStringContext context, in ReadOnlySePayloadSpan payload)
+ {
+ if (!payload.TryGetExpression(out var eColorType) ||
+ !this.TryResolveUInt(in context, eColorType, out var eColorTypeVal))
+ return false;
+
+ if (eColorTypeVal == 0)
+ context.Builder.PopEdgeColor();
+ else if (this.dataManager.GetExcelSheet().TryGetRow(eColorTypeVal, out var row))
+ context.Builder.PushEdgeColorBgra((row.UIForeground >> 8) | (row.UIForeground << 24));
+
+ return true;
+ }
+
+ private bool TryResolveDigit(in SeStringContext context, in ReadOnlySePayloadSpan payload)
+ {
+ if (!payload.TryGetExpression(out var eValue, out var eTargetLength))
+ return false;
+
+ if (!this.TryResolveInt(in context, eValue, out var eValueVal))
+ return false;
+
+ if (!this.TryResolveInt(in context, eTargetLength, out var eTargetLengthVal))
+ return false;
+
+ context.Builder.Append(eValueVal.ToString(new string('0', eTargetLengthVal)));
+
+ return true;
+ }
+
+ private bool TryResolveOrdinal(in SeStringContext context, in ReadOnlySePayloadSpan payload)
+ {
+ if (!payload.TryGetExpression(out var eValue) || !this.TryResolveUInt(in context, eValue, out var eValueVal))
+ return false;
+
+ // TODO: Culture support?
+ context.Builder.Append(
+ $"{eValueVal}{(eValueVal % 10) switch
+ {
+ _ when eValueVal is >= 10 and <= 19 => "th",
+ 1 => "st",
+ 2 => "nd",
+ 3 => "rd",
+ _ => "th",
+ }}");
+ return true;
+ }
+
+ private bool TryResolveLevelPos(in SeStringContext context, in ReadOnlySePayloadSpan payload)
+ {
+ if (!payload.TryGetExpression(out var eLevel) || !this.TryResolveUInt(in context, eLevel, out var eLevelVal))
+ return false;
+
+ if (!this.dataManager.GetExcelSheet(context.Language).TryGetRow(eLevelVal, out var level) ||
+ !level.Map.IsValid)
+ return false;
+
+ if (!this.dataManager.GetExcelSheet(context.Language).TryGetRow(
+ level.Map.Value.PlaceName.RowId,
+ out var placeName))
+ return false;
+
+ var mapPosX = ConvertRawToMapPosX(level.Map.Value, level.X);
+ var mapPosY = ConvertRawToMapPosY(level.Map.Value, level.Z); // Z is [sic]
+
+ context.Builder.Append(
+ this.EvaluateFromAddon(
+ 1637,
+ [placeName.Name, mapPosX, mapPosY],
+ context.Language));
+
+ return true;
+ }
+
+ private unsafe bool TryGetGNumDefault(uint parameterIndex, out uint value)
+ {
+ value = 0u;
+
+ var rtm = RaptureTextModule.Instance();
+ if (rtm is null)
+ return false;
+
+ if (!ThreadSafety.IsMainThread)
+ {
+ Log.Error("Global parameters may only be used from the main thread.");
+ return false;
+ }
+
+ ref var gp = ref rtm->TextModule.MacroDecoder.GlobalParameters;
+ if (parameterIndex >= gp.MySize)
+ return false;
+
+ var p = rtm->TextModule.MacroDecoder.GlobalParameters[parameterIndex];
+ switch (p.Type)
+ {
+ case TextParameterType.Integer:
+ value = (uint)p.IntValue;
+ return true;
+
+ case TextParameterType.ReferencedUtf8String:
+ Log.Error("Requested a number; Utf8String global parameter at {parameterIndex}.", parameterIndex);
+ return false;
+
+ case TextParameterType.String:
+ Log.Error("Requested a number; string global parameter at {parameterIndex}.", parameterIndex);
+ return false;
+
+ case TextParameterType.Uninitialized:
+ Log.Error("Requested a number; uninitialized global parameter at {parameterIndex}.", parameterIndex);
+ return false;
+
+ default:
+ return false;
+ }
+ }
+
+ private unsafe bool TryProduceGStrDefault(SeStringBuilder builder, ClientLanguage language, uint parameterIndex)
+ {
+ var rtm = RaptureTextModule.Instance();
+ if (rtm is null)
+ return false;
+
+ ref var gp = ref rtm->TextModule.MacroDecoder.GlobalParameters;
+ if (parameterIndex >= gp.MySize)
+ return false;
+
+ if (!ThreadSafety.IsMainThread)
+ {
+ Log.Error("Global parameters may only be used from the main thread.");
+ return false;
+ }
+
+ var p = rtm->TextModule.MacroDecoder.GlobalParameters[parameterIndex];
+ switch (p.Type)
+ {
+ case TextParameterType.Integer:
+ builder.Append($"{p.IntValue:D}");
+ return true;
+
+ case TextParameterType.ReferencedUtf8String:
+ this.EvaluateAndAppendTo(
+ builder,
+ p.ReferencedUtf8StringValue->Utf8String.AsSpan(),
+ null,
+ language);
+ return false;
+
+ case TextParameterType.String:
+ this.EvaluateAndAppendTo(builder, new(p.StringValue), null, language);
+ return false;
+
+ case TextParameterType.Uninitialized:
+ default:
+ return false;
+ }
+ }
+
+ private unsafe bool TryResolveUInt(
+ in SeStringContext context, in ReadOnlySeExpressionSpan expression, out uint value)
+ {
+ if (expression.TryGetUInt(out value))
+ return true;
+
+ if (expression.TryGetPlaceholderExpression(out var exprType))
+ {
+ // if (context.TryGetPlaceholderNum(exprType, out value))
+ // return true;
+
+ switch ((ExpressionType)exprType)
+ {
+ case ExpressionType.Millisecond:
+ value = (uint)DateTime.Now.Millisecond;
+ return true;
+ case ExpressionType.Second:
+ value = (uint)MacroDecoder.GetMacroTime()->tm_sec;
+ return true;
+ case ExpressionType.Minute:
+ value = (uint)MacroDecoder.GetMacroTime()->tm_min;
+ return true;
+ case ExpressionType.Hour:
+ value = (uint)MacroDecoder.GetMacroTime()->tm_hour;
+ return true;
+ case ExpressionType.Day:
+ value = (uint)MacroDecoder.GetMacroTime()->tm_mday;
+ return true;
+ case ExpressionType.Weekday:
+ value = (uint)MacroDecoder.GetMacroTime()->tm_wday;
+ return true;
+ case ExpressionType.Month:
+ value = (uint)MacroDecoder.GetMacroTime()->tm_mon + 1;
+ return true;
+ case ExpressionType.Year:
+ value = (uint)MacroDecoder.GetMacroTime()->tm_year + 1900;
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ if (expression.TryGetParameterExpression(out exprType, out var operand1))
+ {
+ if (!this.TryResolveUInt(in context, operand1, out var paramIndex))
+ return false;
+ if (paramIndex == 0)
+ return false;
+ paramIndex--;
+ return (ExpressionType)exprType switch
+ {
+ ExpressionType.LocalNumber => context.TryGetLNum((int)paramIndex, out value), // lnum
+ ExpressionType.GlobalNumber => this.TryGetGNumDefault(paramIndex, out value), // gnum
+ _ => false, // gstr, lstr
+ };
+ }
+
+ if (expression.TryGetBinaryExpression(out exprType, out operand1, out var operand2))
+ {
+ switch ((ExpressionType)exprType)
+ {
+ case ExpressionType.GreaterThanOrEqualTo:
+ case ExpressionType.GreaterThan:
+ case ExpressionType.LessThanOrEqualTo:
+ case ExpressionType.LessThan:
+ if (!this.TryResolveInt(in context, operand1, out var value1)
+ || !this.TryResolveInt(in context, operand2, out var value2))
+ {
+ return false;
+ }
+
+ value = (ExpressionType)exprType switch
+ {
+ ExpressionType.GreaterThanOrEqualTo => value1 >= value2 ? 1u : 0u,
+ ExpressionType.GreaterThan => value1 > value2 ? 1u : 0u,
+ ExpressionType.LessThanOrEqualTo => value1 <= value2 ? 1u : 0u,
+ ExpressionType.LessThan => value1 < value2 ? 1u : 0u,
+ _ => 0u,
+ };
+ return true;
+
+ case ExpressionType.Equal:
+ case ExpressionType.NotEqual:
+ if (this.TryResolveInt(in context, operand1, out value1) &&
+ this.TryResolveInt(in context, operand2, out value2))
+ {
+ if ((ExpressionType)exprType == ExpressionType.Equal)
+ value = value1 == value2 ? 1u : 0u;
+ else
+ value = value1 == value2 ? 0u : 1u;
+ return true;
+ }
+
+ if (operand1.TryGetString(out var strval1) && operand2.TryGetString(out var strval2))
+ {
+ var resolvedStr1 = this.EvaluateAndAppendTo(
+ SeStringBuilder.SharedPool.Get(),
+ strval1,
+ context.LocalParameters,
+ context.Language);
+ var resolvedStr2 = this.EvaluateAndAppendTo(
+ SeStringBuilder.SharedPool.Get(),
+ strval2,
+ context.LocalParameters,
+ context.Language);
+ var equals = resolvedStr1.GetViewAsSpan().SequenceEqual(resolvedStr2.GetViewAsSpan());
+ SeStringBuilder.SharedPool.Return(resolvedStr1);
+ SeStringBuilder.SharedPool.Return(resolvedStr2);
+
+ if ((ExpressionType)exprType == ExpressionType.Equal)
+ value = equals ? 1u : 0u;
+ else
+ value = equals ? 0u : 1u;
+ return true;
+ }
+
+ // compare int with string, string with int??
+
+ return true;
+
+ default:
+ return false;
+ }
+ }
+
+ if (expression.TryGetString(out var str))
+ {
+ var evaluatedStr = this.Evaluate(str, context.LocalParameters, context.Language);
+
+ foreach (var payload in evaluatedStr)
+ {
+ if (!payload.TryGetExpression(out var expr))
+ return false;
+
+ return this.TryResolveUInt(in context, expr, out value);
+ }
+
+ return false;
+ }
+
+ return false;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private bool TryResolveInt(in SeStringContext context, in ReadOnlySeExpressionSpan expression, out int value)
+ {
+ if (this.TryResolveUInt(in context, expression, out var u32))
+ {
+ value = (int)u32;
+ return true;
+ }
+
+ value = 0;
+ return false;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private bool TryResolveBool(in SeStringContext context, in ReadOnlySeExpressionSpan expression, out bool value)
+ {
+ if (this.TryResolveUInt(in context, expression, out var u32))
+ {
+ value = u32 != 0;
+ return true;
+ }
+
+ value = false;
+ return false;
+ }
+
+ private bool ResolveStringExpression(in SeStringContext context, in ReadOnlySeExpressionSpan expression)
+ {
+ uint u32;
+
+ if (expression.TryGetString(out var innerString))
+ {
+ context.Builder.Append(this.Evaluate(innerString, context.LocalParameters, context.Language));
+ return true;
+ }
+
+ /*
+ if (expression.TryGetPlaceholderExpression(out var exprType))
+ {
+ if (context.TryProducePlaceholder(context,exprType))
+ return true;
+ }
+ */
+
+ if (expression.TryGetParameterExpression(out var exprType, out var operand1))
+ {
+ if (!this.TryResolveUInt(in context, operand1, out var paramIndex))
+ return false;
+ if (paramIndex == 0)
+ return false;
+ paramIndex--;
+ switch ((ExpressionType)exprType)
+ {
+ case ExpressionType.LocalNumber: // lnum
+ if (!context.TryGetLNum((int)paramIndex, out u32))
+ return false;
+
+ context.Builder.Append(unchecked((int)u32).ToString());
+ return true;
+
+ case ExpressionType.LocalString: // lstr
+ if (!context.TryGetLStr((int)paramIndex, out var str))
+ return false;
+
+ context.Builder.Append(str);
+ return true;
+
+ case ExpressionType.GlobalNumber: // gnum
+ if (!this.TryGetGNumDefault(paramIndex, out u32))
+ return false;
+
+ context.Builder.Append(unchecked((int)u32).ToString());
+ return true;
+
+ case ExpressionType.GlobalString: // gstr
+ return this.TryProduceGStrDefault(context.Builder, context.Language, paramIndex);
+
+ default:
+ return false;
+ }
+ }
+
+ // Handles UInt and Binary expressions
+ if (!this.TryResolveUInt(in context, expression, out u32))
+ return false;
+
+ context.Builder.Append(((int)u32).ToString());
+ return true;
+ }
+
+ private readonly record struct StringCacheKey(TK Kind, uint Id, ClientLanguage Language)
+ where TK : struct, Enum;
+}
diff --git a/Dalamud/Game/Text/Evaluator/SeStringParameter.cs b/Dalamud/Game/Text/Evaluator/SeStringParameter.cs
new file mode 100644
index 000000000..c1f238f56
--- /dev/null
+++ b/Dalamud/Game/Text/Evaluator/SeStringParameter.cs
@@ -0,0 +1,79 @@
+using System.Globalization;
+
+using Lumina.Text.ReadOnly;
+
+using DSeString = Dalamud.Game.Text.SeStringHandling.SeString;
+using LSeString = Lumina.Text.SeString;
+
+namespace Dalamud.Game.Text.Evaluator;
+
+///
+/// A wrapper for a local parameter, holding either a number or a string.
+///
+public readonly struct SeStringParameter
+{
+ private readonly uint num;
+ private readonly ReadOnlySeString str;
+
+ ///
+ /// Initializes a new instance of the struct for a number parameter.
+ ///
+ /// The number value.
+ public SeStringParameter(uint value)
+ {
+ this.num = value;
+ }
+
+ ///
+ /// Initializes a new instance of the struct for a string parameter.
+ ///
+ /// The string value.
+ public SeStringParameter(ReadOnlySeString value)
+ {
+ this.str = value;
+ this.IsString = true;
+ }
+
+ ///
+ /// Initializes a new instance of the struct for a string parameter.
+ ///
+ /// The string value.
+ public SeStringParameter(string value)
+ {
+ this.str = new ReadOnlySeString(value);
+ this.IsString = true;
+ }
+
+ ///
+ /// Gets a value indicating whether the backing type of this parameter is a string.
+ ///
+ public bool IsString { get; }
+
+ ///
+ /// Gets a numeric value.
+ ///
+ public uint UIntValue =>
+ !this.IsString
+ ? this.num
+ : uint.TryParse(this.str.ExtractText(), out var value) ? value : 0;
+
+ ///
+ /// Gets a string value.
+ ///
+ public ReadOnlySeString StringValue =>
+ this.IsString ? this.str : new(this.num.ToString("D", CultureInfo.InvariantCulture));
+
+ public static implicit operator SeStringParameter(int value) => new((uint)value);
+
+ public static implicit operator SeStringParameter(uint value) => new(value);
+
+ public static implicit operator SeStringParameter(ReadOnlySeString value) => new(value);
+
+ public static implicit operator SeStringParameter(ReadOnlySeStringSpan value) => new(new ReadOnlySeString(value));
+
+ public static implicit operator SeStringParameter(LSeString value) => new(new ReadOnlySeString(value.RawData));
+
+ public static implicit operator SeStringParameter(DSeString value) => new(new ReadOnlySeString(value.Encode()));
+
+ public static implicit operator SeStringParameter(string value) => new(value);
+}
diff --git a/Dalamud/Game/Text/Noun/Enums/EnglishArticleType.cs b/Dalamud/Game/Text/Noun/Enums/EnglishArticleType.cs
new file mode 100644
index 000000000..9214bea0b
--- /dev/null
+++ b/Dalamud/Game/Text/Noun/Enums/EnglishArticleType.cs
@@ -0,0 +1,17 @@
+namespace Dalamud.Game.Text.Noun.Enums;
+
+///
+/// Article types for .
+///
+public enum EnglishArticleType
+{
+ ///
+ /// Indefinite article (a, an).
+ ///
+ Indefinite = 1,
+
+ ///
+ /// Definite article (the).
+ ///
+ Definite = 2,
+}
diff --git a/Dalamud/Game/Text/Noun/Enums/FrenchArticleType.cs b/Dalamud/Game/Text/Noun/Enums/FrenchArticleType.cs
new file mode 100644
index 000000000..3b6d6a63e
--- /dev/null
+++ b/Dalamud/Game/Text/Noun/Enums/FrenchArticleType.cs
@@ -0,0 +1,32 @@
+namespace Dalamud.Game.Text.Noun.Enums;
+
+///
+/// Article types for .
+///
+public enum FrenchArticleType
+{
+ ///
+ /// Indefinite article (une, des).
+ ///
+ Indefinite = 1,
+
+ ///
+ /// Definite article (le, la, les).
+ ///
+ Definite = 2,
+
+ ///
+ /// Possessive article (mon, mes).
+ ///
+ PossessiveFirstPerson = 3,
+
+ ///
+ /// Possessive article (ton, tes).
+ ///
+ PossessiveSecondPerson = 4,
+
+ ///
+ /// Possessive article (son, ses).
+ ///
+ PossessiveThirdPerson = 5,
+}
diff --git a/Dalamud/Game/Text/Noun/Enums/GermanArticleType.cs b/Dalamud/Game/Text/Noun/Enums/GermanArticleType.cs
new file mode 100644
index 000000000..29124e172
--- /dev/null
+++ b/Dalamud/Game/Text/Noun/Enums/GermanArticleType.cs
@@ -0,0 +1,37 @@
+namespace Dalamud.Game.Text.Noun.Enums;
+
+///
+/// Article types for .
+///
+public enum GermanArticleType
+{
+ ///
+ /// Unbestimmter Artikel (ein, eine, etc.).
+ ///
+ Indefinite = 1,
+
+ ///
+ /// Bestimmter Artikel (der, die, das, etc.).
+ ///
+ Definite = 2,
+
+ ///
+ /// Possessivartikel "dein" (dein, deine, etc.).
+ ///
+ Possessive = 3,
+
+ ///
+ /// Negativartikel "kein" (kein, keine, etc.).
+ ///
+ Negative = 4,
+
+ ///
+ /// Nullartikel.
+ ///
+ ZeroArticle = 5,
+
+ ///
+ /// Demonstrativpronomen "dieser" (dieser, diese, etc.).
+ ///
+ Demonstrative = 6,
+}
diff --git a/Dalamud/Game/Text/Noun/Enums/JapaneseArticleType.cs b/Dalamud/Game/Text/Noun/Enums/JapaneseArticleType.cs
new file mode 100644
index 000000000..14a29c4ff
--- /dev/null
+++ b/Dalamud/Game/Text/Noun/Enums/JapaneseArticleType.cs
@@ -0,0 +1,17 @@
+namespace Dalamud.Game.Text.Noun.Enums;
+
+///
+/// Article types for .
+///
+public enum JapaneseArticleType
+{
+ ///
+ /// Near listener (それら).
+ ///
+ NearListener = 1,
+
+ ///
+ /// Distant from both speaker and listener (あれら).
+ ///
+ Distant = 2,
+}
diff --git a/Dalamud/Game/Text/Noun/NounParams.cs b/Dalamud/Game/Text/Noun/NounParams.cs
new file mode 100644
index 000000000..3d5c424be
--- /dev/null
+++ b/Dalamud/Game/Text/Noun/NounParams.cs
@@ -0,0 +1,73 @@
+using Dalamud.Game.Text.Noun.Enums;
+
+using Lumina.Text.ReadOnly;
+
+using LSheets = Lumina.Excel.Sheets;
+
+namespace Dalamud.Game.Text.Noun;
+
+///
+/// Parameters for noun processing.
+///
+internal record struct NounParams()
+{
+ ///
+ /// The language of the sheet to be processed.
+ ///
+ public required ClientLanguage Language;
+
+ ///
+ /// The name of the sheet containing the row to process.
+ ///
+ public required string SheetName = string.Empty;
+
+ ///
+ /// The row id within the sheet to process.
+ ///
+ public required uint RowId;
+
+ ///
+ /// The quantity of the entity (default is 1). Used to determine grammatical number (e.g., singular or plural).
+ ///
+ public int Quantity = 1;
+
+ ///
+ /// The article type.
+ ///
+ ///
+ /// Depending on the , this has different meanings.
+ /// See , , , .
+ ///
+ public int ArticleType = 1;
+
+ ///
+ /// The grammatical case (e.g., Nominative, Genitive, Dative, Accusative) used for German texts (default is 0).
+ ///
+ public int GrammaticalCase = 0;
+
+ ///
+ /// An optional string that is placed in front of the text that should be linked, such as item names (default is an empty string; the game uses "//").
+ ///
+ public ReadOnlySeString LinkMarker = default;
+
+ ///
+ /// An indicator that this noun will be processed from an Action sheet. Only used for German texts.
+ ///
+ public bool IsActionSheet;
+
+ ///
+ /// Gets the column offset.
+ ///
+ public readonly int ColumnOffset => this.SheetName switch
+ {
+ // See "E8 ?? ?? ?? ?? 44 8B 6B 08"
+ nameof(LSheets.BeastTribe) => 10,
+ nameof(LSheets.DeepDungeonItem) => 1,
+ nameof(LSheets.DeepDungeonEquipment) => 1,
+ nameof(LSheets.DeepDungeonMagicStone) => 1,
+ nameof(LSheets.DeepDungeonDemiclone) => 1,
+ nameof(LSheets.Glasses) => 4,
+ nameof(LSheets.GlassesStyle) => 15,
+ _ => 0,
+ };
+}
diff --git a/Dalamud/Game/Text/Noun/NounProcessor.cs b/Dalamud/Game/Text/Noun/NounProcessor.cs
new file mode 100644
index 000000000..18f8cd4a9
--- /dev/null
+++ b/Dalamud/Game/Text/Noun/NounProcessor.cs
@@ -0,0 +1,461 @@
+using System.Collections.Concurrent;
+
+using Dalamud.Configuration.Internal;
+using Dalamud.Data;
+using Dalamud.Game.Text.Noun.Enums;
+using Dalamud.Logging.Internal;
+using Dalamud.Utility;
+
+using Lumina.Excel;
+using Lumina.Text.ReadOnly;
+
+using LSeStringBuilder = Lumina.Text.SeStringBuilder;
+using LSheets = Lumina.Excel.Sheets;
+
+namespace Dalamud.Game.Text.Noun;
+
+/*
+Attributive sheet:
+ Japanese:
+ Unknown0 = Singular Demonstrative
+ Unknown1 = Plural Demonstrative
+ English:
+ Unknown2 = Article before a singular noun beginning with a consonant sound
+ Unknown3 = Article before a generic noun beginning with a consonant sound
+ Unknown4 = N/A
+ Unknown5 = Article before a singular noun beginning with a vowel sound
+ Unknown6 = Article before a generic noun beginning with a vowel sound
+ Unknown7 = N/A
+ German:
+ Unknown8 = Nominative Masculine
+ Unknown9 = Nominative Feminine
+ Unknown10 = Nominative Neutral
+ Unknown11 = Nominative Plural
+ Unknown12 = Genitive Masculine
+ Unknown13 = Genitive Feminine
+ Unknown14 = Genitive Neutral
+ Unknown15 = Genitive Plural
+ Unknown16 = Dative Masculine
+ Unknown17 = Dative Feminine
+ Unknown18 = Dative Neutral
+ Unknown19 = Dative Plural
+ Unknown20 = Accusative Masculine
+ Unknown21 = Accusative Feminine
+ Unknown22 = Accusative Neutral
+ Unknown23 = Accusative Plural
+ French (unsure):
+ Unknown24 = Singular Article
+ Unknown25 = Singular Masculine Article
+ Unknown26 = Plural Masculine Article
+ Unknown27 = ?
+ Unknown28 = ?
+ Unknown29 = Singular Masculine/Feminine Article, before a noun beginning in a vowel or an h
+ Unknown30 = Plural Masculine/Feminine Article, before a noun beginning in a vowel or an h
+ Unknown31 = ?
+ Unknown32 = ?
+ Unknown33 = Singular Feminine Article
+ Unknown34 = Plural Feminine Article
+ Unknown35 = ?
+ Unknown36 = ?
+ Unknown37 = Singular Masculine/Feminine Article, before a noun beginning in a vowel or an h
+ Unknown38 = Plural Masculine/Feminine Article, before a noun beginning in a vowel or an h
+ Unknown39 = ?
+ Unknown40 = ?
+
+Placeholders:
+ [t] = article or grammatical gender (EN: the, DE: der, die, das)
+ [n] = amount (number)
+ [a] = declension
+ [p] = plural
+ [pa] = ?
+*/
+
+///
+/// Provides functionality to process texts from sheets containing grammatical placeholders.
+///
+[ServiceManager.EarlyLoadedService]
+internal class NounProcessor : IServiceType
+{
+ // column names from ExdSchema, most likely incorrect
+ private const int SingularColumnIdx = 0;
+ private const int AdjectiveColumnIdx = 1;
+ private const int PluralColumnIdx = 2;
+ private const int PossessivePronounColumnIdx = 3;
+ private const int StartsWithVowelColumnIdx = 4;
+ private const int Unknown5ColumnIdx = 5; // probably used in Chinese texts
+ private const int PronounColumnIdx = 6;
+ private const int ArticleColumnIdx = 7;
+
+ private static readonly ModuleLog Log = new("NounProcessor");
+
+ [ServiceManager.ServiceDependency]
+ private readonly DataManager dataManager = Service.Get();
+
+ [ServiceManager.ServiceDependency]
+ private readonly DalamudConfiguration dalamudConfiguration = Service.Get();
+
+ private readonly ConcurrentDictionary cache = [];
+
+ [ServiceManager.ServiceConstructor]
+ private NounProcessor()
+ {
+ }
+
+ ///
+ /// Processes a specific row from a sheet and generates a formatted string based on grammatical and language-specific rules.
+ ///
+ /// Parameters for processing.
+ /// A ReadOnlySeString representing the processed text.
+ public ReadOnlySeString ProcessNoun(NounParams nounParams)
+ {
+ if (nounParams.GrammaticalCase < 0 || nounParams.GrammaticalCase > 5)
+ return default;
+
+ if (this.cache.TryGetValue(nounParams, out var value))
+ return value;
+
+ var output = nounParams.Language switch
+ {
+ ClientLanguage.Japanese => this.ResolveNounJa(nounParams),
+ ClientLanguage.English => this.ResolveNounEn(nounParams),
+ ClientLanguage.German => this.ResolveNounDe(nounParams),
+ ClientLanguage.French => this.ResolveNounFr(nounParams),
+ _ => default,
+ };
+
+ this.cache.TryAdd(nounParams, output);
+
+ return output;
+ }
+
+ ///
+ /// Resolves noun placeholders in Japanese text.
+ ///
+ /// Parameters for processing.
+ /// A ReadOnlySeString representing the processed text.
+ ///
+ /// This is a C# implementation of Component::Text::Localize::NounJa.Resolve.
+ ///
+ private ReadOnlySeString ResolveNounJa(NounParams nounParams)
+ {
+ var sheet = this.dataManager.Excel.GetSheet(nounParams.Language.ToLumina(), nounParams.SheetName);
+ if (!sheet.TryGetRow(nounParams.RowId, out var row))
+ {
+ Log.Warning("Sheet {SheetName} does not contain row #{RowId}", nounParams.SheetName, nounParams.RowId);
+ return default;
+ }
+
+ var attributiveSheet = this.dataManager.Excel.GetSheet(nounParams.Language.ToLumina(), nameof(LSheets.Attributive));
+
+ var builder = LSeStringBuilder.SharedPool.Get();
+
+ // Ko-So-A-Do
+ var ksad = attributiveSheet.GetRow((uint)nounParams.ArticleType).ReadStringColumn(nounParams.Quantity > 1 ? 1 : 0);
+ if (!ksad.IsEmpty)
+ {
+ builder.Append(ksad);
+
+ if (nounParams.Quantity > 1)
+ {
+ builder.ReplaceText("[n]"u8, ReadOnlySeString.FromText(nounParams.Quantity.ToString()));
+ }
+ }
+
+ if (!nounParams.LinkMarker.IsEmpty)
+ builder.Append(nounParams.LinkMarker);
+
+ var text = row.ReadStringColumn(nounParams.ColumnOffset);
+ if (!text.IsEmpty)
+ builder.Append(text);
+
+ var ross = builder.ToReadOnlySeString();
+ LSeStringBuilder.SharedPool.Return(builder);
+ return ross;
+ }
+
+ ///
+ /// Resolves noun placeholders in English text.
+ ///
+ /// Parameters for processing.
+ /// A ReadOnlySeString representing the processed text.
+ ///
+ /// This is a C# implementation of Component::Text::Localize::NounEn.Resolve.
+ ///
+ private ReadOnlySeString ResolveNounEn(NounParams nounParams)
+ {
+ /*
+ a1->Offsets[0] = SingularColumnIdx
+ a1->Offsets[1] = PluralColumnIdx
+ a1->Offsets[2] = StartsWithVowelColumnIdx
+ a1->Offsets[3] = PossessivePronounColumnIdx
+ a1->Offsets[4] = ArticleColumnIdx
+ */
+
+ var sheet = this.dataManager.Excel.GetSheet(nounParams.Language.ToLumina(), nounParams.SheetName);
+ if (!sheet.TryGetRow(nounParams.RowId, out var row))
+ {
+ Log.Warning("Sheet {SheetName} does not contain row #{RowId}", nounParams.SheetName, nounParams.RowId);
+ return default;
+ }
+
+ var attributiveSheet = this.dataManager.Excel.GetSheet(nounParams.Language.ToLumina(), nameof(LSheets.Attributive));
+
+ var builder = LSeStringBuilder.SharedPool.Get();
+
+ var isProperNounColumn = nounParams.ColumnOffset + ArticleColumnIdx;
+ var isProperNoun = isProperNounColumn >= 0 ? row.ReadInt8Column(isProperNounColumn) : ~isProperNounColumn;
+ if (isProperNoun == 0)
+ {
+ var startsWithVowelColumn = nounParams.ColumnOffset + StartsWithVowelColumnIdx;
+ var startsWithVowel = startsWithVowelColumn >= 0
+ ? row.ReadInt8Column(startsWithVowelColumn)
+ : ~startsWithVowelColumn;
+
+ var articleColumn = startsWithVowel + (2 * (startsWithVowel + 1));
+ var grammaticalNumberColumnOffset = nounParams.Quantity == 1 ? SingularColumnIdx : PluralColumnIdx;
+ var article = attributiveSheet.GetRow((uint)nounParams.ArticleType)
+ .ReadStringColumn(articleColumn + grammaticalNumberColumnOffset);
+ if (!article.IsEmpty)
+ builder.Append(article);
+
+ if (!nounParams.LinkMarker.IsEmpty)
+ builder.Append(nounParams.LinkMarker);
+ }
+
+ var text = row.ReadStringColumn(nounParams.ColumnOffset + (nounParams.Quantity == 1 ? SingularColumnIdx : PluralColumnIdx));
+ if (!text.IsEmpty)
+ builder.Append(text);
+
+ builder.ReplaceText("[n]"u8, ReadOnlySeString.FromText(nounParams.Quantity.ToString()));
+
+ var ross = builder.ToReadOnlySeString();
+ LSeStringBuilder.SharedPool.Return(builder);
+ return ross;
+ }
+
+ ///
+ /// Resolves noun placeholders in German text.
+ ///
+ /// Parameters for processing.
+ /// A ReadOnlySeString representing the processed text.
+ ///
+ /// This is a C# implementation of Component::Text::Localize::NounDe.Resolve.
+ ///
+ private ReadOnlySeString ResolveNounDe(NounParams nounParams)
+ {
+ /*
+ a1->Offsets[0] = SingularColumnIdx
+ a1->Offsets[1] = PluralColumnIdx
+ a1->Offsets[2] = PronounColumnIdx
+ a1->Offsets[3] = AdjectiveColumnIdx
+ a1->Offsets[4] = PossessivePronounColumnIdx
+ a1->Offsets[5] = Unknown5ColumnIdx
+ a1->Offsets[6] = ArticleColumnIdx
+ */
+
+ var sheet = this.dataManager.Excel.GetSheet(nounParams.Language.ToLumina(), nounParams.SheetName);
+ if (!sheet.TryGetRow(nounParams.RowId, out var row))
+ {
+ Log.Warning("Sheet {SheetName} does not contain row #{RowId}", nounParams.SheetName, nounParams.RowId);
+ return default;
+ }
+
+ var attributiveSheet = this.dataManager.Excel.GetSheet(nounParams.Language.ToLumina(), nameof(LSheets.Attributive));
+
+ var builder = LSeStringBuilder.SharedPool.Get();
+ ReadOnlySeString ross;
+
+ if (nounParams.IsActionSheet)
+ {
+ builder.Append(row.ReadStringColumn(nounParams.GrammaticalCase));
+ builder.ReplaceText("[n]"u8, ReadOnlySeString.FromText(nounParams.Quantity.ToString()));
+
+ ross = builder.ToReadOnlySeString();
+ LSeStringBuilder.SharedPool.Return(builder);
+ return ross;
+ }
+
+ var genderIndexColumn = nounParams.ColumnOffset + PronounColumnIdx;
+ var genderIndex = genderIndexColumn >= 0 ? row.ReadInt8Column(genderIndexColumn) : ~genderIndexColumn;
+
+ var articleIndexColumn = nounParams.ColumnOffset + ArticleColumnIdx;
+ var articleIndex = articleIndexColumn >= 0 ? row.ReadInt8Column(articleIndexColumn) : ~articleIndexColumn;
+
+ var caseColumnOffset = (4 * nounParams.GrammaticalCase) + 8;
+
+ var caseRowOffsetColumn = nounParams.ColumnOffset + (nounParams.Quantity == 1 ? AdjectiveColumnIdx : PossessivePronounColumnIdx);
+ var caseRowOffset = caseRowOffsetColumn >= 0
+ ? row.ReadInt8Column(caseRowOffsetColumn)
+ : (sbyte)~caseRowOffsetColumn;
+
+ if (nounParams.Quantity != 1)
+ genderIndex = 3;
+
+ var hasT = false;
+ var text = row.ReadStringColumn(nounParams.ColumnOffset + (nounParams.Quantity == 1 ? SingularColumnIdx : PluralColumnIdx));
+ if (!text.IsEmpty)
+ {
+ hasT = text.ContainsText("[t]"u8);
+
+ if (articleIndex == 0 && !hasT)
+ {
+ var grammaticalGender = attributiveSheet.GetRow((uint)nounParams.ArticleType)
+ .ReadStringColumn(caseColumnOffset + genderIndex); // Genus
+ if (!grammaticalGender.IsEmpty)
+ builder.Append(grammaticalGender);
+ }
+
+ if (!nounParams.LinkMarker.IsEmpty)
+ builder.Append(nounParams.LinkMarker);
+
+ builder.Append(text);
+
+ var plural = attributiveSheet.GetRow((uint)(caseRowOffset + 26))
+ .ReadStringColumn(caseColumnOffset + genderIndex);
+ if (builder.ContainsText("[p]"u8))
+ builder.ReplaceText("[p]"u8, plural);
+ else
+ builder.Append(plural);
+
+ if (hasT)
+ {
+ var article =
+ attributiveSheet.GetRow(39).ReadStringColumn(caseColumnOffset + genderIndex); // Definiter Artikel
+ builder.ReplaceText("[t]"u8, article);
+ }
+ }
+
+ var pa = attributiveSheet.GetRow(24).ReadStringColumn(caseColumnOffset + genderIndex);
+ builder.ReplaceText("[pa]"u8, pa);
+
+ RawRow declensionRow;
+
+ declensionRow = (GermanArticleType)nounParams.ArticleType switch
+ {
+ // Schwache Flexion eines Adjektivs?!
+ GermanArticleType.Possessive or GermanArticleType.Demonstrative => attributiveSheet.GetRow(25),
+ _ when hasT => attributiveSheet.GetRow(25),
+
+ // Starke Deklination
+ GermanArticleType.ZeroArticle => attributiveSheet.GetRow(38),
+
+ // Gemischte Deklination
+ GermanArticleType.Definite => attributiveSheet.GetRow(37),
+
+ // Starke Flexion eines Artikels?!
+ GermanArticleType.Indefinite or GermanArticleType.Negative => attributiveSheet.GetRow(26),
+ _ => attributiveSheet.GetRow(26),
+ };
+
+ var declension = declensionRow.ReadStringColumn(caseColumnOffset + genderIndex);
+ builder.ReplaceText("[a]"u8, declension);
+
+ builder.ReplaceText("[n]"u8, ReadOnlySeString.FromText(nounParams.Quantity.ToString()));
+
+ ross = builder.ToReadOnlySeString();
+ LSeStringBuilder.SharedPool.Return(builder);
+ return ross;
+ }
+
+ ///
+ /// Resolves noun placeholders in French text.
+ ///
+ /// Parameters for processing.
+ /// A ReadOnlySeString representing the processed text.
+ ///
+ /// This is a C# implementation of Component::Text::Localize::NounFr.Resolve.
+ ///
+ private ReadOnlySeString ResolveNounFr(NounParams nounParams)
+ {
+ /*
+ a1->Offsets[0] = SingularColumnIdx
+ a1->Offsets[1] = PluralColumnIdx
+ a1->Offsets[2] = StartsWithVowelColumnIdx
+ a1->Offsets[3] = PronounColumnIdx
+ a1->Offsets[4] = Unknown5ColumnIdx
+ a1->Offsets[5] = ArticleColumnIdx
+ */
+
+ var sheet = this.dataManager.Excel.GetSheet(nounParams.Language.ToLumina(), nounParams.SheetName);
+ if (!sheet.TryGetRow(nounParams.RowId, out var row))
+ {
+ Log.Warning("Sheet {SheetName} does not contain row #{RowId}", nounParams.SheetName, nounParams.RowId);
+ return default;
+ }
+
+ var attributiveSheet = this.dataManager.Excel.GetSheet(nounParams.Language.ToLumina(), nameof(LSheets.Attributive));
+
+ var builder = LSeStringBuilder.SharedPool.Get();
+ ReadOnlySeString ross;
+
+ var startsWithVowelColumn = nounParams.ColumnOffset + StartsWithVowelColumnIdx;
+ var startsWithVowel = startsWithVowelColumn >= 0
+ ? row.ReadInt8Column(startsWithVowelColumn)
+ : ~startsWithVowelColumn;
+
+ var pronounColumn = nounParams.ColumnOffset + PronounColumnIdx;
+ var pronoun = pronounColumn >= 0 ? row.ReadInt8Column(pronounColumn) : ~pronounColumn;
+
+ var articleColumn = nounParams.ColumnOffset + ArticleColumnIdx;
+ var article = articleColumn >= 0 ? row.ReadInt8Column(articleColumn) : ~articleColumn;
+
+ var v20 = 4 * (startsWithVowel + 6 + (2 * pronoun));
+
+ if (article != 0)
+ {
+ var v21 = attributiveSheet.GetRow((uint)nounParams.ArticleType).ReadStringColumn(v20);
+ if (!v21.IsEmpty)
+ builder.Append(v21);
+
+ if (!nounParams.LinkMarker.IsEmpty)
+ builder.Append(nounParams.LinkMarker);
+
+ var text = row.ReadStringColumn(nounParams.ColumnOffset + (nounParams.Quantity <= 1 ? SingularColumnIdx : PluralColumnIdx));
+ if (!text.IsEmpty)
+ builder.Append(text);
+
+ if (nounParams.Quantity <= 1)
+ builder.ReplaceText("[n]"u8, ReadOnlySeString.FromText(nounParams.Quantity.ToString()));
+
+ ross = builder.ToReadOnlySeString();
+ LSeStringBuilder.SharedPool.Return(builder);
+ return ross;
+ }
+
+ var v17 = row.ReadInt8Column(nounParams.ColumnOffset + Unknown5ColumnIdx);
+ if (v17 != 0 && (nounParams.Quantity > 1 || v17 == 2))
+ {
+ var v29 = attributiveSheet.GetRow((uint)nounParams.ArticleType).ReadStringColumn(v20 + 2);
+ if (!v29.IsEmpty)
+ {
+ builder.Append(v29);
+
+ if (!nounParams.LinkMarker.IsEmpty)
+ builder.Append(nounParams.LinkMarker);
+
+ var text = row.ReadStringColumn(nounParams.ColumnOffset + PluralColumnIdx);
+ if (!text.IsEmpty)
+ builder.Append(text);
+ }
+ }
+ else
+ {
+ var v27 = attributiveSheet.GetRow((uint)nounParams.ArticleType).ReadStringColumn(v20 + (v17 != 0 ? 1 : 3));
+ if (!v27.IsEmpty)
+ builder.Append(v27);
+
+ if (!nounParams.LinkMarker.IsEmpty)
+ builder.Append(nounParams.LinkMarker);
+
+ var text = row.ReadStringColumn(nounParams.ColumnOffset + SingularColumnIdx);
+ if (!text.IsEmpty)
+ builder.Append(text);
+ }
+
+ builder.ReplaceText("[n]"u8, ReadOnlySeString.FromText(nounParams.Quantity.ToString()));
+
+ ross = builder.ToReadOnlySeString();
+ LSeStringBuilder.SharedPool.Return(builder);
+ return ross;
+ }
+}
diff --git a/Dalamud/Game/Text/SeStringHandling/Payloads/ItemPayload.cs b/Dalamud/Game/Text/SeStringHandling/Payloads/ItemPayload.cs
index c31707ff2..d6fd897b8 100644
--- a/Dalamud/Game/Text/SeStringHandling/Payloads/ItemPayload.cs
+++ b/Dalamud/Game/Text/SeStringHandling/Payloads/ItemPayload.cs
@@ -4,6 +4,8 @@ using System.Linq;
using System.Text;
using Dalamud.Data;
+using Dalamud.Utility;
+
using Lumina.Excel;
using Lumina.Excel.Sheets;
using Newtonsoft.Json;
@@ -73,6 +75,7 @@ public class ItemPayload : Payload
///
/// Kinds of items that can be fetched from this payload.
///
+ [Api12ToDo("Move this out of ItemPayload. It's used in other classes too.")]
public enum ItemKind : uint
{
///
@@ -121,7 +124,7 @@ public class ItemPayload : Payload
/// Gets the actual item ID of this payload.
///
[JsonIgnore]
- public uint ItemId => GetAdjustedId(this.rawItemId).ItemId;
+ public uint ItemId => ItemUtil.GetBaseId(this.rawItemId).ItemId;
///
/// Gets the raw, unadjusted item ID of this payload.
@@ -161,7 +164,7 @@ public class ItemPayload : Payload
/// The created item payload.
public static ItemPayload FromRaw(uint rawItemId, string? displayNameOverride = null)
{
- var (id, kind) = GetAdjustedId(rawItemId);
+ var (id, kind) = ItemUtil.GetBaseId(rawItemId);
return new ItemPayload(id, kind, displayNameOverride);
}
@@ -230,7 +233,7 @@ public class ItemPayload : Payload
protected override void DecodeImpl(BinaryReader reader, long endOfStream)
{
this.rawItemId = GetInteger(reader);
- this.Kind = GetAdjustedId(this.rawItemId).Kind;
+ this.Kind = ItemUtil.GetBaseId(this.rawItemId).Kind;
if (reader.BaseStream.Position + 3 < endOfStream)
{
@@ -255,15 +258,4 @@ public class ItemPayload : Payload
this.displayName = Encoding.UTF8.GetString(itemNameBytes);
}
}
-
- private static (uint ItemId, ItemKind Kind) GetAdjustedId(uint rawItemId)
- {
- return rawItemId switch
- {
- > 500_000 and < 1_000_000 => (rawItemId - 500_000, ItemKind.Collectible),
- > 1_000_000 and < 2_000_000 => (rawItemId - 1_000_000, ItemKind.Hq),
- > 2_000_000 => (rawItemId, ItemKind.EventItem), // EventItem IDs are NOT adjusted
- _ => (rawItemId, ItemKind.Normal),
- };
- }
}
diff --git a/Dalamud/Game/Text/SeStringHandling/SeString.cs b/Dalamud/Game/Text/SeStringHandling/SeString.cs
index 7f1955da5..b7618305a 100644
--- a/Dalamud/Game/Text/SeStringHandling/SeString.cs
+++ b/Dalamud/Game/Text/SeStringHandling/SeString.cs
@@ -5,11 +5,16 @@ using System.Runtime.InteropServices;
using System.Text;
using Dalamud.Data;
+using Dalamud.Game.Text.Evaluator;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Utility;
+
using Lumina.Excel.Sheets;
+
using Newtonsoft.Json;
+using LSeStringBuilder = Lumina.Text.SeStringBuilder;
+
namespace Dalamud.Game.Text.SeStringHandling;
///
@@ -187,57 +192,32 @@ public class SeString
/// An SeString containing all the payloads necessary to display an item link in the chat log.
public static SeString CreateItemLink(uint itemId, ItemPayload.ItemKind kind = ItemPayload.ItemKind.Normal, string? displayNameOverride = null)
{
- var data = Service.Get();
+ var clientState = Service.Get();
+ var seStringEvaluator = Service.Get();
- var displayName = displayNameOverride;
- var rarity = 1; // default: white
- if (displayName == null)
- {
- switch (kind)
- {
- case ItemPayload.ItemKind.Normal:
- case ItemPayload.ItemKind.Collectible:
- case ItemPayload.ItemKind.Hq:
- var item = data.GetExcelSheet- ()?.GetRowOrDefault(itemId);
- displayName = item?.Name.ExtractText();
- rarity = item?.Rarity ?? 1;
- break;
- case ItemPayload.ItemKind.EventItem:
- displayName = data.GetExcelSheet()?.GetRowOrDefault(itemId)?.Name.ExtractText();
- break;
- default:
- throw new ArgumentOutOfRangeException(nameof(kind), kind, null);
- }
- }
+ var rawId = ItemUtil.GetRawId(itemId, kind);
- if (displayName == null)
- {
+ var displayName = displayNameOverride ?? ItemUtil.GetItemName(rawId);
+ if (displayName.IsEmpty)
throw new Exception("Invalid item ID specified, could not determine item name.");
- }
- if (kind == ItemPayload.ItemKind.Hq)
- {
- displayName += $" {(char)SeIconChar.HighQuality}";
- }
- else if (kind == ItemPayload.ItemKind.Collectible)
- {
- displayName += $" {(char)SeIconChar.Collectible}";
- }
+ var copyName = ItemUtil.GetItemName(rawId, false).ExtractText();
+ var textColor = ItemUtil.GetItemRarityColorType(rawId);
+ var textEdgeColor = textColor + 1u;
- var textColor = (ushort)(549 + ((rarity - 1) * 2));
- var textGlowColor = (ushort)(textColor + 1);
+ var sb = LSeStringBuilder.SharedPool.Get();
+ var itemLink = sb
+ .PushColorType(textColor)
+ .PushEdgeColorType(textEdgeColor)
+ .PushLinkItem(rawId, copyName)
+ .Append(displayName)
+ .PopLink()
+ .PopEdgeColorType()
+ .PopColorType()
+ .ToReadOnlySeString();
+ LSeStringBuilder.SharedPool.Return(sb);
- // Note: `SeStringBuilder.AddItemLink` uses this function, so don't call it here!
- return new SeStringBuilder()
- .AddUiForeground(textColor)
- .AddUiGlow(textGlowColor)
- .Add(new ItemPayload(itemId, kind))
- .Append(TextArrowPayloads)
- .AddText(displayName)
- .AddUiGlowOff()
- .AddUiForegroundOff()
- .Add(RawPayload.LinkTerminator)
- .Build();
+ return SeString.Parse(seStringEvaluator.EvaluateFromAddon(371, [itemLink], clientState.ClientLanguage));
}
///
@@ -301,7 +281,7 @@ public class SeString
public static SeString CreateMapLink(
uint territoryId, uint mapId, float xCoord, float yCoord, float fudgeFactor = 0.05f) =>
CreateMapLinkWithInstance(territoryId, mapId, null, xCoord, yCoord, fudgeFactor);
-
+
///
/// Creates an SeString representing an entire Payload chain that can be used to link a map position in the chat log.
///
@@ -340,7 +320,7 @@ public class SeString
/// An SeString containing all of the payloads necessary to display a map link in the chat log.
public static SeString? CreateMapLink(string placeName, float xCoord, float yCoord, float fudgeFactor = 0.05f) =>
CreateMapLinkWithInstance(placeName, null, xCoord, yCoord, fudgeFactor);
-
+
///
/// Creates an SeString representing an entire Payload chain that can be used to link a map position in the chat log, matching a specified zone name.
/// Returns null if no corresponding PlaceName was found.
@@ -511,7 +491,7 @@ public class SeString
{
messageBytes.AddRange(p.Encode());
}
-
+
// Add Null Terminator
messageBytes.Add(0);
@@ -526,7 +506,7 @@ public class SeString
{
return this.TextValue;
}
-
+
private static string GetMapLinkNameString(string placeName, int? instance, string coordinateString)
{
var instanceString = string.Empty;
@@ -534,7 +514,7 @@ public class SeString
{
instanceString = (SeIconChar.Instance1 + instance.Value - 1).ToIconString();
}
-
+
return $"{placeName}{instanceString} {coordinateString}";
}
}
diff --git a/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs
index 4937e4af0..9221c2dc5 100644
--- a/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs
+++ b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs
@@ -7,7 +7,6 @@ using BitFaster.Caching.Lru;
using Dalamud.Data;
using Dalamud.Game;
-using Dalamud.Game.Config;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Interface.ImGuiSeStringRenderer.Internal.TextProcessing;
using Dalamud.Interface.Utility;
@@ -44,9 +43,6 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
/// of this placeholder. On its own, usually displayed like [OBJ].
private const int ObjectReplacementCharacter = '\uFFFC';
- [ServiceManager.ServiceDependency]
- private readonly GameConfig gameConfig = Service.Get();
-
/// Cache of compiled SeStrings from .
private readonly ConcurrentLru cache = new(1024);
@@ -570,70 +566,16 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
// Apply gamepad key mapping to icons.
case MacroCode.Icon2
when payload.TryGetExpression(out var icon) && icon.TryGetInt(out var iconId):
- var configName = (BitmapFontIcon)iconId switch
+ ref var iconMapping = ref RaptureAtkModule.Instance()->AtkFontManager.Icon2RemapTable;
+ for (var i = 0; i < 30; i++)
{
- ControllerShoulderLeft => SystemConfigOption.PadButton_L1,
- ControllerShoulderRight => SystemConfigOption.PadButton_R1,
- ControllerTriggerLeft => SystemConfigOption.PadButton_L2,
- ControllerTriggerRight => SystemConfigOption.PadButton_R2,
- ControllerButton3 => SystemConfigOption.PadButton_Triangle,
- ControllerButton1 => SystemConfigOption.PadButton_Cross,
- ControllerButton0 => SystemConfigOption.PadButton_Circle,
- ControllerButton2 => SystemConfigOption.PadButton_Square,
- ControllerStart => SystemConfigOption.PadButton_Start,
- ControllerBack => SystemConfigOption.PadButton_Select,
- ControllerAnalogLeftStick => SystemConfigOption.PadButton_LS,
- ControllerAnalogLeftStickIn => SystemConfigOption.PadButton_LS,
- ControllerAnalogLeftStickUpDown => SystemConfigOption.PadButton_LS,
- ControllerAnalogLeftStickLeftRight => SystemConfigOption.PadButton_LS,
- ControllerAnalogRightStick => SystemConfigOption.PadButton_RS,
- ControllerAnalogRightStickIn => SystemConfigOption.PadButton_RS,
- ControllerAnalogRightStickUpDown => SystemConfigOption.PadButton_RS,
- ControllerAnalogRightStickLeftRight => SystemConfigOption.PadButton_RS,
- _ => (SystemConfigOption?)null,
- };
-
- if (configName is null || !this.gameConfig.TryGet(configName.Value, out PadButtonValue pb))
- return (BitmapFontIcon)iconId;
-
- return pb switch
- {
- PadButtonValue.Autorun_Support => ControllerShoulderLeft,
- PadButtonValue.Hotbar_Set_Change => ControllerShoulderRight,
- PadButtonValue.XHB_Left_Start => ControllerTriggerLeft,
- PadButtonValue.XHB_Right_Start => ControllerTriggerRight,
- PadButtonValue.Jump => ControllerButton3,
- PadButtonValue.Accept => ControllerButton0,
- PadButtonValue.Cancel => ControllerButton1,
- PadButtonValue.Map_Sub => ControllerButton2,
- PadButtonValue.MainCommand => ControllerStart,
- PadButtonValue.HUD_Select => ControllerBack,
- PadButtonValue.Move_Operation => (BitmapFontIcon)iconId switch
+ if (iconMapping[i].IconId == iconId)
{
- ControllerAnalogLeftStick => ControllerAnalogLeftStick,
- ControllerAnalogLeftStickIn => ControllerAnalogLeftStickIn,
- ControllerAnalogLeftStickUpDown => ControllerAnalogLeftStickUpDown,
- ControllerAnalogLeftStickLeftRight => ControllerAnalogLeftStickLeftRight,
- ControllerAnalogRightStick => ControllerAnalogLeftStick,
- ControllerAnalogRightStickIn => ControllerAnalogLeftStickIn,
- ControllerAnalogRightStickUpDown => ControllerAnalogLeftStickUpDown,
- ControllerAnalogRightStickLeftRight => ControllerAnalogLeftStickLeftRight,
- _ => (BitmapFontIcon)iconId,
- },
- PadButtonValue.Camera_Operation => (BitmapFontIcon)iconId switch
- {
- ControllerAnalogLeftStick => ControllerAnalogRightStick,
- ControllerAnalogLeftStickIn => ControllerAnalogRightStickIn,
- ControllerAnalogLeftStickUpDown => ControllerAnalogRightStickUpDown,
- ControllerAnalogLeftStickLeftRight => ControllerAnalogRightStickLeftRight,
- ControllerAnalogRightStick => ControllerAnalogRightStick,
- ControllerAnalogRightStickIn => ControllerAnalogRightStickIn,
- ControllerAnalogRightStickUpDown => ControllerAnalogRightStickUpDown,
- ControllerAnalogRightStickLeftRight => ControllerAnalogRightStickLeftRight,
- _ => (BitmapFontIcon)iconId,
- },
- _ => (BitmapFontIcon)iconId,
- };
+ return (BitmapFontIcon)iconMapping[i].RemappedIconId;
+ }
+ }
+
+ return (BitmapFontIcon)iconId;
}
return None;
diff --git a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs
index f3ec882fc..7326f6745 100644
--- a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs
+++ b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs
@@ -47,11 +47,13 @@ internal class DataWindow : Window, IDisposable
new KeyStateWidget(),
new MarketBoardWidget(),
new NetworkMonitorWidget(),
+ new NounProcessorWidget(),
new ObjectTableWidget(),
new PartyListWidget(),
new PluginIpcWidget(),
new SeFontTestWidget(),
new ServicesWidget(),
+ new SeStringCreatorWidget(),
new SeStringRendererTestWidget(),
new StartInfoWidget(),
new TargetWidget(),
@@ -68,6 +70,7 @@ internal class DataWindow : Window, IDisposable
private bool isExcept;
private bool selectionCollapsed;
private IDataWindowWidget currentWidget;
+ private bool isLoaded;
///
/// Initializes a new instance of the class.
@@ -81,8 +84,6 @@ internal class DataWindow : Window, IDisposable
this.RespectCloseHotkey = false;
this.orderedModules = this.modules.OrderBy(module => module.DisplayName);
this.currentWidget = this.orderedModules.First();
-
- this.Load();
}
///
@@ -91,6 +92,7 @@ internal class DataWindow : Window, IDisposable
///
public override void OnOpen()
{
+ this.Load();
}
///
@@ -183,6 +185,7 @@ internal class DataWindow : Window, IDisposable
if (ImGuiComponents.IconButton("forceReload", FontAwesomeIcon.Sync))
{
+ this.isLoaded = false;
this.Load();
}
@@ -236,6 +239,11 @@ internal class DataWindow : Window, IDisposable
private void Load()
{
+ if (this.isLoaded)
+ return;
+
+ this.isLoaded = true;
+
foreach (var widget in this.modules)
{
widget.Load();
diff --git a/Dalamud/Interface/Internal/Windows/Data/WidgetUtil.cs b/Dalamud/Interface/Internal/Windows/Data/WidgetUtil.cs
new file mode 100644
index 000000000..209970e24
--- /dev/null
+++ b/Dalamud/Interface/Internal/Windows/Data/WidgetUtil.cs
@@ -0,0 +1,34 @@
+using Dalamud.Interface.Utility;
+
+using ImGuiNET;
+
+namespace Dalamud.Interface.Internal.Windows.Data;
+
+///
+/// Common utilities used in Widgets.
+///
+internal class WidgetUtil
+{
+ ///
+ /// Draws text that can be copied on click.
+ ///
+ /// The text shown and to be copied.
+ /// The text in the tooltip.
+ internal static void DrawCopyableText(string text, string tooltipText = "Copy")
+ {
+ ImGuiHelpers.SafeTextWrapped(text);
+
+ if (ImGui.IsItemHovered())
+ {
+ ImGui.SetMouseCursor(ImGuiMouseCursor.Hand);
+ ImGui.BeginTooltip();
+ ImGui.TextUnformatted(tooltipText);
+ ImGui.EndTooltip();
+ }
+
+ if (ImGui.IsItemClicked())
+ {
+ ImGui.SetClipboardText(text);
+ }
+ }
+}
diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AtkArrayDataBrowserWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AtkArrayDataBrowserWidget.cs
index 791dc5310..c3499570c 100644
--- a/Dalamud/Interface/Internal/Windows/Data/Widgets/AtkArrayDataBrowserWidget.cs
+++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AtkArrayDataBrowserWidget.cs
@@ -57,24 +57,6 @@ internal unsafe class AtkArrayDataBrowserWidget : IDataWindowWidget
this.DrawExtendArrayTab();
}
- private static void DrawCopyableText(string text, string tooltipText)
- {
- ImGuiHelpers.SafeTextWrapped(text);
-
- if (ImGui.IsItemHovered())
- {
- ImGui.SetMouseCursor(ImGuiMouseCursor.Hand);
- ImGui.BeginTooltip();
- ImGui.TextUnformatted(tooltipText);
- ImGui.EndTooltip();
- }
-
- if (ImGui.IsItemClicked())
- {
- ImGui.SetClipboardText(text);
- }
- }
-
private void DrawArrayList(Type? arrayType, int arrayCount, short* arrayKeys, AtkArrayData** arrays, ref int selectedIndex)
{
using var table = ImRaii.Table("ArkArrayTable", 3, ImGuiTableFlags.ScrollY | ImGuiTableFlags.Borders, new Vector2(300, -1));
@@ -162,7 +144,7 @@ internal unsafe class AtkArrayDataBrowserWidget : IDataWindowWidget
ImGui.SameLine();
ImGui.TextUnformatted("Address: ");
ImGui.SameLine(0, 0);
- DrawCopyableText($"0x{(nint)array:X}", "Copy address");
+ WidgetUtil.DrawCopyableText($"0x{(nint)array:X}", "Copy address");
if (array->SubscribedAddonsCount > 0)
{
@@ -238,22 +220,22 @@ internal unsafe class AtkArrayDataBrowserWidget : IDataWindowWidget
var ptr = &array->IntArray[i];
ImGui.TableNextColumn(); // Address
- DrawCopyableText($"0x{(nint)ptr:X}", "Copy entry address");
+ WidgetUtil.DrawCopyableText($"0x{(nint)ptr:X}", "Copy entry address");
ImGui.TableNextColumn(); // Integer
- DrawCopyableText((*ptr).ToString(), "Copy value");
+ WidgetUtil.DrawCopyableText((*ptr).ToString(), "Copy value");
ImGui.TableNextColumn(); // Short
- DrawCopyableText((*(short*)ptr).ToString(), "Copy as short");
+ WidgetUtil.DrawCopyableText((*(short*)ptr).ToString(), "Copy as short");
ImGui.TableNextColumn(); // Byte
- DrawCopyableText((*(byte*)ptr).ToString(), "Copy as byte");
+ WidgetUtil.DrawCopyableText((*(byte*)ptr).ToString(), "Copy as byte");
ImGui.TableNextColumn(); // Float
- DrawCopyableText((*(float*)ptr).ToString(), "Copy as float");
+ WidgetUtil.DrawCopyableText((*(float*)ptr).ToString(), "Copy as float");
ImGui.TableNextColumn(); // Hex
- DrawCopyableText($"0x{array->IntArray[i]:X2}", "Copy Hex");
+ WidgetUtil.DrawCopyableText($"0x{array->IntArray[i]:X2}", "Copy Hex");
}
}
@@ -333,11 +315,11 @@ internal unsafe class AtkArrayDataBrowserWidget : IDataWindowWidget
if (this.showTextAddress)
{
if (!isNull)
- DrawCopyableText($"0x{(nint)array->StringArray[i]:X}", "Copy text address");
+ WidgetUtil.DrawCopyableText($"0x{(nint)array->StringArray[i]:X}", "Copy text address");
}
else
{
- DrawCopyableText($"0x{(nint)(&array->StringArray[i]):X}", "Copy entry address");
+ WidgetUtil.DrawCopyableText($"0x{(nint)(&array->StringArray[i]):X}", "Copy entry address");
}
ImGui.TableNextColumn(); // Managed
@@ -351,7 +333,7 @@ internal unsafe class AtkArrayDataBrowserWidget : IDataWindowWidget
{
if (this.showMacroString)
{
- DrawCopyableText(new ReadOnlySeStringSpan(array->StringArray[i]).ToString(), "Copy text");
+ WidgetUtil.DrawCopyableText(new ReadOnlySeStringSpan(array->StringArray[i]).ToString(), "Copy text");
}
else
{
@@ -408,11 +390,11 @@ internal unsafe class AtkArrayDataBrowserWidget : IDataWindowWidget
ImGui.TextUnformatted($"#{i}");
ImGui.TableNextColumn(); // Address
- DrawCopyableText($"0x{(nint)(&array->DataArray[i]):X}", "Copy entry address");
+ WidgetUtil.DrawCopyableText($"0x{(nint)(&array->DataArray[i]):X}", "Copy entry address");
ImGui.TableNextColumn(); // Pointer
if (!isNull)
- DrawCopyableText($"0x{(nint)array->DataArray[i]:X}", "Copy address");
+ WidgetUtil.DrawCopyableText($"0x{(nint)array->DataArray[i]:X}", "Copy address");
}
}
}
diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/FateTableWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/FateTableWidget.cs
index 1a43f2b2d..34b04dae0 100644
--- a/Dalamud/Interface/Internal/Windows/Data/Widgets/FateTableWidget.cs
+++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/FateTableWidget.cs
@@ -69,10 +69,10 @@ internal class FateTableWidget : IDataWindowWidget
ImGui.TextUnformatted($"#{i}");
ImGui.TableNextColumn(); // Address
- DrawCopyableText($"0x{fate.Address:X}", "Click to copy Address");
+ WidgetUtil.DrawCopyableText($"0x{fate.Address:X}", "Click to copy Address");
ImGui.TableNextColumn(); // FateId
- DrawCopyableText(fate.FateId.ToString(), "Click to copy FateId (RowId of Fate sheet)");
+ WidgetUtil.DrawCopyableText(fate.FateId.ToString(), "Click to copy FateId (RowId of Fate sheet)");
ImGui.TableNextColumn(); // State
ImGui.TextUnformatted(fate.State.ToString());
@@ -140,7 +140,7 @@ internal class FateTableWidget : IDataWindowWidget
ImGui.TableNextColumn(); // Name
- DrawCopyableText(fate.Name.ToString(), "Click to copy Name");
+ WidgetUtil.DrawCopyableText(fate.Name.ToString(), "Click to copy Name");
ImGui.TableNextColumn(); // Progress
ImGui.TextUnformatted($"{fate.Progress}%");
@@ -156,28 +156,10 @@ internal class FateTableWidget : IDataWindowWidget
ImGui.TextUnformatted(fate.HasBonus.ToString());
ImGui.TableNextColumn(); // Position
- DrawCopyableText(fate.Position.ToString(), "Click to copy Position");
+ WidgetUtil.DrawCopyableText(fate.Position.ToString(), "Click to copy Position");
ImGui.TableNextColumn(); // Radius
- DrawCopyableText(fate.Radius.ToString(), "Click to copy Radius");
- }
- }
-
- private static void DrawCopyableText(string text, string tooltipText)
- {
- ImGuiHelpers.SafeTextWrapped(text);
-
- if (ImGui.IsItemHovered())
- {
- ImGui.SetMouseCursor(ImGuiMouseCursor.Hand);
- ImGui.BeginTooltip();
- ImGui.TextUnformatted(tooltipText);
- ImGui.EndTooltip();
- }
-
- if (ImGui.IsItemClicked())
- {
- ImGui.SetClipboardText(text);
+ WidgetUtil.DrawCopyableText(fate.Radius.ToString(), "Click to copy Radius");
}
}
}
diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/NounProcessorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/NounProcessorWidget.cs
new file mode 100644
index 000000000..bc0bd0ac9
--- /dev/null
+++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/NounProcessorWidget.cs
@@ -0,0 +1,207 @@
+using System.Linq;
+using System.Text;
+
+using Dalamud.Data;
+using Dalamud.Game;
+using Dalamud.Game.ClientState;
+using Dalamud.Game.Text.Noun;
+using Dalamud.Game.Text.Noun.Enums;
+using Dalamud.Interface.Utility.Raii;
+
+using ImGuiNET;
+using Lumina.Data;
+using Lumina.Excel;
+using Lumina.Excel.Sheets;
+
+namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
+
+///
+/// Widget for the NounProcessor service.
+///
+internal class NounProcessorWidget : IDataWindowWidget
+{
+ /// A list of German grammatical cases.
+ internal static readonly string[] GermanCases = ["Nominative", "Genitive", "Dative", "Accusative"];
+
+ private static readonly Type[] NounSheets = [
+ typeof(Aetheryte),
+ typeof(BNpcName),
+ typeof(BeastTribe),
+ typeof(DeepDungeonEquipment),
+ typeof(DeepDungeonItem),
+ typeof(DeepDungeonMagicStone),
+ typeof(DeepDungeonDemiclone),
+ typeof(ENpcResident),
+ typeof(EObjName),
+ typeof(EurekaAetherItem),
+ typeof(EventItem),
+ typeof(GCRankGridaniaFemaleText),
+ typeof(GCRankGridaniaMaleText),
+ typeof(GCRankLimsaFemaleText),
+ typeof(GCRankLimsaMaleText),
+ typeof(GCRankUldahFemaleText),
+ typeof(GCRankUldahMaleText),
+ typeof(GatheringPointName),
+ typeof(Glasses),
+ typeof(GlassesStyle),
+ typeof(HousingPreset),
+ typeof(Item),
+ typeof(MJIName),
+ typeof(Mount),
+ typeof(Ornament),
+ typeof(TripleTriadCard),
+ ];
+
+ private ClientLanguage[] languages = [];
+ private string[] languageNames = [];
+
+ private int selectedSheetNameIndex = 0;
+ private int selectedLanguageIndex = 0;
+ private int rowId = 1;
+ private int amount = 1;
+
+ ///
+ public string[]? CommandShortcuts { get; init; } = { "noun" };
+
+ ///
+ public string DisplayName { get; init; } = "Noun Processor";
+
+ ///
+ public bool Ready { get; set; }
+
+ ///
+ public void Load()
+ {
+ this.languages = Enum.GetValues();
+ this.languageNames = Enum.GetNames();
+ this.selectedLanguageIndex = (int)Service.Get().ClientLanguage;
+
+ this.Ready = true;
+ }
+
+ ///
+ public void Draw()
+ {
+ var nounProcessor = Service.Get();
+ var dataManager = Service.Get();
+ var clientState = Service.Get();
+
+ var sheetType = NounSheets.ElementAt(this.selectedSheetNameIndex);
+ var language = this.languages[this.selectedLanguageIndex];
+
+ ImGui.SetNextItemWidth(300);
+ if (ImGui.Combo("###SelectedSheetName", ref this.selectedSheetNameIndex, NounSheets.Select(t => t.Name).ToArray(), NounSheets.Length))
+ {
+ this.rowId = 1;
+ }
+
+ ImGui.SameLine();
+
+ ImGui.SetNextItemWidth(120);
+ if (ImGui.Combo("###SelectedLanguage", ref this.selectedLanguageIndex, this.languageNames, this.languageNames.Length))
+ {
+ language = this.languages[this.selectedLanguageIndex];
+ this.rowId = 1;
+ }
+
+ ImGui.SetNextItemWidth(120);
+ var sheet = dataManager.Excel.GetSheet(Language.English, sheetType.Name);
+ var minRowId = (int)sheet.FirstOrDefault().RowId;
+ var maxRowId = (int)sheet.LastOrDefault().RowId;
+ if (ImGui.InputInt("RowId###RowId", ref this.rowId, 1, 10, ImGuiInputTextFlags.AutoSelectAll))
+ {
+ if (this.rowId < minRowId)
+ this.rowId = minRowId;
+
+ if (this.rowId >= maxRowId)
+ this.rowId = maxRowId;
+ }
+
+ ImGui.SameLine();
+ ImGui.TextUnformatted($"(Range: {minRowId} - {maxRowId})");
+
+ ImGui.SetNextItemWidth(120);
+ if (ImGui.InputInt("Amount###Amount", ref this.amount, 1, 10, ImGuiInputTextFlags.AutoSelectAll))
+ {
+ if (this.amount <= 0)
+ this.amount = 1;
+ }
+
+ var articleTypeEnumType = language switch
+ {
+ ClientLanguage.Japanese => typeof(JapaneseArticleType),
+ ClientLanguage.German => typeof(GermanArticleType),
+ ClientLanguage.French => typeof(FrenchArticleType),
+ _ => typeof(EnglishArticleType),
+ };
+
+ var numCases = language == ClientLanguage.German ? 4 : 1;
+
+#if DEBUG
+ if (ImGui.Button("Copy as self-test entry"))
+ {
+ var sb = new StringBuilder();
+
+ foreach (var articleType in Enum.GetValues(articleTypeEnumType))
+ {
+ for (var grammaticalCase = 0; grammaticalCase < numCases; grammaticalCase++)
+ {
+ var nounParams = new NounParams()
+ {
+ SheetName = sheetType.Name,
+ RowId = (uint)this.rowId,
+ Language = language,
+ Quantity = this.amount,
+ ArticleType = (int)articleType,
+ GrammaticalCase = grammaticalCase,
+ };
+ var output = nounProcessor.ProcessNoun(nounParams).ExtractText().Replace("\"", "\\\"");
+ var caseParam = language == ClientLanguage.German ? $"(int)GermanCases.{GermanCases[grammaticalCase]}" : "1";
+ sb.AppendLine($"new(nameof(LSheets.{sheetType.Name}), {this.rowId}, ClientLanguage.{language}, {this.amount}, (int){articleTypeEnumType.Name}.{Enum.GetName(articleTypeEnumType, articleType)}, {caseParam}, \"{output}\"),");
+ }
+ }
+
+ ImGui.SetClipboardText(sb.ToString());
+ }
+#endif
+
+ using var table = ImRaii.Table("TextDecoderTable", 1 + numCases, ImGuiTableFlags.ScrollY | ImGuiTableFlags.RowBg | ImGuiTableFlags.Borders | ImGuiTableFlags.NoSavedSettings);
+ if (!table) return;
+
+ ImGui.TableSetupColumn("ArticleType", ImGuiTableColumnFlags.WidthFixed, 150);
+ for (var i = 0; i < numCases; i++)
+ ImGui.TableSetupColumn(language == ClientLanguage.German ? GermanCases[i] : "Text");
+ ImGui.TableSetupScrollFreeze(6, 1);
+ ImGui.TableHeadersRow();
+
+ foreach (var articleType in Enum.GetValues(articleTypeEnumType))
+ {
+ ImGui.TableNextRow();
+ ImGui.TableNextColumn();
+ ImGui.TableHeader(articleType.ToString());
+
+ for (var currentCase = 0; currentCase < numCases; currentCase++)
+ {
+ ImGui.TableNextColumn();
+
+ try
+ {
+ var nounParams = new NounParams()
+ {
+ SheetName = sheetType.Name,
+ RowId = (uint)this.rowId,
+ Language = language,
+ Quantity = this.amount,
+ ArticleType = (int)articleType,
+ GrammaticalCase = currentCase,
+ };
+ ImGui.TextUnformatted(nounProcessor.ProcessNoun(nounParams).ExtractText());
+ }
+ catch (Exception ex)
+ {
+ ImGui.TextUnformatted(ex.ToString());
+ }
+ }
+ }
+ }
+}
diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs
new file mode 100644
index 000000000..2a56cb6c7
--- /dev/null
+++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs
@@ -0,0 +1,1276 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Numerics;
+using System.Text;
+
+using Dalamud.Configuration.Internal;
+using Dalamud.Data;
+using Dalamud.Game;
+using Dalamud.Game.Text.Evaluator;
+using Dalamud.Game.Text.Noun.Enums;
+using Dalamud.Game.Text.SeStringHandling;
+using Dalamud.Interface.Utility;
+using Dalamud.Interface.Utility.Raii;
+using Dalamud.Memory;
+using Dalamud.Utility;
+
+using FFXIVClientStructs.FFXIV.Client.System.String;
+using FFXIVClientStructs.FFXIV.Client.UI;
+using FFXIVClientStructs.FFXIV.Client.UI.Misc;
+using FFXIVClientStructs.FFXIV.Component.Text;
+
+using ImGuiNET;
+
+using Lumina.Data;
+using Lumina.Data.Files.Excel;
+using Lumina.Data.Structs.Excel;
+using Lumina.Excel;
+using Lumina.Excel.Sheets;
+using Lumina.Text.Expressions;
+using Lumina.Text.Payloads;
+using Lumina.Text.ReadOnly;
+
+using LSeStringBuilder = Lumina.Text.SeStringBuilder;
+
+namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
+
+///
+/// Widget to create SeStrings.
+///
+internal class SeStringCreatorWidget : IDataWindowWidget
+{
+ private const LinkMacroPayloadType DalamudLinkType = (LinkMacroPayloadType)Payload.EmbeddedInfoType.DalamudLink - 1;
+
+ private readonly Dictionary expressionNames = new()
+ {
+ { MacroCode.SetResetTime, ["Hour", "WeekDay"] },
+ { MacroCode.SetTime, ["Time"] },
+ { MacroCode.If, ["Condition", "StatementTrue", "StatementFalse"] },
+ { MacroCode.Switch, ["Condition"] },
+ { MacroCode.PcName, ["EntityId"] },
+ { MacroCode.IfPcGender, ["EntityId", "CaseMale", "CaseFemale"] },
+ { MacroCode.IfPcName, ["EntityId", "CaseTrue", "CaseFalse"] },
+ // { MacroCode.Josa, [] },
+ // { MacroCode.Josaro, [] },
+ { MacroCode.IfSelf, ["EntityId", "CaseTrue", "CaseFalse"] },
+ // { MacroCode.NewLine, [] },
+ { MacroCode.Wait, ["Seconds"] },
+ { MacroCode.Icon, ["IconId"] },
+ { MacroCode.Color, ["Color"] },
+ { MacroCode.EdgeColor, ["Color"] },
+ { MacroCode.ShadowColor, ["Color"] },
+ // { MacroCode.SoftHyphen, [] },
+ // { MacroCode.Key, [] },
+ // { MacroCode.Scale, [] },
+ { MacroCode.Bold, ["Enabled"] },
+ { MacroCode.Italic, ["Enabled"] },
+ // { MacroCode.Edge, [] },
+ // { MacroCode.Shadow, [] },
+ // { MacroCode.NonBreakingSpace, [] },
+ { MacroCode.Icon2, ["IconId"] },
+ // { MacroCode.Hyphen, [] },
+ { MacroCode.Num, ["Value"] },
+ { MacroCode.Hex, ["Value"] },
+ { MacroCode.Kilo, ["Value", "Separator"] },
+ { MacroCode.Byte, ["Value"] },
+ { MacroCode.Sec, ["Time"] },
+ { MacroCode.Time, ["Value"] },
+ { MacroCode.Float, ["Value", "Radix", "Separator"] },
+ { MacroCode.Link, ["Type"] },
+ { MacroCode.Sheet, ["SheetName", "RowId", "ColumnIndex", "ColumnParam"] },
+ { MacroCode.String, ["String"] },
+ { MacroCode.Caps, ["String"] },
+ { MacroCode.Head, ["String"] },
+ { MacroCode.Split, ["String", "Separator"] },
+ { MacroCode.HeadAll, ["String"] },
+ // { MacroCode.Fixed, [] },
+ { MacroCode.Lower, ["String"] },
+ { MacroCode.JaNoun, ["SheetName", "ArticleType", "RowId", "Amount", "Case", "UnkInt5"] },
+ { MacroCode.EnNoun, ["SheetName", "ArticleType", "RowId", "Amount", "Case", "UnkInt5"] },
+ { MacroCode.DeNoun, ["SheetName", "ArticleType", "RowId", "Amount", "Case", "UnkInt5"] },
+ { MacroCode.FrNoun, ["SheetName", "ArticleType", "RowId", "Amount", "Case", "UnkInt5"] },
+ { MacroCode.ChNoun, ["SheetName", "ArticleType", "RowId", "Amount", "Case", "UnkInt5"] },
+ { MacroCode.LowerHead, ["String"] },
+ { MacroCode.ColorType, ["ColorType"] },
+ { MacroCode.EdgeColorType, ["ColorType"] },
+ { MacroCode.Digit, ["Value", "TargetLength"] },
+ { MacroCode.Ordinal, ["Value"] },
+ { MacroCode.Sound, ["IsJingle", "SoundId"] },
+ { MacroCode.LevelPos, ["LevelId"] },
+ };
+
+ private readonly Dictionary linkExpressionNames = new()
+ {
+ { LinkMacroPayloadType.Character, ["Flags", "WorldId"] },
+ { LinkMacroPayloadType.Item, ["ItemId", "Rarity"] },
+ { LinkMacroPayloadType.MapPosition, ["TerritoryType/MapId", "RawX", "RawY"] },
+ { LinkMacroPayloadType.Quest, ["QuestId"] },
+ { LinkMacroPayloadType.Achievement, ["AchievementId"] },
+ { LinkMacroPayloadType.HowTo, ["HowToId"] },
+ // PartyFinderNotification
+ { LinkMacroPayloadType.Status, ["StatusId"] },
+ { LinkMacroPayloadType.PartyFinder, ["ListingId", string.Empty, "WorldId"] },
+ { LinkMacroPayloadType.AkatsukiNote, ["AkatsukiNoteId"] },
+ { DalamudLinkType, ["CommandId", "Extra1", "Extra2", "ExtraString"] },
+ };
+
+ private readonly Dictionary fixedExpressionNames = new()
+ {
+ { 1, ["Type0", "Type1", "WorldId"] },
+ { 2, ["Type0", "Type1", "ClassJobId", "Level"] },
+ { 3, ["Type0", "Type1", "TerritoryTypeId", "Instance & MapId", "RawX", "RawY", "RawZ", "PlaceNameIdOverride"] },
+ { 4, ["Type0", "Type1", "ItemId", "Rarity", string.Empty, string.Empty, "Item Name"] },
+ { 5, ["Type0", "Type1", "Sound Effect Id"] },
+ { 6, ["Type0", "Type1", "ObjStrId"] },
+ { 7, ["Type0", "Type1", "Text"] },
+ { 8, ["Type0", "Type1", "Seconds"] },
+ { 9, ["Type0", "Type1", string.Empty] },
+ { 10, ["Type0", "Type1", "StatusId", "HasOverride", "NameOverride", "DescriptionOverride"] },
+ { 11, ["Type0", "Type1", "ListingId", string.Empty, "WorldId", "CrossWorldFlag"] },
+ { 12, ["Type0", "Type1", "QuestId", string.Empty, string.Empty, string.Empty, "QuestName"] },
+ };
+
+ private readonly List entries = [
+ new TextEntry(TextEntryType.String, "Welcome to "),
+ new TextEntry(TextEntryType.Macro, ""),
+ new TextEntry(TextEntryType.Macro, ""),
+ new TextEntry(TextEntryType.String, "Dalamud"),
+ new TextEntry(TextEntryType.Macro, ""),
+ new TextEntry(TextEntryType.Macro, ""),
+ new TextEntry(TextEntryType.Macro, " "),
+ ];
+
+ private SeStringParameter[]? localParameters = [Util.GetScmVersion()];
+ private ReadOnlySeString input;
+ private ClientLanguage? language;
+ private int importSelectedSheetName;
+ private int importRowId;
+ private string[]? validImportSheetNames;
+ private float inputsWidth;
+ private float lastContentWidth;
+
+ private enum TextEntryType
+ {
+ String,
+ Macro,
+ Fixed,
+ }
+
+ ///
+ public string[]? CommandShortcuts { get; init; } = [];
+
+ ///
+ public string DisplayName { get; init; } = "SeString Creator";
+
+ ///
+ public bool Ready { get; set; }
+
+ ///
+ public void Load()
+ {
+ this.language = Service.Get().EffectiveLanguage.ToClientLanguage();
+ this.UpdateInputString(false);
+ this.Ready = true;
+ }
+
+ ///
+ public void Draw()
+ {
+ var contentWidth = ImGui.GetContentRegionAvail().X;
+
+ // split panels in the middle by default
+ if (this.inputsWidth == 0)
+ {
+ this.inputsWidth = contentWidth / 2f;
+ }
+
+ // resize panels relative to the window size
+ if (contentWidth != this.lastContentWidth)
+ {
+ var originalWidth = this.lastContentWidth != 0 ? this.lastContentWidth : contentWidth;
+ this.inputsWidth = this.inputsWidth / originalWidth * contentWidth;
+ this.lastContentWidth = contentWidth;
+ }
+
+ using var tabBar = ImRaii.TabBar("SeStringCreatorWidgetTabBar");
+ if (!tabBar) return;
+
+ this.DrawCreatorTab(contentWidth);
+ this.DrawGlobalParametersTab();
+ }
+
+ private void DrawCreatorTab(float contentWidth)
+ {
+ using var tab = ImRaii.TabItem("Creator");
+ if (!tab) return;
+
+ this.DrawControls();
+ ImGui.Spacing();
+ this.DrawInputs();
+
+ this.localParameters ??= this.GetLocalParameters(this.input.AsSpan(), []);
+
+ var evaluated = Service.Get().Evaluate(
+ this.input.AsSpan(),
+ this.localParameters,
+ this.language);
+
+ ImGui.SameLine(0, 0);
+
+ ImGui.Button("###InputPanelResizer", new Vector2(4, -1));
+ if (ImGui.IsItemActive())
+ {
+ this.inputsWidth += ImGui.GetIO().MouseDelta.X;
+ }
+
+ if (ImGui.IsItemHovered())
+ {
+ ImGui.SetMouseCursor(ImGuiMouseCursor.ResizeEW);
+
+ if (ImGui.IsMouseDoubleClicked(ImGuiMouseButton.Left))
+ {
+ this.inputsWidth = contentWidth / 2f;
+ }
+ }
+
+ ImGui.SameLine();
+
+ using var child = ImRaii.Child("Preview", new Vector2(ImGui.GetContentRegionAvail().X, -1));
+ if (!child) return;
+
+ if (this.localParameters!.Length != 0)
+ {
+ ImGui.Spacing();
+ this.DrawParameters();
+ }
+
+ this.DrawPreview(evaluated);
+
+ ImGui.Spacing();
+ this.DrawPayloads(evaluated);
+ }
+
+ private unsafe void DrawGlobalParametersTab()
+ {
+ using var tab = ImRaii.TabItem("Global Parameters");
+ if (!tab) return;
+
+ using var table = ImRaii.Table("GlobalParametersTable", 5, ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY | ImGuiTableFlags.NoSavedSettings);
+ if (!table) return;
+
+ ImGui.TableSetupColumn("Id", ImGuiTableColumnFlags.WidthFixed, 40);
+ ImGui.TableSetupColumn("Type", ImGuiTableColumnFlags.WidthFixed, 100);
+ ImGui.TableSetupColumn("ValuePtr", ImGuiTableColumnFlags.WidthFixed, 120);
+ ImGui.TableSetupColumn("Value", ImGuiTableColumnFlags.WidthStretch);
+ ImGui.TableSetupColumn("Description", ImGuiTableColumnFlags.WidthStretch);
+ ImGui.TableSetupScrollFreeze(5, 1);
+ ImGui.TableHeadersRow();
+
+ var deque = RaptureTextModule.Instance()->GlobalParameters;
+ for (var i = 0u; i < deque.MySize; i++)
+ {
+ var item = deque[i];
+
+ ImGui.TableNextRow();
+ ImGui.TableNextColumn(); // Id
+ ImGui.TextUnformatted(i.ToString());
+
+ ImGui.TableNextColumn(); // Type
+ ImGui.TextUnformatted(item.Type.ToString());
+
+ ImGui.TableNextColumn(); // ValuePtr
+ WidgetUtil.DrawCopyableText($"0x{(nint)item.ValuePtr:X}");
+
+ ImGui.TableNextColumn(); // Value
+ switch (item.Type)
+ {
+ case TextParameterType.Integer:
+ WidgetUtil.DrawCopyableText($"0x{item.IntValue:X}");
+ ImGui.SameLine();
+ WidgetUtil.DrawCopyableText(item.IntValue.ToString());
+ break;
+
+ case TextParameterType.ReferencedUtf8String:
+ if (item.ReferencedUtf8StringValue != null)
+ WidgetUtil.DrawCopyableText(new ReadOnlySeStringSpan(item.ReferencedUtf8StringValue->Utf8String).ToString());
+ else
+ ImGui.TextUnformatted("null");
+
+ break;
+
+ case TextParameterType.String:
+ if (item.StringValue != null)
+ WidgetUtil.DrawCopyableText(MemoryHelper.ReadStringNullTerminated((nint)item.StringValue));
+ else
+ ImGui.TextUnformatted("null");
+ break;
+ }
+
+ ImGui.TableNextColumn();
+ ImGui.TextUnformatted(i switch
+ {
+ 0 => "Player Name",
+ 1 => "Temp Player 1 Name",
+ 2 => "Temp Player 2 Name",
+ 3 => "Player Sex",
+ 4 => "Temp Player 1 Sex",
+ 5 => "Temp Player 2 Sex",
+ 6 => "Temp Player 1 Unk 1",
+ 7 => "Temp Player 2 Unk 1",
+ 10 => "Eorzea Time Hours",
+ 11 => "Eorzea Time Minutes",
+ 12 => "ColorSay",
+ 13 => "ColorShout",
+ 14 => "ColorTell",
+ 15 => "ColorParty",
+ 16 => "ColorAlliance",
+ 17 => "ColorLS1",
+ 18 => "ColorLS2",
+ 19 => "ColorLS3",
+ 20 => "ColorLS4",
+ 21 => "ColorLS5",
+ 22 => "ColorLS6",
+ 23 => "ColorLS7",
+ 24 => "ColorLS8",
+ 25 => "ColorFCompany",
+ 26 => "ColorPvPGroup",
+ 27 => "ColorPvPGroupAnnounce",
+ 28 => "ColorBeginner",
+ 29 => "ColorEmoteUser",
+ 30 => "ColorEmote",
+ 31 => "ColorYell",
+ 32 => "ColorFCAnnounce",
+ 33 => "ColorBeginnerAnnounce",
+ 34 => "ColorCWLS",
+ 35 => "ColorAttackSuccess",
+ 36 => "ColorAttackFailure",
+ 37 => "ColorAction",
+ 38 => "ColorItem",
+ 39 => "ColorCureGive",
+ 40 => "ColorBuffGive",
+ 41 => "ColorDebuffGive",
+ 42 => "ColorEcho",
+ 43 => "ColorSysMsg",
+ 51 => "Player Grand Company Rank (Maelstrom)",
+ 52 => "Player Grand Company Rank (Twin Adders)",
+ 53 => "Player Grand Company Rank (Immortal Flames)",
+ 54 => "Companion Name",
+ 55 => "Content Name",
+ 56 => "ColorSysBattle",
+ 57 => "ColorSysGathering",
+ 58 => "ColorSysErr",
+ 59 => "ColorNpcSay",
+ 60 => "ColorItemNotice",
+ 61 => "ColorGrowup",
+ 62 => "ColorLoot",
+ 63 => "ColorCraft",
+ 64 => "ColorGathering",
+ 65 => "Temp Player 1 Unk 2",
+ 66 => "Temp Player 2 Unk 2",
+ 67 => "Player ClassJobId",
+ 68 => "Player Level",
+ 70 => "Player Race",
+ 71 => "Player Synced Level",
+ 77 => "Client/Plattform?",
+ 78 => "Player BirthMonth",
+ 82 => "Datacenter Region",
+ 83 => "ColorCWLS2",
+ 84 => "ColorCWLS3",
+ 85 => "ColorCWLS4",
+ 86 => "ColorCWLS5",
+ 87 => "ColorCWLS6",
+ 88 => "ColorCWLS7",
+ 89 => "ColorCWLS8",
+ 91 => "Player Grand Company",
+ 92 => "TerritoryType Id",
+ 93 => "Is Soft Keyboard Enabled",
+ 94 => "LogSetRoleColor 1: LogColorRoleTank",
+ 95 => "LogSetRoleColor 2: LogColorRoleTank",
+ 96 => "LogSetRoleColor 1: LogColorRoleHealer",
+ 97 => "LogSetRoleColor 2: LogColorRoleHealer",
+ 98 => "LogSetRoleColor 1: LogColorRoleDPS",
+ 99 => "LogSetRoleColor 2: LogColorRoleDPS",
+ 100 => "LogSetRoleColor 1: LogColorOtherClass",
+ 101 => "LogSetRoleColor 2: LogColorOtherClass",
+ 102 => "Has Login Security Token",
+ _ => string.Empty,
+ });
+ }
+ }
+
+ private unsafe void DrawControls()
+ {
+ if (ImGui.Button("Add entry"))
+ {
+ this.entries.Add(new(TextEntryType.String, string.Empty));
+ }
+
+ ImGui.SameLine();
+
+ if (ImGui.Button("Add from Sheet"))
+ {
+ ImGui.OpenPopup("AddFromSheetPopup");
+ }
+
+ this.DrawAddFromSheetPopup();
+
+ ImGui.SameLine();
+
+ if (ImGui.Button("Print"))
+ {
+ var output = Utf8String.CreateEmpty();
+ var temp = Utf8String.CreateEmpty();
+ var temp2 = Utf8String.CreateEmpty();
+
+ foreach (var entry in this.entries)
+ {
+ switch (entry.Type)
+ {
+ case TextEntryType.String:
+ output->ConcatCStr(entry.Message);
+ break;
+
+ case TextEntryType.Macro:
+ temp->Clear();
+ RaptureTextModule.Instance()->MacroEncoder.EncodeString(temp, entry.Message);
+ output->Append(temp);
+ break;
+
+ case TextEntryType.Fixed:
+ temp->SetString(entry.Message);
+ temp2->Clear();
+
+ RaptureTextModule.Instance()->TextModule.ProcessMacroCode(temp2, temp->StringPtr);
+ var out1 = PronounModule.Instance()->ProcessString(temp2, true);
+ var out2 = PronounModule.Instance()->ProcessString(out1, false);
+
+ output->Append(out2);
+ break;
+ }
+ }
+
+ RaptureLogModule.Instance()->PrintString(output->StringPtr);
+ temp2->Dtor(true);
+ temp->Dtor(true);
+ output->Dtor(true);
+ }
+
+ ImGui.SameLine();
+
+ if (ImGui.Button("Print Evaluated"))
+ {
+ var sb = new LSeStringBuilder();
+
+ foreach (var entry in this.entries)
+ {
+ switch (entry.Type)
+ {
+ case TextEntryType.String:
+ sb.Append(entry.Message);
+ break;
+
+ case TextEntryType.Macro:
+ case TextEntryType.Fixed:
+ sb.AppendMacroString(entry.Message);
+ break;
+ }
+ }
+
+ RaptureLogModule.Instance()->PrintString(Service.Get().Evaluate(sb.ToReadOnlySeString()));
+ }
+
+ if (this.entries.Count != 0)
+ {
+ ImGui.SameLine();
+
+ if (ImGui.Button("Copy MacroString"))
+ {
+ var sb = new LSeStringBuilder();
+
+ foreach (var entry in this.entries)
+ {
+ switch (entry.Type)
+ {
+ case TextEntryType.String:
+ sb.Append(entry.Message);
+ break;
+
+ case TextEntryType.Macro:
+ case TextEntryType.Fixed:
+ sb.AppendMacroString(entry.Message);
+ break;
+ }
+ }
+
+ ImGui.SetClipboardText(sb.ToReadOnlySeString().ToString());
+ }
+
+ ImGui.SameLine();
+
+ if (ImGui.Button("Clear entries"))
+ {
+ this.entries.Clear();
+ this.UpdateInputString();
+ }
+ }
+
+ var raptureTextModule = RaptureTextModule.Instance();
+ if (!raptureTextModule->MacroEncoder.EncoderError.IsEmpty)
+ {
+ ImGui.SameLine();
+ ImGui.TextUnformatted(raptureTextModule->MacroEncoder.EncoderError.ToString()); // TODO: EncoderError doesn't clear
+ }
+
+ ImGui.SameLine();
+ ImGui.SetNextItemWidth(90 * ImGuiHelpers.GlobalScale);
+ using (var dropdown = ImRaii.Combo("##Language", this.language.ToString() ?? "Language..."))
+ {
+ if (dropdown)
+ {
+ var values = Enum.GetValues().OrderBy((ClientLanguage lang) => lang.ToString());
+ foreach (var value in values)
+ {
+ if (ImGui.Selectable(Enum.GetName(value), value == this.language))
+ {
+ this.language = value;
+ this.UpdateInputString();
+ }
+ }
+ }
+ }
+ }
+
+ private void DrawAddFromSheetPopup()
+ {
+ using var popup = ImRaii.Popup("AddFromSheetPopup");
+ if (!popup) return;
+
+ var dataManager = Service.Get();
+
+ this.validImportSheetNames ??= dataManager.Excel.SheetNames.Where(sheetName =>
+ {
+ try
+ {
+ var headerFile = dataManager.GameData.GetFile($"exd/{sheetName}.exh");
+ if (headerFile.Header.Variant != ExcelVariant.Default)
+ return false;
+
+ var sheet = dataManager.Excel.GetSheet(Language.English, sheetName);
+ return sheet.Columns.Any(col => col.Type == ExcelColumnDataType.String);
+ }
+ catch
+ {
+ return false;
+ }
+ }).OrderBy(sheetName => sheetName, StringComparer.InvariantCulture).ToArray();
+
+ var sheetChanged = ImGui.Combo("Sheet Name", ref this.importSelectedSheetName, this.validImportSheetNames, this.validImportSheetNames.Length);
+
+ try
+ {
+ var sheet = dataManager.Excel.GetSheet(this.language?.ToLumina() ?? Language.English, this.validImportSheetNames[this.importSelectedSheetName]);
+ var minRowId = (int)sheet.FirstOrDefault().RowId;
+ var maxRowId = (int)sheet.LastOrDefault().RowId;
+
+ var rowIdChanged = ImGui.InputInt("RowId", ref this.importRowId, 1, 10);
+
+ ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X);
+ ImGui.TextUnformatted($"(Range: {minRowId} - {maxRowId})");
+
+ if (sheetChanged || rowIdChanged)
+ {
+ if (sheetChanged || this.importRowId < minRowId)
+ this.importRowId = minRowId;
+
+ if (this.importRowId > maxRowId)
+ this.importRowId = maxRowId;
+ }
+
+ if (!sheet.TryGetRow((uint)this.importRowId, out var row))
+ {
+ ImGui.TextColored(new Vector4(1, 0, 0, 1), "Row not found");
+ return;
+ }
+
+ ImGui.TextUnformatted("Select string to add:");
+
+ using var table = ImRaii.Table("StringSelectionTable", 2, ImGuiTableFlags.Borders | ImGuiTableFlags.NoSavedSettings);
+ if (!table) return;
+
+ ImGui.TableSetupColumn("Column", ImGuiTableColumnFlags.WidthFixed, 50);
+ ImGui.TableSetupColumn("Value", ImGuiTableColumnFlags.WidthStretch);
+ ImGui.TableSetupScrollFreeze(0, 1);
+ ImGui.TableHeadersRow();
+
+ for (var i = 0; i < sheet.Columns.Count; i++)
+ {
+ var column = sheet.Columns[i];
+ if (column.Type != ExcelColumnDataType.String)
+ continue;
+
+ var value = row.ReadStringColumn(i);
+ if (value.IsEmpty)
+ continue;
+
+ ImGui.TableNextRow();
+ ImGui.TableNextColumn();
+ ImGui.TextUnformatted(i.ToString());
+
+ ImGui.TableNextColumn();
+ if (ImGui.Selectable($"{value.ToString().Truncate(100)}###Column{i}"))
+ {
+ foreach (var payload in value)
+ {
+ switch (payload.Type)
+ {
+ case ReadOnlySePayloadType.Text:
+ this.entries.Add(new(TextEntryType.String, Encoding.UTF8.GetString(payload.Body.Span)));
+ break;
+
+ case ReadOnlySePayloadType.Macro:
+ this.entries.Add(new(TextEntryType.Macro, payload.ToString()));
+ break;
+ }
+ }
+
+ this.UpdateInputString();
+ ImGui.CloseCurrentPopup();
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ ImGui.TextUnformatted(e.Message);
+ return;
+ }
+ }
+
+ private unsafe void DrawInputs()
+ {
+ using var child = ImRaii.Child("Inputs", new Vector2(this.inputsWidth, -1));
+ if (!child) return;
+
+ using var table = ImRaii.Table("StringMakerTable", 3, ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY | ImGuiTableFlags.NoSavedSettings);
+ if (!table) return;
+
+ ImGui.TableSetupColumn("Type", ImGuiTableColumnFlags.WidthFixed, 100);
+ ImGui.TableSetupColumn("Text", ImGuiTableColumnFlags.WidthStretch);
+ ImGui.TableSetupColumn("Actions", ImGuiTableColumnFlags.WidthFixed, 80);
+ ImGui.TableSetupScrollFreeze(3, 1);
+ ImGui.TableHeadersRow();
+
+ var arrowUpButtonSize = this.GetIconButtonSize(FontAwesomeIcon.ArrowUp);
+ var arrowDownButtonSize = this.GetIconButtonSize(FontAwesomeIcon.ArrowDown);
+ var trashButtonSize = this.GetIconButtonSize(FontAwesomeIcon.Trash);
+ var terminalButtonSize = this.GetIconButtonSize(FontAwesomeIcon.Terminal);
+
+ var entryToRemove = -1;
+ var entryToMoveUp = -1;
+ var entryToMoveDown = -1;
+ var updateString = false;
+
+ for (var i = 0; i < this.entries.Count; i++)
+ {
+ var key = $"##Entry{i}";
+ var entry = this.entries[i];
+
+ ImGui.TableNextRow();
+
+ ImGui.TableNextColumn(); // Type
+ var type = (int)entry.Type;
+ ImGui.SetNextItemWidth(-1);
+ if (ImGui.Combo($"##Type{i}", ref type, ["String", "Macro", "Fixed"], 3))
+ {
+ entry.Type = (TextEntryType)type;
+ updateString |= true;
+ }
+
+ ImGui.TableNextColumn(); // Text
+ var message = entry.Message;
+ ImGui.SetNextItemWidth(-1);
+ if (ImGui.InputText($"##{i}_Message", ref message, 255))
+ {
+ entry.Message = message;
+ updateString |= true;
+ }
+
+ ImGui.TableNextColumn(); // Actions
+
+ if (i > 0)
+ {
+ if (this.IconButton(key + "_Up", FontAwesomeIcon.ArrowUp, "Move up"))
+ {
+ entryToMoveUp = i;
+ }
+ }
+ else
+ {
+ ImGui.Dummy(arrowUpButtonSize);
+ }
+
+ ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X);
+
+ if (i < this.entries.Count - 1)
+ {
+ if (this.IconButton(key + "_Down", FontAwesomeIcon.ArrowDown, "Move down"))
+ {
+ entryToMoveDown = i;
+ }
+ }
+ else
+ {
+ ImGui.Dummy(arrowDownButtonSize);
+ }
+
+ ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X);
+
+ if (ImGui.IsKeyDown(ImGuiKey.LeftShift) || ImGui.IsKeyDown(ImGuiKey.RightShift))
+ {
+ if (this.IconButton(key + "_Delete", FontAwesomeIcon.Trash, "Delete"))
+ {
+ entryToRemove = i;
+ }
+ }
+ else
+ {
+ this.IconButton(
+ key + "_Delete",
+ FontAwesomeIcon.Trash,
+ "Delete with shift",
+ disabled: true);
+ }
+ }
+
+ table.Dispose();
+
+ if (entryToMoveUp != -1)
+ {
+ var removedItem = this.entries[entryToMoveUp];
+ this.entries.RemoveAt(entryToMoveUp);
+ this.entries.Insert(entryToMoveUp - 1, removedItem);
+ updateString |= true;
+ }
+
+ if (entryToMoveDown != -1)
+ {
+ var removedItem = this.entries[entryToMoveDown];
+ this.entries.RemoveAt(entryToMoveDown);
+ this.entries.Insert(entryToMoveDown + 1, removedItem);
+ updateString |= true;
+ }
+
+ if (entryToRemove != -1)
+ {
+ this.entries.RemoveAt(entryToRemove);
+ updateString |= true;
+ }
+
+ if (updateString)
+ {
+ this.UpdateInputString();
+ }
+ }
+
+ private unsafe void UpdateInputString(bool resetLocalParameters = true)
+ {
+ var sb = new LSeStringBuilder();
+
+ foreach (var entry in this.entries)
+ {
+ switch (entry.Type)
+ {
+ case TextEntryType.String:
+ sb.Append(entry.Message);
+ break;
+
+ case TextEntryType.Macro:
+ case TextEntryType.Fixed:
+ sb.AppendMacroString(entry.Message);
+ break;
+ }
+ }
+
+ this.input = sb.ToReadOnlySeString();
+
+ if (resetLocalParameters)
+ this.localParameters = null;
+ }
+
+ private void DrawPreview(ReadOnlySeString str)
+ {
+ using var nodeColor = ImRaii.PushColor(ImGuiCol.Text, 0xFF00FF00);
+ using var node = ImRaii.TreeNode("Preview", ImGuiTreeNodeFlags.DefaultOpen);
+ nodeColor.Pop();
+ if (!node) return;
+
+ ImGui.Dummy(new Vector2(0, ImGui.GetTextLineHeight()));
+ ImGui.SameLine(0, 0);
+ ImGuiHelpers.SeStringWrapped(str);
+ }
+
+ private void DrawParameters()
+ {
+ using var nodeColor = ImRaii.PushColor(ImGuiCol.Text, 0xFF00FF00);
+ using var node = ImRaii.TreeNode("Parameters", ImGuiTreeNodeFlags.DefaultOpen);
+ nodeColor.Pop();
+ if (!node) return;
+
+ for (var i = 0; i < this.localParameters!.Length; i++)
+ {
+ if (this.localParameters[i].IsString)
+ {
+ var str = this.localParameters[i].StringValue.ExtractText();
+ if (ImGui.InputText($"lstr({i + 1})", ref str, 255))
+ {
+ this.localParameters[i] = new(str);
+ }
+ }
+ else
+ {
+ var num = (int)this.localParameters[i].UIntValue;
+ if (ImGui.InputInt($"lnum({i + 1})", ref num))
+ {
+ this.localParameters[i] = new((uint)num);
+ }
+ }
+ }
+ }
+
+ private void DrawPayloads(ReadOnlySeString evaluated)
+ {
+ using (var nodeColor = ImRaii.PushColor(ImGuiCol.Text, 0xFF00FF00))
+ using (var node = ImRaii.TreeNode("Payloads", ImGuiTreeNodeFlags.DefaultOpen | ImGuiTreeNodeFlags.SpanAvailWidth))
+ {
+ nodeColor.Pop();
+ if (node) this.DrawSeString("payloads", this.input.AsSpan(), treeNodeFlags: ImGuiTreeNodeFlags.DefaultOpen | ImGuiTreeNodeFlags.SpanAvailWidth);
+ }
+
+ if (this.input.Equals(evaluated))
+ return;
+
+ using (var nodeColor = ImRaii.PushColor(ImGuiCol.Text, 0xFF00FF00))
+ using (var node = ImRaii.TreeNode("Payloads (Evaluated)", ImGuiTreeNodeFlags.DefaultOpen | ImGuiTreeNodeFlags.SpanAvailWidth))
+ {
+ nodeColor.Pop();
+ if (node) this.DrawSeString("payloads-evaluated", evaluated.AsSpan(), treeNodeFlags: ImGuiTreeNodeFlags.DefaultOpen | ImGuiTreeNodeFlags.SpanAvailWidth);
+ }
+ }
+
+ private void DrawSeString(string id, ReadOnlySeStringSpan rosss, bool asTreeNode = false, bool renderSeString = false, int depth = 0, ImGuiTreeNodeFlags treeNodeFlags = ImGuiTreeNodeFlags.None)
+ {
+ using var seStringId = ImRaii.PushId(id);
+
+ if (rosss.PayloadCount == 0)
+ {
+ ImGui.Dummy(Vector2.Zero);
+ return;
+ }
+
+ using var node = asTreeNode ? this.SeStringTreeNode(id, rosss) : null;
+ if (asTreeNode && !node!) return;
+
+ if (!asTreeNode && renderSeString)
+ {
+ ImGuiHelpers.SeStringWrapped(rosss, new()
+ {
+ ForceEdgeColor = true,
+ });
+ }
+
+ var payloadIdx = -1;
+ foreach (var payload in rosss)
+ {
+ payloadIdx++;
+ using var payloadId = ImRaii.PushId(payloadIdx);
+
+ var preview = payload.Type.ToString();
+ if (payload.Type == ReadOnlySePayloadType.Macro)
+ preview += $": {payload.MacroCode}";
+
+ using var nodeColor = ImRaii.PushColor(ImGuiCol.Text, 0xFF00FFFF);
+ using var payloadNode = ImRaii.TreeNode($"[{payloadIdx}] {preview}", ImGuiTreeNodeFlags.DefaultOpen | ImGuiTreeNodeFlags.SpanAvailWidth);
+ nodeColor.Pop();
+ if (!payloadNode) continue;
+
+ using var table = ImRaii.Table($"##Payload{payloadIdx}Table", 2);
+ if (!table) return;
+
+ ImGui.TableSetupColumn("Label", ImGuiTableColumnFlags.WidthFixed, 120);
+ ImGui.TableSetupColumn("Tree", ImGuiTableColumnFlags.WidthStretch);
+
+ ImGui.TableNextRow();
+ ImGui.TableNextColumn();
+ ImGui.TextUnformatted(payload.Type == ReadOnlySePayloadType.Text ? "Text" : "ToString()");
+ ImGui.TableNextColumn();
+ var text = payload.ToString();
+ WidgetUtil.DrawCopyableText($"\"{text}\"", text);
+
+ if (payload.Type != ReadOnlySePayloadType.Macro)
+ continue;
+
+ if (payload.ExpressionCount > 0)
+ {
+ var exprIdx = 0;
+ uint? subType = null;
+ uint? fixedType = null;
+
+ if (payload.MacroCode == MacroCode.Link && payload.TryGetExpression(out var linkExpr1) && linkExpr1.TryGetUInt(out var linkExpr1Val))
+ {
+ subType = linkExpr1Val;
+ }
+ else if (payload.MacroCode == MacroCode.Fixed && payload.TryGetExpression(out var fixedTypeExpr, out var linkExpr2) && fixedTypeExpr.TryGetUInt(out var fixedTypeVal) && linkExpr2.TryGetUInt(out var linkExpr2Val))
+ {
+ subType = linkExpr2Val;
+ fixedType = fixedTypeVal;
+ }
+
+ foreach (var expr in payload)
+ {
+ using var exprId = ImRaii.PushId(exprIdx);
+
+ this.DrawExpression(payload.MacroCode, subType, fixedType, exprIdx++, expr);
+ }
+ }
+ }
+ }
+
+ private unsafe void DrawExpression(MacroCode macroCode, uint? subType, uint? fixedType, int exprIdx, ReadOnlySeExpressionSpan expr)
+ {
+ ImGui.TableNextRow();
+
+ ImGui.TableNextColumn();
+ var expressionName = this.GetExpressionName(macroCode, subType, exprIdx, expr);
+ ImGui.TextUnformatted($"[{exprIdx}] " + (string.IsNullOrEmpty(expressionName) ? $"Expr {exprIdx}" : expressionName));
+
+ ImGui.TableNextColumn();
+
+ if (expr.Body.IsEmpty)
+ {
+ ImGui.TextUnformatted("(?)");
+ return;
+ }
+
+ if (expr.TryGetUInt(out var u32))
+ {
+ if (macroCode is MacroCode.Icon or MacroCode.Icon2 && exprIdx == 0)
+ {
+ var iconId = u32;
+
+ if (macroCode == MacroCode.Icon2)
+ {
+ var iconMapping = RaptureAtkModule.Instance()->AtkFontManager.Icon2RemapTable;
+ for (var i = 0; i < 30; i++)
+ {
+ if (iconMapping[i].IconId == iconId)
+ {
+ iconId = iconMapping[i].RemappedIconId;
+ break;
+ }
+ }
+ }
+
+ var builder = LSeStringBuilder.SharedPool.Get();
+ builder.AppendIcon(iconId);
+ ImGuiHelpers.SeStringWrapped(builder.ToArray());
+ LSeStringBuilder.SharedPool.Return(builder);
+
+ ImGui.SameLine();
+ }
+
+ WidgetUtil.DrawCopyableText(u32.ToString());
+ ImGui.SameLine();
+ WidgetUtil.DrawCopyableText($"0x{u32:X}");
+
+ if (macroCode == MacroCode.Link && exprIdx == 0)
+ {
+ var name = subType != null && (LinkMacroPayloadType)subType == DalamudLinkType
+ ? "Dalamud"
+ : Enum.GetName((LinkMacroPayloadType)u32);
+
+ if (!string.IsNullOrEmpty(name))
+ {
+ ImGui.SameLine();
+ ImGui.TextUnformatted(name);
+ }
+ }
+
+ if (macroCode is MacroCode.JaNoun or MacroCode.EnNoun or MacroCode.DeNoun or MacroCode.FrNoun && exprIdx == 1)
+ {
+ var language = macroCode switch
+ {
+ MacroCode.JaNoun => ClientLanguage.Japanese,
+ MacroCode.DeNoun => ClientLanguage.German,
+ MacroCode.FrNoun => ClientLanguage.French,
+ _ => ClientLanguage.English,
+ };
+ var articleTypeEnumType = language switch
+ {
+ ClientLanguage.Japanese => typeof(JapaneseArticleType),
+ ClientLanguage.German => typeof(GermanArticleType),
+ ClientLanguage.French => typeof(FrenchArticleType),
+ _ => typeof(EnglishArticleType),
+ };
+ ImGui.SameLine();
+ ImGui.TextUnformatted(Enum.GetName(articleTypeEnumType, u32));
+ }
+
+ if (macroCode is MacroCode.DeNoun && exprIdx == 4 && u32 is >= 0 and <= 3)
+ {
+ ImGui.SameLine();
+ ImGui.TextUnformatted(NounProcessorWidget.GermanCases[u32]);
+ }
+
+ if (macroCode is MacroCode.Fixed && subType != null && fixedType != null && fixedType is 100 or 200 && subType == 5 && exprIdx == 2)
+ {
+ ImGui.SameLine();
+ if (ImGui.SmallButton("Play"))
+ {
+ UIGlobals.PlayChatSoundEffect(u32 + 1);
+ }
+ }
+
+ if (macroCode is MacroCode.Link && subType != null && exprIdx == 1)
+ {
+ var dataManager = Service.Get();
+
+ switch ((LinkMacroPayloadType)subType)
+ {
+ case LinkMacroPayloadType.Item when dataManager.GetExcelSheet
- (this.language).TryGetRow(u32, out var itemRow):
+ ImGui.SameLine();
+ ImGui.TextUnformatted(itemRow.Name.ExtractText());
+ break;
+
+ case LinkMacroPayloadType.Quest when dataManager.GetExcelSheet(this.language).TryGetRow(u32, out var questRow):
+ ImGui.SameLine();
+ ImGui.TextUnformatted(questRow.Name.ExtractText());
+ break;
+
+ case LinkMacroPayloadType.Achievement when dataManager.GetExcelSheet(this.language).TryGetRow(u32, out var achievementRow):
+ ImGui.SameLine();
+ ImGui.TextUnformatted(achievementRow.Name.ExtractText());
+ break;
+
+ case LinkMacroPayloadType.HowTo when dataManager.GetExcelSheet(this.language).TryGetRow(u32, out var howToRow):
+ ImGui.SameLine();
+ ImGui.TextUnformatted(howToRow.Name.ExtractText());
+ break;
+
+ case LinkMacroPayloadType.Status when dataManager.GetExcelSheet(this.language).TryGetRow(u32, out var statusRow):
+ ImGui.SameLine();
+ ImGui.TextUnformatted(statusRow.Name.ExtractText());
+ break;
+
+ case LinkMacroPayloadType.AkatsukiNote when
+ dataManager.GetSubrowExcelSheet(this.language).TryGetRow(u32, out var akatsukiNoteRow) &&
+ dataManager.GetExcelSheet(this.language).TryGetRow((uint)akatsukiNoteRow[0].Unknown2, out var akatsukiNoteStringRow):
+ ImGui.SameLine();
+ ImGui.TextUnformatted(akatsukiNoteStringRow.Unknown0.ExtractText());
+ break;
+ }
+ }
+
+ return;
+ }
+
+ if (expr.TryGetString(out var s))
+ {
+ this.DrawSeString("Preview", s, treeNodeFlags: ImGuiTreeNodeFlags.DefaultOpen);
+ return;
+ }
+
+ if (expr.TryGetPlaceholderExpression(out var exprType))
+ {
+ if (((ExpressionType)exprType).GetNativeName() is { } nativeName)
+ {
+ ImGui.TextUnformatted(nativeName);
+ return;
+ }
+
+ ImGui.TextUnformatted($"?x{exprType:X02}");
+ return;
+ }
+
+ if (expr.TryGetParameterExpression(out exprType, out var e1))
+ {
+ if (((ExpressionType)exprType).GetNativeName() is { } nativeName)
+ {
+ ImGui.TextUnformatted($"{nativeName}({e1.ToString()})");
+ return;
+ }
+
+ throw new InvalidOperationException("All native names must be defined for unary expressions.");
+ }
+
+ if (expr.TryGetBinaryExpression(out exprType, out e1, out var e2))
+ {
+ if (((ExpressionType)exprType).GetNativeName() is { } nativeName)
+ {
+ ImGui.TextUnformatted($"{e1.ToString()} {nativeName} {e2.ToString()}");
+ return;
+ }
+
+ throw new InvalidOperationException("All native names must be defined for binary expressions.");
+ }
+
+ var sb = new StringBuilder();
+ sb.EnsureCapacity(1 + 3 * expr.Body.Length);
+ sb.Append($"({expr.Body[0]:X02}");
+ for (var i = 1; i < expr.Body.Length; i++)
+ sb.Append($" {expr.Body[i]:X02}");
+ sb.Append(')');
+ ImGui.TextUnformatted(sb.ToString());
+ }
+
+ private string GetExpressionName(MacroCode macroCode, uint? subType, int idx, ReadOnlySeExpressionSpan expr)
+ {
+ if (this.expressionNames.TryGetValue(macroCode, out var names) && idx < names.Length)
+ return names[idx];
+
+ if (macroCode == MacroCode.Switch)
+ return $"Case {idx - 1}";
+
+ if (macroCode == MacroCode.Link && subType != null && this.linkExpressionNames.TryGetValue((LinkMacroPayloadType)subType, out var linkNames) && idx - 1 < linkNames.Length)
+ return linkNames[idx - 1];
+
+ if (macroCode == MacroCode.Fixed && subType != null && this.fixedExpressionNames.TryGetValue((uint)subType, out var fixedNames) && idx < fixedNames.Length)
+ return fixedNames[idx];
+
+ if (macroCode == MacroCode.Link && idx == 4)
+ return "Copy String";
+
+ return string.Empty;
+ }
+
+ private SeStringParameter[] GetLocalParameters(ReadOnlySeStringSpan rosss, Dictionary? parameters)
+ {
+ parameters ??= [];
+
+ void ProcessString(ReadOnlySeStringSpan rosss)
+ {
+ foreach (var payload in rosss)
+ {
+ foreach (var expression in payload)
+ {
+ ProcessExpression(expression);
+ }
+ }
+ }
+
+ void ProcessExpression(ReadOnlySeExpressionSpan expression)
+ {
+ if (expression.TryGetString(out var exprString))
+ {
+ ProcessString(exprString);
+ return;
+ }
+
+ if (expression.TryGetBinaryExpression(out var expressionType, out var operand1, out var operand2))
+ {
+ ProcessExpression(operand1);
+ ProcessExpression(operand2);
+ return;
+ }
+
+ if (expression.TryGetParameterExpression(out expressionType, out var operand))
+ {
+ if (!operand.TryGetUInt(out var index))
+ return;
+
+ if (parameters.ContainsKey(index))
+ return;
+
+ if (expressionType == (int)ExpressionType.LocalNumber)
+ {
+ parameters[index] = new SeStringParameter(0);
+ return;
+ }
+ else if (expressionType == (int)ExpressionType.LocalString)
+ {
+ parameters[index] = new SeStringParameter(string.Empty);
+ return;
+ }
+ }
+ }
+
+ ProcessString(rosss);
+
+ if (parameters.Count > 0)
+ {
+ var last = parameters.OrderBy(x => x.Key).Last();
+
+ if (parameters.Count != last.Key)
+ {
+ // fill missing local parameter slots, so we can go off the array index in SeStringContext
+
+ for (var i = 1u; i <= last.Key; i++)
+ {
+ if (!parameters.ContainsKey(i))
+ parameters[i] = new SeStringParameter(0);
+ }
+ }
+ }
+
+ return parameters.OrderBy(x => x.Key).Select(x => x.Value).ToArray();
+ }
+
+ private ImRaii.IEndObject SeStringTreeNode(string id, ReadOnlySeStringSpan previewText, uint color = 0xFF00FFFF, ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags.None)
+ {
+ using var titleColor = ImRaii.PushColor(ImGuiCol.Text, color);
+ var node = ImRaii.TreeNode("##" + id, flags);
+ ImGui.SameLine();
+ ImGuiHelpers.SeStringWrapped(previewText, new()
+ {
+ ForceEdgeColor = true,
+ WrapWidth = 9999,
+ });
+ return node;
+ }
+
+ private bool IconButton(string key, FontAwesomeIcon icon, string tooltip, Vector2 size = default, bool disabled = false, bool active = false)
+ {
+ using var iconFont = ImRaii.PushFont(UiBuilder.IconFont);
+ if (!key.StartsWith("##")) key = "##" + key;
+
+ var disposables = new List();
+
+ if (disabled)
+ {
+ disposables.Add(ImRaii.PushColor(ImGuiCol.Text, ImGui.GetStyle().Colors[(int)ImGuiCol.TextDisabled]));
+ disposables.Add(ImRaii.PushColor(ImGuiCol.ButtonActive, ImGui.GetStyle().Colors[(int)ImGuiCol.Button]));
+ disposables.Add(ImRaii.PushColor(ImGuiCol.ButtonHovered, ImGui.GetStyle().Colors[(int)ImGuiCol.Button]));
+ }
+ else if (active)
+ {
+ disposables.Add(ImRaii.PushColor(ImGuiCol.Button, ImGui.GetStyle().Colors[(int)ImGuiCol.ButtonActive]));
+ }
+
+ var pressed = ImGui.Button(icon.ToIconString() + key, size);
+
+ foreach (var disposable in disposables)
+ disposable.Dispose();
+
+ iconFont?.Dispose();
+
+ if (ImGui.IsItemHovered())
+ {
+ ImGui.BeginTooltip();
+ ImGui.TextUnformatted(tooltip);
+ ImGui.EndTooltip();
+ }
+
+ return pressed;
+ }
+
+ private Vector2 GetIconButtonSize(FontAwesomeIcon icon)
+ {
+ using var iconFont = ImRaii.PushFont(UiBuilder.IconFont);
+ return ImGui.CalcTextSize(icon.ToIconString()) + ImGui.GetStyle().FramePadding * 2;
+ }
+
+ private class TextEntry(TextEntryType type, string text)
+ {
+ public string Message { get; set; } = text;
+
+ public TextEntryType Type { get; set; } = type;
+ }
+}
diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/GamepadStateAgingStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/GamepadStateAgingStep.cs
index ccee570c7..323d82bbc 100644
--- a/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/GamepadStateAgingStep.cs
+++ b/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/GamepadStateAgingStep.cs
@@ -1,6 +1,11 @@
-using Dalamud.Game.ClientState.GamePad;
+using System.Linq;
-using ImGuiNET;
+using Dalamud.Game.ClientState.GamePad;
+using Dalamud.Interface.Utility;
+
+using Lumina.Text.Payloads;
+
+using LSeStringBuilder = Lumina.Text.SeStringBuilder;
namespace Dalamud.Interface.Internal.Windows.SelfTest.AgingSteps;
@@ -17,11 +22,34 @@ internal class GamepadStateAgingStep : IAgingStep
{
var gamepadState = Service.Get();
- ImGui.Text("Hold down North, East, L1");
+ var buttons = new (GamepadButtons Button, uint IconId)[]
+ {
+ (GamepadButtons.North, 11),
+ (GamepadButtons.East, 8),
+ (GamepadButtons.L1, 12),
+ };
- if (gamepadState.Raw(GamepadButtons.North) == 1
- && gamepadState.Raw(GamepadButtons.East) == 1
- && gamepadState.Raw(GamepadButtons.L1) == 1)
+ var builder = LSeStringBuilder.SharedPool.Get();
+
+ builder.Append("Hold down ");
+
+ for (var i = 0; i < buttons.Length; i++)
+ {
+ var (button, iconId) = buttons[i];
+
+ builder.BeginMacro(MacroCode.Icon).AppendUIntExpression(iconId).EndMacro();
+ builder.PushColorRgba(gamepadState.Raw(button) == 1 ? 0x0000FF00u : 0x000000FF);
+ builder.Append(button.ToString());
+ builder.PopColor();
+
+ builder.Append(i < buttons.Length - 1 ? ", " : ".");
+ }
+
+ ImGuiHelpers.SeStringWrapped(builder.ToReadOnlySeString());
+
+ LSeStringBuilder.SharedPool.Return(builder);
+
+ if (buttons.All(tuple => gamepadState.Raw(tuple.Button) == 1))
{
return SelfTestStepResult.Pass;
}
diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/NounProcessorAgingStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/NounProcessorAgingStep.cs
new file mode 100644
index 000000000..4073616b2
--- /dev/null
+++ b/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/NounProcessorAgingStep.cs
@@ -0,0 +1,259 @@
+using Dalamud.Game;
+using Dalamud.Game.Text.Noun;
+using Dalamud.Game.Text.Noun.Enums;
+
+using ImGuiNET;
+
+using LSheets = Lumina.Excel.Sheets;
+
+namespace Dalamud.Interface.Internal.Windows.SelfTest.AgingSteps;
+
+///
+/// Test setup for NounProcessor.
+///
+internal class NounProcessorAgingStep : IAgingStep
+{
+ private NounTestEntry[] tests =
+ [
+ new(nameof(LSheets.BNpcName), 1330, ClientLanguage.Japanese, 1, (int)JapaneseArticleType.NearListener, 1, "その蜂蜜酒の運び人"),
+ new(nameof(LSheets.BNpcName), 1330, ClientLanguage.Japanese, 1, (int)JapaneseArticleType.Distant, 1, "蜂蜜酒の運び人"),
+ new(nameof(LSheets.BNpcName), 1330, ClientLanguage.Japanese, 2, (int)JapaneseArticleType.NearListener, 1, "それらの蜂蜜酒の運び人"),
+ new(nameof(LSheets.BNpcName), 1330, ClientLanguage.Japanese, 2, (int)JapaneseArticleType.Distant, 1, "あれらの蜂蜜酒の運び人"),
+
+ new(nameof(LSheets.BNpcName), 1330, ClientLanguage.English, 1, (int)EnglishArticleType.Indefinite, 1, "a mead-porting Midlander"),
+ new(nameof(LSheets.BNpcName), 1330, ClientLanguage.English, 1, (int)EnglishArticleType.Definite, 1, "the mead-porting Midlander"),
+ new(nameof(LSheets.BNpcName), 1330, ClientLanguage.English, 2, (int)EnglishArticleType.Indefinite, 1, "mead-porting Midlanders"),
+ new(nameof(LSheets.BNpcName), 1330, ClientLanguage.English, 2, (int)EnglishArticleType.Definite, 1, "mead-porting Midlanders"),
+
+ new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 1, (int)GermanArticleType.Indefinite, (int)GermanCases.Nominative, "ein Met schleppender Wiesländer"),
+ new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 1, (int)GermanArticleType.Indefinite, (int)GermanCases.Genitive, "eines Met schleppenden Wiesländers"),
+ new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 1, (int)GermanArticleType.Indefinite, (int)GermanCases.Dative, "einem Met schleppenden Wiesländer"),
+ new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 1, (int)GermanArticleType.Indefinite, (int)GermanCases.Accusative, "einen Met schleppenden Wiesländer"),
+ new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 1, (int)GermanArticleType.Definite, (int)GermanCases.Nominative, "der Met schleppender Wiesländer"),
+ new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 1, (int)GermanArticleType.Definite, (int)GermanCases.Genitive, "des Met schleppenden Wiesländers"),
+ new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 1, (int)GermanArticleType.Definite, (int)GermanCases.Dative, "dem Met schleppenden Wiesländer"),
+ new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 1, (int)GermanArticleType.Definite, (int)GermanCases.Accusative, "den Met schleppenden Wiesländer"),
+ new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 1, (int)GermanArticleType.Possessive, (int)GermanCases.Nominative, "dein Met schleppende Wiesländer"),
+ new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 1, (int)GermanArticleType.Possessive, (int)GermanCases.Genitive, "deines Met schleppenden Wiesländers"),
+ new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 1, (int)GermanArticleType.Possessive, (int)GermanCases.Dative, "deinem Met schleppenden Wiesländer"),
+ new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 1, (int)GermanArticleType.Possessive, (int)GermanCases.Accusative, "deinen Met schleppenden Wiesländer"),
+ new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 1, (int)GermanArticleType.Negative, (int)GermanCases.Nominative, "kein Met schleppender Wiesländer"),
+ new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 1, (int)GermanArticleType.Negative, (int)GermanCases.Genitive, "keines Met schleppenden Wiesländers"),
+ new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 1, (int)GermanArticleType.Negative, (int)GermanCases.Dative, "keinem Met schleppenden Wiesländer"),
+ new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 1, (int)GermanArticleType.Negative, (int)GermanCases.Accusative, "keinen Met schleppenden Wiesländer"),
+ new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 1, (int)GermanArticleType.ZeroArticle, (int)GermanCases.Nominative, "Met schleppender Wiesländer"),
+ new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 1, (int)GermanArticleType.ZeroArticle, (int)GermanCases.Genitive, "Met schleppenden Wiesländers"),
+ new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 1, (int)GermanArticleType.ZeroArticle, (int)GermanCases.Dative, "Met schleppendem Wiesländer"),
+ new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 1, (int)GermanArticleType.ZeroArticle, (int)GermanCases.Accusative, "Met schleppenden Wiesländer"),
+ new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 1, (int)GermanArticleType.Demonstrative, (int)GermanCases.Nominative, "dieser Met schleppende Wiesländer"),
+ new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 1, (int)GermanArticleType.Demonstrative, (int)GermanCases.Genitive, "dieses Met schleppenden Wiesländers"),
+ new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 1, (int)GermanArticleType.Demonstrative, (int)GermanCases.Dative, "diesem Met schleppenden Wiesländer"),
+ new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 1, (int)GermanArticleType.Demonstrative, (int)GermanCases.Accusative, "diesen Met schleppenden Wiesländer"),
+ new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 2, (int)GermanArticleType.Indefinite, (int)GermanCases.Nominative, "2 Met schleppenden Wiesländer"),
+ new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 2, (int)GermanArticleType.Indefinite, (int)GermanCases.Genitive, "2 Met schleppenden Wiesländer"),
+ new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 2, (int)GermanArticleType.Indefinite, (int)GermanCases.Dative, "2 Met schleppenden Wiesländern"),
+ new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 2, (int)GermanArticleType.Indefinite, (int)GermanCases.Accusative, "2 Met schleppenden Wiesländer"),
+ new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 2, (int)GermanArticleType.Definite, (int)GermanCases.Nominative, "die Met schleppende Wiesländer"),
+ new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 2, (int)GermanArticleType.Definite, (int)GermanCases.Genitive, "der Met schleppender Wiesländer"),
+ new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 2, (int)GermanArticleType.Definite, (int)GermanCases.Dative, "den Met schleppenden Wiesländern"),
+ new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 2, (int)GermanArticleType.Definite, (int)GermanCases.Accusative, "die Met schleppende Wiesländer"),
+ new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 2, (int)GermanArticleType.Possessive, (int)GermanCases.Nominative, "deine Met schleppenden Wiesländer"),
+ new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 2, (int)GermanArticleType.Possessive, (int)GermanCases.Genitive, "deiner Met schleppenden Wiesländer"),
+ new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 2, (int)GermanArticleType.Possessive, (int)GermanCases.Dative, "deinen Met schleppenden Wiesländern"),
+ new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 2, (int)GermanArticleType.Possessive, (int)GermanCases.Accusative, "deine Met schleppenden Wiesländer"),
+ new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 2, (int)GermanArticleType.Negative, (int)GermanCases.Nominative, "keine Met schleppenden Wiesländer"),
+ new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 2, (int)GermanArticleType.Negative, (int)GermanCases.Genitive, "keiner Met schleppenden Wiesländer"),
+ new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 2, (int)GermanArticleType.Negative, (int)GermanCases.Dative, "keinen Met schleppenden Wiesländern"),
+ new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 2, (int)GermanArticleType.Negative, (int)GermanCases.Accusative, "keine Met schleppenden Wiesländer"),
+ new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 2, (int)GermanArticleType.ZeroArticle, (int)GermanCases.Nominative, "Met schleppende Wiesländer"),
+ new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 2, (int)GermanArticleType.ZeroArticle, (int)GermanCases.Genitive, "Met schleppender Wiesländer"),
+ new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 2, (int)GermanArticleType.ZeroArticle, (int)GermanCases.Dative, "Met schleppenden Wiesländern"),
+ new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 2, (int)GermanArticleType.ZeroArticle, (int)GermanCases.Accusative, "Met schleppende Wiesländer"),
+ new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 2, (int)GermanArticleType.Demonstrative, (int)GermanCases.Nominative, "diese Met schleppenden Wiesländer"),
+ new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 2, (int)GermanArticleType.Demonstrative, (int)GermanCases.Genitive, "dieser Met schleppenden Wiesländer"),
+ new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 2, (int)GermanArticleType.Demonstrative, (int)GermanCases.Dative, "diesen Met schleppenden Wiesländern"),
+ new(nameof(LSheets.BNpcName), 1330, ClientLanguage.German, 2, (int)GermanArticleType.Demonstrative, (int)GermanCases.Accusative, "diese Met schleppenden Wiesländer"),
+
+ new(nameof(LSheets.BNpcName), 1330, ClientLanguage.French, 1, (int)FrenchArticleType.Indefinite, 1, "un livreur d'hydromel"),
+ new(nameof(LSheets.BNpcName), 1330, ClientLanguage.French, 1, (int)FrenchArticleType.Definite, 1, "le livreur d'hydromel"),
+ new(nameof(LSheets.BNpcName), 1330, ClientLanguage.French, 1, (int)FrenchArticleType.PossessiveFirstPerson, 1, "mon livreur d'hydromel"),
+ new(nameof(LSheets.BNpcName), 1330, ClientLanguage.French, 1, (int)FrenchArticleType.PossessiveSecondPerson, 1, "ton livreur d'hydromel"),
+ new(nameof(LSheets.BNpcName), 1330, ClientLanguage.French, 1, (int)FrenchArticleType.PossessiveThirdPerson, 1, "son livreur d'hydromel"),
+ new(nameof(LSheets.BNpcName), 1330, ClientLanguage.French, 2, (int)FrenchArticleType.Indefinite, 1, "des livreurs d'hydromel"),
+ new(nameof(LSheets.BNpcName), 1330, ClientLanguage.French, 2, (int)FrenchArticleType.Definite, 1, "les livreurs d'hydromel"),
+ new(nameof(LSheets.BNpcName), 1330, ClientLanguage.French, 2, (int)FrenchArticleType.PossessiveFirstPerson, 1, "mes livreurs d'hydromel"),
+ new(nameof(LSheets.BNpcName), 1330, ClientLanguage.French, 2, (int)FrenchArticleType.PossessiveSecondPerson, 1, "tes livreurs d'hydromel"),
+ new(nameof(LSheets.BNpcName), 1330, ClientLanguage.French, 2, (int)FrenchArticleType.PossessiveThirdPerson, 1, "ses livreurs d'hydromel"),
+
+ new(nameof(LSheets.ENpcResident), 1031947, ClientLanguage.Japanese, 1, (int)JapaneseArticleType.NearListener, 1, "その酔いどれのネル"),
+ new(nameof(LSheets.ENpcResident), 1031947, ClientLanguage.Japanese, 1, (int)JapaneseArticleType.Distant, 1, "酔いどれのネル"),
+
+ new(nameof(LSheets.ENpcResident), 1031947, ClientLanguage.English, 1, (int)EnglishArticleType.Indefinite, 1, "Nell Half-full"),
+ new(nameof(LSheets.ENpcResident), 1031947, ClientLanguage.English, 1, (int)EnglishArticleType.Definite, 1, "Nell Half-full"),
+
+ new(nameof(LSheets.ENpcResident), 1031947, ClientLanguage.German, 1, (int)GermanArticleType.Indefinite, (int)GermanCases.Nominative, "Nell die Beschwipste"),
+ new(nameof(LSheets.ENpcResident), 1031947, ClientLanguage.German, 1, (int)GermanArticleType.Indefinite, (int)GermanCases.Genitive, "Nell der Beschwipsten"),
+ new(nameof(LSheets.ENpcResident), 1031947, ClientLanguage.German, 1, (int)GermanArticleType.Indefinite, (int)GermanCases.Dative, "Nell der Beschwipsten"),
+ new(nameof(LSheets.ENpcResident), 1031947, ClientLanguage.German, 1, (int)GermanArticleType.Indefinite, (int)GermanCases.Accusative, "Nell die Beschwipste"),
+ new(nameof(LSheets.ENpcResident), 1031947, ClientLanguage.German, 1, (int)GermanArticleType.Definite, (int)GermanCases.Nominative, "Nell die Beschwipste"),
+ new(nameof(LSheets.ENpcResident), 1031947, ClientLanguage.German, 1, (int)GermanArticleType.Definite, (int)GermanCases.Genitive, "Nell der Beschwipsten"),
+ new(nameof(LSheets.ENpcResident), 1031947, ClientLanguage.German, 1, (int)GermanArticleType.Definite, (int)GermanCases.Dative, "Nell der Beschwipsten"),
+ new(nameof(LSheets.ENpcResident), 1031947, ClientLanguage.German, 1, (int)GermanArticleType.Definite, (int)GermanCases.Accusative, "Nell die Beschwipste"),
+ new(nameof(LSheets.ENpcResident), 1031947, ClientLanguage.German, 1, (int)GermanArticleType.Possessive, (int)GermanCases.Nominative, "Nell die Beschwipste"),
+ new(nameof(LSheets.ENpcResident), 1031947, ClientLanguage.German, 1, (int)GermanArticleType.Possessive, (int)GermanCases.Genitive, "Nell der Beschwipsten"),
+ new(nameof(LSheets.ENpcResident), 1031947, ClientLanguage.German, 1, (int)GermanArticleType.Possessive, (int)GermanCases.Dative, "Nell der Beschwipsten"),
+ new(nameof(LSheets.ENpcResident), 1031947, ClientLanguage.German, 1, (int)GermanArticleType.Possessive, (int)GermanCases.Accusative, "Nell die Beschwipste"),
+ new(nameof(LSheets.ENpcResident), 1031947, ClientLanguage.German, 1, (int)GermanArticleType.Negative, (int)GermanCases.Nominative, "Nell die Beschwipste"),
+ new(nameof(LSheets.ENpcResident), 1031947, ClientLanguage.German, 1, (int)GermanArticleType.Negative, (int)GermanCases.Genitive, "Nell der Beschwipsten"),
+ new(nameof(LSheets.ENpcResident), 1031947, ClientLanguage.German, 1, (int)GermanArticleType.Negative, (int)GermanCases.Dative, "Nell der Beschwipsten"),
+ new(nameof(LSheets.ENpcResident), 1031947, ClientLanguage.German, 1, (int)GermanArticleType.Negative, (int)GermanCases.Accusative, "Nell die Beschwipste"),
+ new(nameof(LSheets.ENpcResident), 1031947, ClientLanguage.German, 1, (int)GermanArticleType.ZeroArticle, (int)GermanCases.Nominative, "Nell die Beschwipste"),
+ new(nameof(LSheets.ENpcResident), 1031947, ClientLanguage.German, 1, (int)GermanArticleType.ZeroArticle, (int)GermanCases.Genitive, "Nell der Beschwipsten"),
+ new(nameof(LSheets.ENpcResident), 1031947, ClientLanguage.German, 1, (int)GermanArticleType.ZeroArticle, (int)GermanCases.Dative, "Nell der Beschwipsten"),
+ new(nameof(LSheets.ENpcResident), 1031947, ClientLanguage.German, 1, (int)GermanArticleType.ZeroArticle, (int)GermanCases.Accusative, "Nell die Beschwipste"),
+ new(nameof(LSheets.ENpcResident), 1031947, ClientLanguage.German, 1, (int)GermanArticleType.Demonstrative, (int)GermanCases.Nominative, "Nell die Beschwipste"),
+ new(nameof(LSheets.ENpcResident), 1031947, ClientLanguage.German, 1, (int)GermanArticleType.Demonstrative, (int)GermanCases.Genitive, "Nell der Beschwipsten"),
+ new(nameof(LSheets.ENpcResident), 1031947, ClientLanguage.German, 1, (int)GermanArticleType.Demonstrative, (int)GermanCases.Dative, "Nell der Beschwipsten"),
+ new(nameof(LSheets.ENpcResident), 1031947, ClientLanguage.German, 1, (int)GermanArticleType.Demonstrative, (int)GermanCases.Accusative, "Nell die Beschwipste"),
+
+ new(nameof(LSheets.ENpcResident), 1031947, ClientLanguage.French, 1, (int)FrenchArticleType.Indefinite, 1, "Nell la Boit-sans-soif"),
+ new(nameof(LSheets.ENpcResident), 1031947, ClientLanguage.French, 1, (int)FrenchArticleType.Definite, 1, "Nell la Boit-sans-soif"),
+ new(nameof(LSheets.ENpcResident), 1031947, ClientLanguage.French, 1, (int)FrenchArticleType.PossessiveFirstPerson, 1, "ma Nell la Boit-sans-soif"),
+ new(nameof(LSheets.ENpcResident), 1031947, ClientLanguage.French, 1, (int)FrenchArticleType.PossessiveSecondPerson, 1, "ta Nell la Boit-sans-soif"),
+ new(nameof(LSheets.ENpcResident), 1031947, ClientLanguage.French, 1, (int)FrenchArticleType.PossessiveThirdPerson, 1, "sa Nell la Boit-sans-soif"),
+
+ new(nameof(LSheets.Item), 44348, ClientLanguage.Japanese, 1, (int)JapaneseArticleType.NearListener, 1, "その希少トームストーン:幻想"),
+ new(nameof(LSheets.Item), 44348, ClientLanguage.Japanese, 1, (int)JapaneseArticleType.Distant, 1, "希少トームストーン:幻想"),
+ new(nameof(LSheets.Item), 44348, ClientLanguage.Japanese, 2, (int)JapaneseArticleType.NearListener, 1, "それらの希少トームストーン:幻想"),
+ new(nameof(LSheets.Item), 44348, ClientLanguage.Japanese, 2, (int)JapaneseArticleType.Distant, 1, "あれらの希少トームストーン:幻想"),
+
+ new(nameof(LSheets.Item), 44348, ClientLanguage.English, 1, (int)EnglishArticleType.Indefinite, 1, "an irregular tomestone of phantasmagoria"),
+ new(nameof(LSheets.Item), 44348, ClientLanguage.English, 1, (int)EnglishArticleType.Definite, 1, "the irregular tomestone of phantasmagoria"),
+ new(nameof(LSheets.Item), 44348, ClientLanguage.English, 2, (int)EnglishArticleType.Indefinite, 1, "irregular tomestones of phantasmagoria"),
+ new(nameof(LSheets.Item), 44348, ClientLanguage.English, 2, (int)EnglishArticleType.Definite, 1, "irregular tomestones of phantasmagoria"),
+
+ new(nameof(LSheets.Item), 44348, ClientLanguage.German, 1, (int)GermanArticleType.Indefinite, (int)GermanCases.Nominative, "ein ungewöhnlicher Allagischer Stein der Phantasmagorie"),
+ new(nameof(LSheets.Item), 44348, ClientLanguage.German, 1, (int)GermanArticleType.Indefinite, (int)GermanCases.Genitive, "eines ungewöhnlichen Allagischen Steins der Phantasmagorie"),
+ new(nameof(LSheets.Item), 44348, ClientLanguage.German, 1, (int)GermanArticleType.Indefinite, (int)GermanCases.Dative, "einem ungewöhnlichen Allagischen Stein der Phantasmagorie"),
+ new(nameof(LSheets.Item), 44348, ClientLanguage.German, 1, (int)GermanArticleType.Indefinite, (int)GermanCases.Accusative, "einen ungewöhnlichen Allagischen Stein der Phantasmagorie"),
+ new(nameof(LSheets.Item), 44348, ClientLanguage.German, 1, (int)GermanArticleType.Definite, (int)GermanCases.Nominative, "der ungewöhnlicher Allagischer Stein der Phantasmagorie"),
+ new(nameof(LSheets.Item), 44348, ClientLanguage.German, 1, (int)GermanArticleType.Definite, (int)GermanCases.Genitive, "des ungewöhnlichen Allagischen Steins der Phantasmagorie"),
+ new(nameof(LSheets.Item), 44348, ClientLanguage.German, 1, (int)GermanArticleType.Definite, (int)GermanCases.Dative, "dem ungewöhnlichen Allagischen Stein der Phantasmagorie"),
+ new(nameof(LSheets.Item), 44348, ClientLanguage.German, 1, (int)GermanArticleType.Definite, (int)GermanCases.Accusative, "den ungewöhnlichen Allagischen Stein der Phantasmagorie"),
+ new(nameof(LSheets.Item), 44348, ClientLanguage.German, 1, (int)GermanArticleType.Possessive, (int)GermanCases.Nominative, "dein ungewöhnliche Allagische Stein der Phantasmagorie"),
+ new(nameof(LSheets.Item), 44348, ClientLanguage.German, 1, (int)GermanArticleType.Possessive, (int)GermanCases.Genitive, "deines ungewöhnlichen Allagischen Steins der Phantasmagorie"),
+ new(nameof(LSheets.Item), 44348, ClientLanguage.German, 1, (int)GermanArticleType.Possessive, (int)GermanCases.Dative, "deinem ungewöhnlichen Allagischen Stein der Phantasmagorie"),
+ new(nameof(LSheets.Item), 44348, ClientLanguage.German, 1, (int)GermanArticleType.Possessive, (int)GermanCases.Accusative, "deinen ungewöhnlichen Allagischen Stein der Phantasmagorie"),
+ new(nameof(LSheets.Item), 44348, ClientLanguage.German, 1, (int)GermanArticleType.Negative, (int)GermanCases.Nominative, "kein ungewöhnlicher Allagischer Stein der Phantasmagorie"),
+ new(nameof(LSheets.Item), 44348, ClientLanguage.German, 1, (int)GermanArticleType.Negative, (int)GermanCases.Genitive, "keines ungewöhnlichen Allagischen Steins der Phantasmagorie"),
+ new(nameof(LSheets.Item), 44348, ClientLanguage.German, 1, (int)GermanArticleType.Negative, (int)GermanCases.Dative, "keinem ungewöhnlichen Allagischen Stein der Phantasmagorie"),
+ new(nameof(LSheets.Item), 44348, ClientLanguage.German, 1, (int)GermanArticleType.Negative, (int)GermanCases.Accusative, "keinen ungewöhnlichen Allagischen Stein der Phantasmagorie"),
+ new(nameof(LSheets.Item), 44348, ClientLanguage.German, 1, (int)GermanArticleType.ZeroArticle, (int)GermanCases.Nominative, "ungewöhnlicher Allagischer Stein der Phantasmagorie"),
+ new(nameof(LSheets.Item), 44348, ClientLanguage.German, 1, (int)GermanArticleType.ZeroArticle, (int)GermanCases.Genitive, "ungewöhnlichen Allagischen Steins der Phantasmagorie"),
+ new(nameof(LSheets.Item), 44348, ClientLanguage.German, 1, (int)GermanArticleType.ZeroArticle, (int)GermanCases.Dative, "ungewöhnlichem Allagischem Stein der Phantasmagorie"),
+ new(nameof(LSheets.Item), 44348, ClientLanguage.German, 1, (int)GermanArticleType.ZeroArticle, (int)GermanCases.Accusative, "ungewöhnlichen Allagischen Stein der Phantasmagorie"),
+ new(nameof(LSheets.Item), 44348, ClientLanguage.German, 1, (int)GermanArticleType.Demonstrative, (int)GermanCases.Nominative, "dieser ungewöhnliche Allagische Stein der Phantasmagorie"),
+ new(nameof(LSheets.Item), 44348, ClientLanguage.German, 1, (int)GermanArticleType.Demonstrative, (int)GermanCases.Genitive, "dieses ungewöhnlichen Allagischen Steins der Phantasmagorie"),
+ new(nameof(LSheets.Item), 44348, ClientLanguage.German, 1, (int)GermanArticleType.Demonstrative, (int)GermanCases.Dative, "diesem ungewöhnlichen Allagischen Stein der Phantasmagorie"),
+ new(nameof(LSheets.Item), 44348, ClientLanguage.German, 1, (int)GermanArticleType.Demonstrative, (int)GermanCases.Accusative, "diesen ungewöhnlichen Allagischen Stein der Phantasmagorie"),
+ new(nameof(LSheets.Item), 44348, ClientLanguage.German, 2, (int)GermanArticleType.Indefinite, (int)GermanCases.Nominative, "2 ungewöhnlichen Allagischen Steine der Phantasmagorie"),
+ new(nameof(LSheets.Item), 44348, ClientLanguage.German, 2, (int)GermanArticleType.Indefinite, (int)GermanCases.Genitive, "2 ungewöhnlichen Allagischen Steine der Phantasmagorie"),
+ new(nameof(LSheets.Item), 44348, ClientLanguage.German, 2, (int)GermanArticleType.Indefinite, (int)GermanCases.Dative, "2 ungewöhnlichen Allagischen Steinen der Phantasmagorie"),
+ new(nameof(LSheets.Item), 44348, ClientLanguage.German, 2, (int)GermanArticleType.Indefinite, (int)GermanCases.Accusative, "2 ungewöhnlichen Allagischen Steine der Phantasmagorie"),
+ new(nameof(LSheets.Item), 44348, ClientLanguage.German, 2, (int)GermanArticleType.Definite, (int)GermanCases.Nominative, "die ungewöhnliche Allagische Steine der Phantasmagorie"),
+ new(nameof(LSheets.Item), 44348, ClientLanguage.German, 2, (int)GermanArticleType.Definite, (int)GermanCases.Genitive, "der ungewöhnlicher Allagischer Steine der Phantasmagorie"),
+ new(nameof(LSheets.Item), 44348, ClientLanguage.German, 2, (int)GermanArticleType.Definite, (int)GermanCases.Dative, "den ungewöhnlichen Allagischen Steinen der Phantasmagorie"),
+ new(nameof(LSheets.Item), 44348, ClientLanguage.German, 2, (int)GermanArticleType.Definite, (int)GermanCases.Accusative, "die ungewöhnliche Allagische Steine der Phantasmagorie"),
+ new(nameof(LSheets.Item), 44348, ClientLanguage.German, 2, (int)GermanArticleType.Possessive, (int)GermanCases.Nominative, "deine ungewöhnlichen Allagischen Steine der Phantasmagorie"),
+ new(nameof(LSheets.Item), 44348, ClientLanguage.German, 2, (int)GermanArticleType.Possessive, (int)GermanCases.Genitive, "deiner ungewöhnlichen Allagischen Steine der Phantasmagorie"),
+ new(nameof(LSheets.Item), 44348, ClientLanguage.German, 2, (int)GermanArticleType.Possessive, (int)GermanCases.Dative, "deinen ungewöhnlichen Allagischen Steinen der Phantasmagorie"),
+ new(nameof(LSheets.Item), 44348, ClientLanguage.German, 2, (int)GermanArticleType.Possessive, (int)GermanCases.Accusative, "deine ungewöhnlichen Allagischen Steine der Phantasmagorie"),
+ new(nameof(LSheets.Item), 44348, ClientLanguage.German, 2, (int)GermanArticleType.Negative, (int)GermanCases.Nominative, "keine ungewöhnlichen Allagischen Steine der Phantasmagorie"),
+ new(nameof(LSheets.Item), 44348, ClientLanguage.German, 2, (int)GermanArticleType.Negative, (int)GermanCases.Genitive, "keiner ungewöhnlichen Allagischen Steine der Phantasmagorie"),
+ new(nameof(LSheets.Item), 44348, ClientLanguage.German, 2, (int)GermanArticleType.Negative, (int)GermanCases.Dative, "keinen ungewöhnlichen Allagischen Steinen der Phantasmagorie"),
+ new(nameof(LSheets.Item), 44348, ClientLanguage.German, 2, (int)GermanArticleType.Negative, (int)GermanCases.Accusative, "keine ungewöhnlichen Allagischen Steine der Phantasmagorie"),
+ new(nameof(LSheets.Item), 44348, ClientLanguage.German, 2, (int)GermanArticleType.ZeroArticle, (int)GermanCases.Nominative, "ungewöhnliche Allagische Steine der Phantasmagorie"),
+ new(nameof(LSheets.Item), 44348, ClientLanguage.German, 2, (int)GermanArticleType.ZeroArticle, (int)GermanCases.Genitive, "ungewöhnlicher Allagischer Steine der Phantasmagorie"),
+ new(nameof(LSheets.Item), 44348, ClientLanguage.German, 2, (int)GermanArticleType.ZeroArticle, (int)GermanCases.Dative, "ungewöhnlichen Allagischen Steinen der Phantasmagorie"),
+ new(nameof(LSheets.Item), 44348, ClientLanguage.German, 2, (int)GermanArticleType.ZeroArticle, (int)GermanCases.Accusative, "ungewöhnliche Allagische Steine der Phantasmagorie"),
+ new(nameof(LSheets.Item), 44348, ClientLanguage.German, 2, (int)GermanArticleType.Demonstrative, (int)GermanCases.Nominative, "diese ungewöhnlichen Allagischen Steine der Phantasmagorie"),
+ new(nameof(LSheets.Item), 44348, ClientLanguage.German, 2, (int)GermanArticleType.Demonstrative, (int)GermanCases.Genitive, "dieser ungewöhnlichen Allagischen Steine der Phantasmagorie"),
+ new(nameof(LSheets.Item), 44348, ClientLanguage.German, 2, (int)GermanArticleType.Demonstrative, (int)GermanCases.Dative, "diesen ungewöhnlichen Allagischen Steinen der Phantasmagorie"),
+ new(nameof(LSheets.Item), 44348, ClientLanguage.German, 2, (int)GermanArticleType.Demonstrative, (int)GermanCases.Accusative, "diese ungewöhnlichen Allagischen Steine der Phantasmagorie"),
+
+ new(nameof(LSheets.Item), 44348, ClientLanguage.French, 1, (int)FrenchArticleType.Indefinite, 1, "un mémoquartz inhabituel fantasmagorique"),
+ new(nameof(LSheets.Item), 44348, ClientLanguage.French, 1, (int)FrenchArticleType.Definite, 1, "le mémoquartz inhabituel fantasmagorique"),
+ new(nameof(LSheets.Item), 44348, ClientLanguage.French, 1, (int)FrenchArticleType.PossessiveFirstPerson, 1, "mon mémoquartz inhabituel fantasmagorique"),
+ new(nameof(LSheets.Item), 44348, ClientLanguage.French, 1, (int)FrenchArticleType.PossessiveSecondPerson, 1, "ton mémoquartz inhabituel fantasmagorique"),
+ new(nameof(LSheets.Item), 44348, ClientLanguage.French, 1, (int)FrenchArticleType.PossessiveThirdPerson, 1, "son mémoquartz inhabituel fantasmagorique"),
+ new(nameof(LSheets.Item), 44348, ClientLanguage.French, 2, (int)FrenchArticleType.Indefinite, 1, "des mémoquartz inhabituels fantasmagoriques"),
+ new(nameof(LSheets.Item), 44348, ClientLanguage.French, 2, (int)FrenchArticleType.Definite, 1, "les mémoquartz inhabituels fantasmagoriques"),
+ new(nameof(LSheets.Item), 44348, ClientLanguage.French, 2, (int)FrenchArticleType.PossessiveFirstPerson, 1, "mes mémoquartz inhabituels fantasmagoriques"),
+ new(nameof(LSheets.Item), 44348, ClientLanguage.French, 2, (int)FrenchArticleType.PossessiveSecondPerson, 1, "tes mémoquartz inhabituels fantasmagoriques"),
+ new(nameof(LSheets.Item), 44348, ClientLanguage.French, 2, (int)FrenchArticleType.PossessiveThirdPerson, 1, "ses mémoquartz inhabituels fantasmagoriques"),
+
+ new(nameof(LSheets.Action), 45, ClientLanguage.German, 1, (int)FrenchArticleType.Indefinite, 1, "Blumenflüsterer IV"),
+ ];
+
+ private enum GermanCases
+ {
+ Nominative,
+ Genitive,
+ Dative,
+ Accusative,
+ }
+
+ ///
+ public string Name => "Test NounProcessor";
+
+ ///
+ public unsafe SelfTestStepResult RunStep()
+ {
+ var nounProcessor = Service.Get();
+
+ for (var i = 0; i < this.tests.Length; i++)
+ {
+ var e = this.tests[i];
+
+ var nounParams = new NounParams()
+ {
+ SheetName = e.SheetName,
+ RowId = e.RowId,
+ Language = e.Language,
+ Quantity = e.Quantity,
+ ArticleType = e.ArticleType,
+ GrammaticalCase = e.GrammaticalCase,
+ };
+ var output = nounProcessor.ProcessNoun(nounParams);
+
+ if (e.ExpectedResult != output)
+ {
+ ImGui.TextUnformatted($"Mismatch detected (Test #{i}):");
+ ImGui.TextUnformatted($"Got: {output}");
+ ImGui.TextUnformatted($"Expected: {e.ExpectedResult}");
+
+ if (ImGui.Button("Continue"))
+ return SelfTestStepResult.Fail;
+
+ return SelfTestStepResult.Waiting;
+ }
+ }
+
+ return SelfTestStepResult.Pass;
+ }
+
+ ///
+ public void CleanUp()
+ {
+ // ignored
+ }
+
+ private record struct NounTestEntry(
+ string SheetName,
+ uint RowId,
+ ClientLanguage Language,
+ int Quantity,
+ int ArticleType,
+ int GrammaticalCase,
+ string ExpectedResult);
+}
diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/SeStringEvaluatorAgingStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/SeStringEvaluatorAgingStep.cs
new file mode 100644
index 000000000..3a0a7d546
--- /dev/null
+++ b/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/SeStringEvaluatorAgingStep.cs
@@ -0,0 +1,92 @@
+using Dalamud.Game.ClientState;
+using Dalamud.Game.Text.Evaluator;
+
+using ImGuiNET;
+
+using Lumina.Text.ReadOnly;
+
+namespace Dalamud.Interface.Internal.Windows.SelfTest.AgingSteps;
+
+///
+/// Test setup for SeStringEvaluator.
+///
+internal class SeStringEvaluatorAgingStep : IAgingStep
+{
+ private int step = 0;
+
+ ///
+ public string Name => "Test SeStringEvaluator";
+
+ ///
+ public SelfTestStepResult RunStep()
+ {
+ var seStringEvaluator = Service.Get();
+
+ switch (this.step)
+ {
+ case 0:
+ ImGui.TextUnformatted("Is this the current time, and is it ticking?");
+
+ // This checks that EvaluateFromAddon fetches the correct Addon row,
+ // that MacroDecoder.GetMacroTime()->SetTime() has been called
+ // and that local and global parameters have been read correctly.
+
+ ImGui.TextUnformatted(seStringEvaluator.EvaluateFromAddon(31, [(uint)DateTimeOffset.UtcNow.ToUnixTimeSeconds()]).ExtractText());
+
+ if (ImGui.Button("Yes"))
+ this.step++;
+
+ ImGui.SameLine();
+
+ if (ImGui.Button("No"))
+ return SelfTestStepResult.Fail;
+
+ break;
+
+ case 1:
+ ImGui.TextUnformatted("Checking pcname macro using the local player name...");
+
+ // This makes sure that NameCache.Instance()->TryGetCharacterInfoByEntityId() has been called,
+ // that it returned the local players name by using its EntityId,
+ // and that it didn't include the world name by checking the HomeWorldId against AgentLobby.Instance()->LobbyData.HomeWorldId.
+
+ var clientState = Service.Get();
+ var localPlayer = clientState.LocalPlayer;
+ if (localPlayer is null)
+ {
+ ImGui.TextUnformatted("You need to be logged in for this step.");
+
+ if (ImGui.Button("Skip"))
+ return SelfTestStepResult.NotRan;
+
+ return SelfTestStepResult.Waiting;
+ }
+
+ var evaluatedPlayerName = seStringEvaluator.Evaluate(ReadOnlySeString.FromMacroString(""), [localPlayer.EntityId]).ExtractText();
+ var localPlayerName = localPlayer.Name.TextValue;
+
+ if (evaluatedPlayerName != localPlayerName)
+ {
+ ImGui.TextUnformatted("The player name doesn't match:");
+ ImGui.TextUnformatted($"Evaluated Player Name (got): {evaluatedPlayerName}");
+ ImGui.TextUnformatted($"Local Player Name (expected): {localPlayerName}");
+
+ if (ImGui.Button("Continue"))
+ return SelfTestStepResult.Fail;
+
+ return SelfTestStepResult.Waiting;
+ }
+
+ return SelfTestStepResult.Pass;
+ }
+
+ return SelfTestStepResult.Waiting;
+ }
+
+ ///
+ public void CleanUp()
+ {
+ // ignored
+ this.step = 0;
+ }
+}
diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/SheetRedirectResolverAgingStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/SheetRedirectResolverAgingStep.cs
new file mode 100644
index 000000000..0c9dc763f
--- /dev/null
+++ b/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/SheetRedirectResolverAgingStep.cs
@@ -0,0 +1,130 @@
+using System.Runtime.InteropServices;
+
+using Dalamud.Game;
+using Dalamud.Game.Text.Evaluator.Internal;
+
+using FFXIVClientStructs.FFXIV.Client.System.String;
+using FFXIVClientStructs.FFXIV.Client.UI.Misc;
+
+using ImGuiNET;
+
+namespace Dalamud.Interface.Internal.Windows.SelfTest.AgingSteps;
+
+///
+/// Test setup for SheetRedirectResolver.
+///
+internal class SheetRedirectResolverAgingStep : IAgingStep
+{
+ private RedirectEntry[] redirects =
+ [
+ new("Item", 10, SheetRedirectFlags.Item),
+ new("ItemHQ", 10, SheetRedirectFlags.Item | SheetRedirectFlags.HighQuality),
+ new("ItemMP", 10, SheetRedirectFlags.Item | SheetRedirectFlags.Collectible),
+ new("Item", 35588, SheetRedirectFlags.Item),
+ new("Item", 1035588, SheetRedirectFlags.Item | SheetRedirectFlags.HighQuality),
+ new("Item", 2000217, SheetRedirectFlags.Item | SheetRedirectFlags.EventItem),
+ new("ActStr", 10, SheetRedirectFlags.Action), // Trait
+ new("ActStr", 1000010, SheetRedirectFlags.Action | SheetRedirectFlags.ActionSheet), // Action
+ new("ActStr", 2000010, SheetRedirectFlags.Action), // Item
+ new("ActStr", 3000010, SheetRedirectFlags.Action), // EventItem
+ new("ActStr", 4000010, SheetRedirectFlags.Action), // EventAction
+ new("ActStr", 5000010, SheetRedirectFlags.Action), // GeneralAction
+ new("ActStr", 6000010, SheetRedirectFlags.Action), // BuddyAction
+ new("ActStr", 7000010, SheetRedirectFlags.Action), // MainCommand
+ new("ActStr", 8000010, SheetRedirectFlags.Action), // Companion
+ new("ActStr", 9000010, SheetRedirectFlags.Action), // CraftAction
+ new("ActStr", 10000010, SheetRedirectFlags.Action | SheetRedirectFlags.ActionSheet), // Action
+ new("ActStr", 11000010, SheetRedirectFlags.Action), // PetAction
+ new("ActStr", 12000010, SheetRedirectFlags.Action), // CompanyAction
+ new("ActStr", 13000010, SheetRedirectFlags.Action), // Mount
+ // new("ActStr", 14000010, RedirectFlags.Action),
+ // new("ActStr", 15000010, RedirectFlags.Action),
+ // new("ActStr", 16000010, RedirectFlags.Action),
+ // new("ActStr", 17000010, RedirectFlags.Action),
+ // new("ActStr", 18000010, RedirectFlags.Action),
+ new("ActStr", 19000010, SheetRedirectFlags.Action), // BgcArmyAction
+ new("ActStr", 20000010, SheetRedirectFlags.Action), // Ornament
+ new("ObjStr", 10), // BNpcName
+ new("ObjStr", 1000010), // ENpcResident
+ new("ObjStr", 2000010), // Treasure
+ new("ObjStr", 3000010), // Aetheryte
+ new("ObjStr", 4000010), // GatheringPointName
+ new("ObjStr", 5000010), // EObjName
+ new("ObjStr", 6000010), // Mount
+ new("ObjStr", 7000010), // Companion
+ // new("ObjStr", 8000010),
+ // new("ObjStr", 9000010),
+ new("ObjStr", 10000010), // Item
+ new("EObj", 2003479), // EObj => EObjName
+ new("Treasure", 1473), // Treasure (without name, falls back to rowId 0)
+ new("Treasure", 1474), // Treasure (with name)
+ new("WeatherPlaceName", 0),
+ new("WeatherPlaceName", 28),
+ new("WeatherPlaceName", 40),
+ new("WeatherPlaceName", 52),
+ new("WeatherPlaceName", 2300),
+ ];
+
+ private unsafe delegate SheetRedirectFlags ResolveSheetRedirect(RaptureTextModule* thisPtr, Utf8String* sheetName, uint* rowId, uint* flags);
+
+ ///
+ public string Name => "Test SheetRedirectResolver";
+
+ ///
+ public unsafe SelfTestStepResult RunStep()
+ {
+ // Client::UI::Misc::RaptureTextModule_ResolveSheetRedirect
+ if (!Service.Get().TryScanText("E8 ?? ?? ?? ?? 44 8B E8 A8 10", out var addr))
+ return SelfTestStepResult.Fail;
+
+ var sheetRedirectResolver = Service.Get();
+ var resolveSheetRedirect = Marshal.GetDelegateForFunctionPointer(addr);
+ var utf8SheetName = Utf8String.CreateEmpty();
+
+ try
+ {
+ for (var i = 0; i < this.redirects.Length; i++)
+ {
+ var redirect = this.redirects[i];
+
+ utf8SheetName->SetString(redirect.SheetName);
+
+ var rowId1 = redirect.RowId;
+ uint colIndex1 = ushort.MaxValue;
+ var flags1 = resolveSheetRedirect(RaptureTextModule.Instance(), utf8SheetName, &rowId1, &colIndex1);
+
+ var sheetName2 = redirect.SheetName;
+ var rowId2 = redirect.RowId;
+ uint colIndex2 = ushort.MaxValue;
+ var flags2 = sheetRedirectResolver.Resolve(ref sheetName2, ref rowId2, ref colIndex2);
+
+ if (utf8SheetName->ToString() != sheetName2 || rowId1 != rowId2 || colIndex1 != colIndex2 || flags1 != flags2)
+ {
+ ImGui.TextUnformatted($"Mismatch detected (Test #{i}):");
+ ImGui.TextUnformatted($"Input: {redirect.SheetName}#{redirect.RowId}");
+ ImGui.TextUnformatted($"Game: {utf8SheetName->ToString()}#{rowId1}-{colIndex1} ({flags1})");
+ ImGui.TextUnformatted($"Evaluated: {sheetName2}#{rowId2}-{colIndex2} ({flags2})");
+
+ if (ImGui.Button("Continue"))
+ return SelfTestStepResult.Fail;
+
+ return SelfTestStepResult.Waiting;
+ }
+ }
+
+ return SelfTestStepResult.Pass;
+ }
+ finally
+ {
+ utf8SheetName->Dtor(true);
+ }
+ }
+
+ ///
+ public void CleanUp()
+ {
+ // ignored
+ }
+
+ private record struct RedirectEntry(string SheetName, uint RowId, SheetRedirectFlags Flags = SheetRedirectFlags.None);
+}
diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs b/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs
index 3b3670228..1be6f31a3 100644
--- a/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs
+++ b/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs
@@ -50,6 +50,9 @@ internal class SelfTestWindow : Window
new DutyStateAgingStep(),
new GameConfigAgingStep(),
new MarketBoardAgingStep(),
+ new SheetRedirectResolverAgingStep(),
+ new NounProcessorAgingStep(),
+ new SeStringEvaluatorAgingStep(),
new LogoutEventAgingStep(),
};
diff --git a/Dalamud/Interface/Utility/ImGuiHelpers.cs b/Dalamud/Interface/Utility/ImGuiHelpers.cs
index 2093d9bcb..d9056fec4 100644
--- a/Dalamud/Interface/Utility/ImGuiHelpers.cs
+++ b/Dalamud/Interface/Utility/ImGuiHelpers.cs
@@ -210,8 +210,6 @@ public static class ImGuiHelpers
/// ImGui ID, if link functionality is desired.
/// Button flags to use on link interaction.
/// Interaction result of the rendered text.
- /// This function is experimental. Report any issues to GitHub issues or to Discord #dalamud-dev channel.
- /// The function definition is stable; only in the next API version a function may be removed.
public static SeStringDrawResult SeStringWrapped(
ReadOnlySpan sss,
scoped in SeStringDrawParams style = default,
@@ -226,8 +224,6 @@ public static class ImGuiHelpers
/// ImGui ID, if link functionality is desired.
/// Button flags to use on link interaction.
/// Interaction result of the rendered text.
- /// This function is experimental. Report any issues to GitHub issues or to Discord #dalamud-dev channel.
- /// The function definition is stable; only in the next API version a function may be removed.
public static SeStringDrawResult CompileSeStringWrapped(
string text,
scoped in SeStringDrawParams style = default,
diff --git a/Dalamud/Plugin/Services/ISeStringEvaluator.cs b/Dalamud/Plugin/Services/ISeStringEvaluator.cs
new file mode 100644
index 000000000..2bd423b7c
--- /dev/null
+++ b/Dalamud/Plugin/Services/ISeStringEvaluator.cs
@@ -0,0 +1,79 @@
+using System.Diagnostics.CodeAnalysis;
+
+using Dalamud.Game;
+using Dalamud.Game.ClientState.Objects.Enums;
+using Dalamud.Game.Text.Evaluator;
+
+using Lumina.Text.ReadOnly;
+
+namespace Dalamud.Plugin.Services;
+
+///
+/// Defines a service for retrieving localized text for various in-game entities.
+///
+[Experimental("SeStringEvaluator")]
+public interface ISeStringEvaluator
+{
+ ///
+ /// Evaluates macros in a .
+ ///
+ /// The string containing macros.
+ /// An optional list of local parameters.
+ /// An optional language override.
+ /// An evaluated .
+ ReadOnlySeString Evaluate(ReadOnlySeString str, Span localParameters = default, ClientLanguage? language = null);
+
+ ///
+ /// Evaluates macros in a .
+ ///
+ /// The string containing macros.
+ /// An optional list of local parameters.
+ /// An optional language override.
+ /// An evaluated .
+ ReadOnlySeString Evaluate(ReadOnlySeStringSpan str, Span localParameters = default, ClientLanguage? language = null);
+
+ ///
+ /// Evaluates macros in text from the Addon sheet.
+ ///
+ /// The row id of the Addon sheet.
+ /// An optional list of local parameters.
+ /// An optional language override.
+ /// An evaluated .
+ ReadOnlySeString EvaluateFromAddon(uint addonId, Span localParameters = default, ClientLanguage? language = null);
+
+ ///
+ /// Evaluates macros in text from the Lobby sheet.
+ ///
+ /// The row id of the Lobby sheet.
+ /// An optional list of local parameters.
+ /// An optional language override.
+ /// An evaluated .
+ ReadOnlySeString EvaluateFromLobby(uint lobbyId, Span localParameters = default, ClientLanguage? language = null);
+
+ ///
+ /// Evaluates macros in text from the LogMessage sheet.
+ ///
+ /// The row id of the LogMessage sheet.
+ /// An optional list of local parameters.
+ /// An optional language override.
+ /// An evaluated .
+ ReadOnlySeString EvaluateFromLogMessage(uint logMessageId, Span localParameters = default, ClientLanguage? language = null);
+
+ ///
+ /// Evaluates ActStr from the given ActionKind and id.
+ ///
+ /// The ActionKind.
+ /// The action id.
+ /// An optional language override.
+ /// The name of the action.
+ string EvaluateActStr(ActionKind actionKind, uint id, ClientLanguage? language = null);
+
+ ///
+ /// Evaluates ObjStr from the given ObjectKind and id.
+ ///
+ /// The ObjectKind.
+ /// The object id.
+ /// An optional language override.
+ /// The singular name of the object.
+ string EvaluateObjStr(ObjectKind objectKind, uint id, ClientLanguage? language = null);
+}
diff --git a/Dalamud/Utility/ActionKindExtensions.cs b/Dalamud/Utility/ActionKindExtensions.cs
new file mode 100644
index 000000000..21026bc31
--- /dev/null
+++ b/Dalamud/Utility/ActionKindExtensions.cs
@@ -0,0 +1,26 @@
+using Dalamud.Game;
+
+namespace Dalamud.Utility;
+
+///
+/// Extension methods for the enum.
+///
+public static class ActionKindExtensions
+{
+ ///
+ /// Converts the id of an ActionKind to the id used in the ActStr sheet redirect.
+ ///
+ /// The ActionKind this id is for.
+ /// The id.
+ /// An id that can be used in the ActStr sheet redirect.
+ public static uint GetActStrId(this ActionKind actionKind, uint id)
+ {
+ // See "83 F9 0D 76 0B"
+ var idx = (uint)actionKind;
+
+ if (idx is <= 13 or 19 or 20)
+ return id + (1000000 * idx);
+
+ return 0;
+ }
+}
diff --git a/Dalamud/Utility/ClientLanguageExtensions.cs b/Dalamud/Utility/ClientLanguageExtensions.cs
index 69c39c9b8..47f0a2082 100644
--- a/Dalamud/Utility/ClientLanguageExtensions.cs
+++ b/Dalamud/Utility/ClientLanguageExtensions.cs
@@ -23,4 +23,40 @@ public static class ClientLanguageExtensions
_ => throw new ArgumentOutOfRangeException(nameof(language)),
};
}
+
+ ///
+ /// Gets the language code from a ClientLanguage.
+ ///
+ /// The ClientLanguage to convert.
+ /// The language code (ja, en, de, fr).
+ /// An exception that is thrown when no valid ClientLanguage was given.
+ public static string ToCode(this ClientLanguage value)
+ {
+ return value switch
+ {
+ ClientLanguage.Japanese => "ja",
+ ClientLanguage.English => "en",
+ ClientLanguage.German => "de",
+ ClientLanguage.French => "fr",
+ _ => throw new ArgumentOutOfRangeException(nameof(value)),
+ };
+ }
+
+ ///
+ /// Gets the ClientLanguage from a language code.
+ ///
+ /// The language code to convert (ja, en, de, fr).
+ /// The ClientLanguage.
+ /// An exception that is thrown when no valid language code was given.
+ public static ClientLanguage ToClientLanguage(this string value)
+ {
+ return value switch
+ {
+ "ja" => ClientLanguage.Japanese,
+ "en" => ClientLanguage.English,
+ "de" => ClientLanguage.German,
+ "fr" => ClientLanguage.French,
+ _ => throw new ArgumentOutOfRangeException(nameof(value)),
+ };
+ }
}
diff --git a/Dalamud/Utility/ItemUtil.cs b/Dalamud/Utility/ItemUtil.cs
new file mode 100644
index 000000000..de1e5a721
--- /dev/null
+++ b/Dalamud/Utility/ItemUtil.cs
@@ -0,0 +1,159 @@
+using System.Runtime.CompilerServices;
+
+using Dalamud.Data;
+using Dalamud.Game;
+using Dalamud.Game.Text.SeStringHandling.Payloads;
+using Dalamud.Game.Text;
+
+using Lumina.Excel.Sheets;
+using Lumina.Text.ReadOnly;
+
+using static Dalamud.Game.Text.SeStringHandling.Payloads.ItemPayload;
+using Lumina.Text;
+
+namespace Dalamud.Utility;
+
+///
+/// Utilities related to Items.
+///
+internal static class ItemUtil
+{
+ private static int? eventItemRowCount;
+
+ /// Converts raw item ID to item ID with its classification.
+ /// Raw item ID.
+ /// Item ID and its classification.
+ internal static (uint ItemId, ItemKind Kind) GetBaseId(uint rawItemId)
+ {
+ if (IsEventItem(rawItemId)) return (rawItemId, ItemKind.EventItem); // EventItem IDs are NOT adjusted
+ if (IsHighQuality(rawItemId)) return (rawItemId - 1_000_000, ItemKind.Hq);
+ if (IsCollectible(rawItemId)) return (rawItemId - 500_000, ItemKind.Collectible);
+ return (rawItemId, ItemKind.Normal);
+ }
+
+ /// Converts item ID with its classification to raw item ID.
+ /// Item ID.
+ /// Item classification.
+ /// Raw Item ID.
+ internal static uint GetRawId(uint itemId, ItemKind kind)
+ {
+ return kind switch
+ {
+ ItemKind.Collectible when itemId < 500_000 => itemId + 500_000,
+ ItemKind.Hq when itemId < 1_000_000 => itemId + 1_000_000,
+ ItemKind.EventItem => itemId, // EventItem IDs are not adjusted
+ _ => itemId,
+ };
+ }
+
+ ///
+ /// Checks if the item id belongs to a normal item.
+ ///
+ /// The item id to check.
+ /// true when the item id belongs to a normal item.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static bool IsNormalItem(uint itemId)
+ {
+ return itemId < 500_000;
+ }
+
+ ///
+ /// Checks if the item id belongs to a collectible item.
+ ///
+ /// The item id to check.
+ /// true when the item id belongs to a collectible item.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static bool IsCollectible(uint itemId)
+ {
+ return itemId is >= 500_000 and < 1_000_000;
+ }
+
+ ///
+ /// Checks if the item id belongs to a high quality item.
+ ///
+ /// The item id to check.
+ /// true when the item id belongs to a high quality item.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static bool IsHighQuality(uint itemId)
+ {
+ return itemId is >= 1_000_000 and < 2_000_000;
+ }
+
+ ///
+ /// Checks if the item id belongs to an event item.
+ ///
+ /// The item id to check.
+ /// true when the item id belongs to an event item.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static bool IsEventItem(uint itemId)
+ {
+ return itemId >= 2_000_000 && itemId - 2_000_000 < (eventItemRowCount ??= Service.Get().GetExcelSheet().Count);
+ }
+
+ ///
+ /// Gets the name of an item.
+ ///
+ /// The raw item id.
+ /// Whether to include the High Quality or Collectible icon.
+ /// An optional client language override.
+ /// The item name.
+ internal static ReadOnlySeString GetItemName(uint itemId, bool includeIcon = true, ClientLanguage? language = null)
+ {
+ var dataManager = Service.Get();
+
+ if (IsEventItem(itemId))
+ {
+ return dataManager
+ .GetExcelSheet(language)
+ .TryGetRow(itemId, out var eventItem)
+ ? eventItem.Name
+ : default;
+ }
+
+ var (baseId, kind) = GetBaseId(itemId);
+
+ if (!dataManager
+ .GetExcelSheet
- (language)
+ .TryGetRow(baseId, out var item))
+ {
+ return default;
+ }
+
+ if (!includeIcon || kind is not (ItemKind.Hq or ItemKind.Collectible))
+ return item.Name;
+
+ var builder = SeStringBuilder.SharedPool.Get();
+
+ builder.Append(item.Name);
+
+ switch (kind)
+ {
+ case ItemPayload.ItemKind.Hq:
+ builder.Append($" {(char)SeIconChar.HighQuality}");
+ break;
+ case ItemPayload.ItemKind.Collectible:
+ builder.Append($" {(char)SeIconChar.Collectible}");
+ break;
+ }
+
+ var itemName = builder.ToReadOnlySeString();
+ SeStringBuilder.SharedPool.Return(builder);
+ return itemName;
+ }
+
+ ///
+ /// Gets the color row id for an item name.
+ ///
+ /// The raw item Id.
+ /// Wheather this color is used as edge color.
+ /// The Color row id.
+ internal static uint GetItemRarityColorType(uint itemId, bool isEdgeColor = false)
+ {
+ var rarity = 1u;
+
+ if (!IsEventItem(itemId) && Service.Get().GetExcelSheet
- ().TryGetRow(GetBaseId(itemId).ItemId, out var item))
+ rarity = item.Rarity;
+
+ return (isEdgeColor ? 548u : 547u) + (rarity * 2u);
+ }
+}
diff --git a/Dalamud/Utility/ObjectKindExtensions.cs b/Dalamud/Utility/ObjectKindExtensions.cs
new file mode 100644
index 000000000..5d42dc760
--- /dev/null
+++ b/Dalamud/Utility/ObjectKindExtensions.cs
@@ -0,0 +1,33 @@
+using Dalamud.Game.ClientState.Objects.Enums;
+
+namespace Dalamud.Utility;
+
+///
+/// Extension methods for the enum.
+///
+public static class ObjectKindExtensions
+{
+ ///
+ /// Converts the id of an ObjectKind to the id used in the ObjStr sheet redirect.
+ ///
+ /// The ObjectKind this id is for.
+ /// The id.
+ /// An id that can be used in the ObjStr sheet redirect.
+ public static uint GetObjStrId(this ObjectKind objectKind, uint id)
+ {
+ // See "8D 41 FE 83 F8 0C 77 4D"
+ return objectKind switch
+ {
+ ObjectKind.BattleNpc => id < 1000000 ? id : id - 900000,
+ ObjectKind.EventNpc => id,
+ ObjectKind.Treasure or
+ ObjectKind.Aetheryte or
+ ObjectKind.GatheringPoint or
+ ObjectKind.Companion or
+ ObjectKind.Housing => id + (1000000 * (uint)objectKind) - 2000000,
+ ObjectKind.EventObj => id + (1000000 * (uint)objectKind) - 4000000,
+ ObjectKind.CardStand => id + 3000000,
+ _ => 0,
+ };
+ }
+}
diff --git a/Dalamud/Utility/SeStringExtensions.cs b/Dalamud/Utility/SeStringExtensions.cs
index f50f19d8e..b21b9b743 100644
--- a/Dalamud/Utility/SeStringExtensions.cs
+++ b/Dalamud/Utility/SeStringExtensions.cs
@@ -1,3 +1,5 @@
+using System.Linq;
+
using Lumina.Text.Parse;
using Lumina.Text.ReadOnly;
@@ -74,4 +76,154 @@ public static class SeStringExtensions
/// character name to validate.
/// indicator if character is name is valid.
public static bool IsValidCharacterName(this DSeString value) => value.ToString().IsValidCharacterName();
+
+ ///
+ /// Determines whether the contains only text payloads.
+ ///
+ /// The to check.
+ /// true if the string contains only text payloads; otherwise, false.
+ public static bool IsTextOnly(this ReadOnlySeString ross)
+ {
+ return ross.AsSpan().IsTextOnly();
+ }
+
+ ///
+ /// Determines whether the contains only text payloads.
+ ///
+ /// The to check.
+ /// true if the span contains only text payloads; otherwise, false.
+ public static bool IsTextOnly(this ReadOnlySeStringSpan rosss)
+ {
+ foreach (var payload in rosss)
+ {
+ if (payload.Type != ReadOnlySePayloadType.Text)
+ return false;
+ }
+
+ return true;
+ }
+
+ ///
+ /// Determines whether the contains the specified text.
+ ///
+ /// The to search.
+ /// The text to find.
+ /// true if the text is found; otherwise, false.
+ public static bool ContainsText(this ReadOnlySeString ross, ReadOnlySpan needle)
+ {
+ return ross.AsSpan().ContainsText(needle);
+ }
+
+ ///
+ /// Determines whether the contains the specified text.
+ ///
+ /// The to search.
+ /// The text to find.
+ /// true if the text is found; otherwise, false.
+ public static bool ContainsText(this ReadOnlySeStringSpan rosss, ReadOnlySpan needle)
+ {
+ foreach (var payload in rosss)
+ {
+ if (payload.Type != ReadOnlySePayloadType.Text)
+ continue;
+
+ if (payload.Body.IndexOf(needle) != -1)
+ return true;
+ }
+
+ return false;
+ }
+
+ ///
+ /// Determines whether the contains the specified text.
+ ///
+ /// The builder to search.
+ /// The text to find.
+ /// true if the text is found; otherwise, false.
+ public static bool ContainsText(this LSeStringBuilder builder, ReadOnlySpan needle)
+ {
+ return builder.ToReadOnlySeString().ContainsText(needle);
+ }
+
+ ///
+ /// Replaces occurrences of a specified text in a with another text.
+ ///
+ /// The original string.
+ /// The text to find.
+ /// The replacement text.
+ /// A new with the replacements made.
+ public static ReadOnlySeString ReplaceText(
+ this ReadOnlySeString ross,
+ ReadOnlySpan toFind,
+ ReadOnlySpan replacement)
+ {
+ if (ross.IsEmpty)
+ return ross;
+
+ var sb = LSeStringBuilder.SharedPool.Get();
+
+ foreach (var payload in ross)
+ {
+ if (payload.Type == ReadOnlySePayloadType.Invalid)
+ continue;
+
+ if (payload.Type != ReadOnlySePayloadType.Text)
+ {
+ sb.Append(payload);
+ continue;
+ }
+
+ var index = payload.Body.Span.IndexOf(toFind);
+ if (index == -1)
+ {
+ sb.Append(payload);
+ continue;
+ }
+
+ var lastIndex = 0;
+ while (index != -1)
+ {
+ sb.Append(payload.Body.Span[lastIndex..index]);
+
+ if (!replacement.IsEmpty)
+ {
+ sb.Append(replacement);
+ }
+
+ lastIndex = index + toFind.Length;
+ index = payload.Body.Span[lastIndex..].IndexOf(toFind);
+
+ if (index != -1)
+ index += lastIndex;
+ }
+
+ sb.Append(payload.Body.Span[lastIndex..]);
+ }
+
+ var output = sb.ToReadOnlySeString();
+ LSeStringBuilder.SharedPool.Return(sb);
+ return output;
+ }
+
+ ///
+ /// Replaces occurrences of a specified text in an with another text.
+ ///
+ /// The builder to modify.
+ /// The text to find.
+ /// The replacement text.
+ public static void ReplaceText(
+ this LSeStringBuilder builder,
+ ReadOnlySpan toFind,
+ ReadOnlySpan replacement)
+ {
+ if (toFind.IsEmpty)
+ return;
+
+ var str = builder.ToReadOnlySeString();
+ if (str.IsEmpty)
+ return;
+
+ var replaced = ReplaceText(new ReadOnlySeString(builder.GetViewAsMemory()), toFind, replacement);
+ builder.Clear().Append(replaced);
+ }
}
diff --git a/Dalamud/Utility/StringExtensions.cs b/Dalamud/Utility/StringExtensions.cs
index 24aa48446..50973e338 100644
--- a/Dalamud/Utility/StringExtensions.cs
+++ b/Dalamud/Utility/StringExtensions.cs
@@ -1,4 +1,5 @@
using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
using FFXIVClientStructs.FFXIV.Client.UI;
@@ -43,4 +44,48 @@ public static class StringExtensions
if (!UIGlobals.IsValidPlayerCharacterName(value)) return false;
return includeLegacy || value.Length <= 21;
}
+
+ ///
+ /// Converts the first character of the string to uppercase while leaving the rest of the string unchanged.
+ ///
+ /// The input string.
+ ///
+ /// A new string with the first character converted to uppercase.
+ [return: NotNullIfNotNull("input")]
+ public static string? FirstCharToUpper(this string? input, CultureInfo? culture = null) =>
+ string.IsNullOrWhiteSpace(input)
+ ? input
+ : $"{char.ToUpper(input[0], culture ?? CultureInfo.CurrentCulture)}{input.AsSpan(1)}";
+
+ ///
+ /// Converts the first character of the string to lowercase while leaving the rest of the string unchanged.
+ ///
+ /// The input string.
+ ///
+ /// A new string with the first character converted to lowercase.
+ [return: NotNullIfNotNull("input")]
+ public static string? FirstCharToLower(this string? input, CultureInfo? culture = null) =>
+ string.IsNullOrWhiteSpace(input)
+ ? input
+ : $"{char.ToLower(input[0], culture ?? CultureInfo.CurrentCulture)}{input.AsSpan(1)}";
+
+ ///
+ /// Removes soft hyphen characters (U+00AD) from the input string.
+ ///
+ /// The input string to remove soft hyphen characters from.
+ /// A string with all soft hyphens removed.
+ public static string StripSoftHyphen(this string input) => input.Replace("\u00AD", string.Empty);
+
+ ///
+ /// Truncates the given string to the specified maximum number of characters,
+ /// appending an ellipsis if truncation occurs.
+ ///
+ /// The string to truncate.
+ /// The maximum allowed length of the string.
+ /// The string to append if truncation occurs (defaults to "...").
+ /// The truncated string, or the original string if no truncation is needed.
+ public static string? Truncate(this string input, int maxChars, string ellipses = "...")
+ {
+ return string.IsNullOrEmpty(input) || input.Length <= maxChars ? input : input[..maxChars] + ellipses;
+ }
}