Add SeStringEvaluator service (#2188)

* Add SeStringEvaluator service

* Move DrawCopyableText into WidgetUtil

* Use Icon2RemapTable in SeStringRenderer

* Beautify some code

* Make sure to use the correct language

* Add SeString Creator widget

* Fix getting local parameters

* Update expressionNames

* misc changes

* Use InvariantCulture in TryResolveSheet

* Add SeStringEvaluatorAgingStep

* Fix item id comparisons

* Add SheetRedirectResolverAgingStep

* Add NounProcessorAgingStep

* Update SeString.CreateItemLink

This also adds the internal ItemUtil class.

* Fix name of SeStringCreator widget

* Add Global Parameters tab to SeStringCreatorWidget

* Load widgets on demand

* Update SeStringCreatorWidget

* Resizable SeStringCreatorWidget panels

* Update GamepadStateAgingStep

* Experimental status was removed in #2144

* Update SheetRedirectResolver, rewrite Noun params

* Fixes for 4 am code

* Remove incorrect column offset

I have no idea how that happened.

* Draw names of linked things

---------

Co-authored-by: Soreepeong <3614868+Soreepeong@users.noreply.github.com>
Co-authored-by: KazWolfe <KazWolfe@users.noreply.github.com>
This commit is contained in:
Haselnussbomber 2025-03-24 17:00:27 +01:00 committed by GitHub
parent 7cac19ce81
commit fdbfdbb2cd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
36 changed files with 5831 additions and 196 deletions

View file

@ -0,0 +1,89 @@
namespace Dalamud.Game;
/// <summary>
/// Enum describing possible action kinds.
/// </summary>
public enum ActionKind
{
/// <summary>
/// A Trait.
/// </summary>
Trait = 0,
/// <summary>
/// An Action.
/// </summary>
Action = 1,
/// <summary>
/// A usable Item.
/// </summary>
Item = 2, // does not work?
/// <summary>
/// A usable EventItem.
/// </summary>
EventItem = 3, // does not work?
/// <summary>
/// An EventAction.
/// </summary>
EventAction = 4,
/// <summary>
/// A GeneralAction.
/// </summary>
GeneralAction = 5,
/// <summary>
/// A BuddyAction.
/// </summary>
BuddyAction = 6,
/// <summary>
/// A MainCommand.
/// </summary>
MainCommand = 7,
/// <summary>
/// A Companion.
/// </summary>
Companion = 8, // unresolved?!
/// <summary>
/// A CraftAction.
/// </summary>
CraftAction = 9,
/// <summary>
/// An Action (again).
/// </summary>
Action2 = 10, // what's the difference?
/// <summary>
/// A PetAction.
/// </summary>
PetAction = 11,
/// <summary>
/// A CompanyAction.
/// </summary>
CompanyAction = 12,
/// <summary>
/// A Mount.
/// </summary>
Mount = 13,
// 14-18 unused
/// <summary>
/// A BgcArmyAction.
/// </summary>
BgcArmyAction = 19,
/// <summary>
/// An Ornament.
/// </summary>
Ornament = 20,
}

View file

@ -323,7 +323,7 @@ internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui
return ret; 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.handleActionHoverHook.Original(hoverState, actionKind, actionId, a4, a5);
this.HoveredAction.ActionKind = (HoverActionKind)actionKind; this.HoveredAction.ActionKind = (HoverActionKind)actionKind;

View file

@ -0,0 +1,30 @@
using Lumina.Text;
namespace Dalamud.Game.Text.Evaluator.Internal;
/// <summary>
/// Wraps payloads in an open and close icon, for example the Auto Translation open/close brackets.
/// </summary>
internal readonly struct SeStringBuilderIconWrap : IDisposable
{
private readonly SeStringBuilder builder;
private readonly uint iconClose;
/// <summary>
/// Initializes a new instance of the <see cref="SeStringBuilderIconWrap"/> struct.<br/>
/// Appends an icon macro with <paramref name="iconOpen"/> on creation, and an icon macro with
/// <paramref name="iconClose"/> on disposal.
/// </summary>
/// <param name="builder">The builder to use.</param>
/// <param name="iconOpen">The open icon id.</param>
/// <param name="iconClose">The close icon id.</param>
public SeStringBuilderIconWrap(SeStringBuilder builder, uint iconOpen, uint iconClose)
{
this.builder = builder;
this.iconClose = iconClose;
this.builder.AppendIcon(iconOpen);
}
/// <inheritdoc/>
public void Dispose() => this.builder.AppendIcon(this.iconClose);
}

View file

@ -0,0 +1,83 @@
using System.Globalization;
using Dalamud.Utility;
using Lumina.Text;
using Lumina.Text.ReadOnly;
namespace Dalamud.Game.Text.Evaluator.Internal;
/// <summary>
/// A context wrapper used in <see cref="SeStringEvaluator"/>.
/// </summary>
internal readonly ref struct SeStringContext
{
/// <summary>
/// The <see cref="SeStringBuilder"/> to append text and macros to.
/// </summary>
internal readonly SeStringBuilder Builder;
/// <summary>
/// A list of local parameters.
/// </summary>
internal readonly Span<SeStringParameter> LocalParameters;
/// <summary>
/// The target language, used for sheet lookups.
/// </summary>
internal readonly ClientLanguage Language;
/// <summary>
/// Initializes a new instance of the <see cref="SeStringContext"/> struct.
/// </summary>
/// <param name="builder">The <see cref="SeStringBuilder"/> to append text and macros to.</param>
/// <param name="localParameters">A list of local parameters.</param>
/// <param name="language">The target language, used for sheet lookups.</param>
internal SeStringContext(SeStringBuilder builder, Span<SeStringParameter> localParameters, ClientLanguage language)
{
this.Builder = builder;
this.LocalParameters = localParameters;
this.Language = language;
}
/// <summary>
/// Gets the <see cref="System.Globalization.CultureInfo"/> of the current target <see cref="Language"/>.
/// </summary>
internal CultureInfo CultureInfo => Localization.GetCultureInfoFromLangCode(this.Language.ToCode());
/// <summary>
/// Tries to get a number from the local parameters at the specified index.
/// </summary>
/// <param name="index">The index in the <see cref="LocalParameters"/> list.</param>
/// <param name="value">The local parameter number.</param>
/// <returns><c>true</c> if the local parameters list contained a parameter at given index, <c>false</c> otherwise.</returns>
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;
}
/// <summary>
/// Tries to get a string from the local parameters at the specified index.
/// </summary>
/// <param name="index">The index in the <see cref="LocalParameters"/> list.</param>
/// <param name="value">The local parameter string.</param>
/// <returns><c>true</c> if the local parameters list contained a parameter at given index, <c>false</c> otherwise.</returns>
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;
}
}

View file

@ -0,0 +1,49 @@
namespace Dalamud.Game.Text.Evaluator.Internal;
/// <summary>
/// An enum providing additional information about the sheet redirect.
/// </summary>
[Flags]
internal enum SheetRedirectFlags
{
/// <summary>
/// No flags.
/// </summary>
None = 0,
/// <summary>
/// Resolved to a sheet related with items.
/// </summary>
Item = 1,
/// <summary>
/// Resolved to the EventItem sheet.
/// </summary>
EventItem = 2,
/// <summary>
/// Resolved to a high quality item.
/// </summary>
/// <remarks>
/// Append Addon#9.
/// </remarks>
HighQuality = 4,
/// <summary>
/// Resolved to a collectible item.
/// </summary>
/// <remarks>
/// Append Addon#150.
/// </remarks>
Collectible = 8,
/// <summary>
/// Resolved to a sheet related with actions.
/// </summary>
Action = 16,
/// <summary>
/// Resolved to the Action sheet.
/// </summary>
ActionSheet = 32,
}

View file

@ -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;
/// <summary>
/// A service to resolve sheet redirects in expressions.
/// </summary>
[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<DataManager>.Get();
[ServiceManager.ServiceConstructor]
private SheetRedirectResolver()
{
}
/// <summary>
/// Resolves the sheet redirect, if any is present.
/// </summary>
/// <param name="sheetName">The sheet name.</param>
/// <param name="rowId">The row id.</param>
/// <param name="colIndex">The column index. Use <c>ushort.MaxValue</c> as default.</param>
/// <returns>Flags giving additional information about the redirect.</returns>
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<LSheets.EventItem>().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<LSheets.Treasure>().TryGetRow(rowId, out var treasureRow) &&
treasureRow.Unknown0.IsEmpty)
rowId = 0; // defaulting to "Treasure Coffer"
break;
case 3: // Aetheryte
rowId = this.dataManager.GetExcelSheet<LSheets.Aetheryte>()
.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<LSheets.Treasure>().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<LSheets.WeatherReportReplace>().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<LSheets.InstanceContent>().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<LSheets.PartyContent>().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<LSheets.PublicContent>().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<LSheets.AkatsukiNote>().TryGetRow(rowId, out var row))
rowId = (uint)row[0].Unknown2;
break;
}
}
return flags;
}
}

File diff suppressed because it is too large Load diff

View file

@ -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;
/// <summary>
/// A wrapper for a local parameter, holding either a number or a string.
/// </summary>
public readonly struct SeStringParameter
{
private readonly uint num;
private readonly ReadOnlySeString str;
/// <summary>
/// Initializes a new instance of the <see cref="SeStringParameter"/> struct for a number parameter.
/// </summary>
/// <param name="value">The number value.</param>
public SeStringParameter(uint value)
{
this.num = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="SeStringParameter"/> struct for a string parameter.
/// </summary>
/// <param name="value">The string value.</param>
public SeStringParameter(ReadOnlySeString value)
{
this.str = value;
this.IsString = true;
}
/// <summary>
/// Initializes a new instance of the <see cref="SeStringParameter"/> struct for a string parameter.
/// </summary>
/// <param name="value">The string value.</param>
public SeStringParameter(string value)
{
this.str = new ReadOnlySeString(value);
this.IsString = true;
}
/// <summary>
/// Gets a value indicating whether the backing type of this parameter is a string.
/// </summary>
public bool IsString { get; }
/// <summary>
/// Gets a numeric value.
/// </summary>
public uint UIntValue =>
!this.IsString
? this.num
: uint.TryParse(this.str.ExtractText(), out var value) ? value : 0;
/// <summary>
/// Gets a string value.
/// </summary>
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);
}

View file

@ -0,0 +1,17 @@
namespace Dalamud.Game.Text.Noun.Enums;
/// <summary>
/// Article types for <see cref="ClientLanguage.English"/>.
/// </summary>
public enum EnglishArticleType
{
/// <summary>
/// Indefinite article (a, an).
/// </summary>
Indefinite = 1,
/// <summary>
/// Definite article (the).
/// </summary>
Definite = 2,
}

View file

@ -0,0 +1,32 @@
namespace Dalamud.Game.Text.Noun.Enums;
/// <summary>
/// Article types for <see cref="ClientLanguage.French"/>.
/// </summary>
public enum FrenchArticleType
{
/// <summary>
/// Indefinite article (une, des).
/// </summary>
Indefinite = 1,
/// <summary>
/// Definite article (le, la, les).
/// </summary>
Definite = 2,
/// <summary>
/// Possessive article (mon, mes).
/// </summary>
PossessiveFirstPerson = 3,
/// <summary>
/// Possessive article (ton, tes).
/// </summary>
PossessiveSecondPerson = 4,
/// <summary>
/// Possessive article (son, ses).
/// </summary>
PossessiveThirdPerson = 5,
}

View file

@ -0,0 +1,37 @@
namespace Dalamud.Game.Text.Noun.Enums;
/// <summary>
/// Article types for <see cref="ClientLanguage.German"/>.
/// </summary>
public enum GermanArticleType
{
/// <summary>
/// Unbestimmter Artikel (ein, eine, etc.).
/// </summary>
Indefinite = 1,
/// <summary>
/// Bestimmter Artikel (der, die, das, etc.).
/// </summary>
Definite = 2,
/// <summary>
/// Possessivartikel "dein" (dein, deine, etc.).
/// </summary>
Possessive = 3,
/// <summary>
/// Negativartikel "kein" (kein, keine, etc.).
/// </summary>
Negative = 4,
/// <summary>
/// Nullartikel.
/// </summary>
ZeroArticle = 5,
/// <summary>
/// Demonstrativpronomen "dieser" (dieser, diese, etc.).
/// </summary>
Demonstrative = 6,
}

View file

@ -0,0 +1,17 @@
namespace Dalamud.Game.Text.Noun.Enums;
/// <summary>
/// Article types for <see cref="ClientLanguage.Japanese"/>.
/// </summary>
public enum JapaneseArticleType
{
/// <summary>
/// Near listener (それら).
/// </summary>
NearListener = 1,
/// <summary>
/// Distant from both speaker and listener (あれら).
/// </summary>
Distant = 2,
}

View file

@ -0,0 +1,73 @@
using Dalamud.Game.Text.Noun.Enums;
using Lumina.Text.ReadOnly;
using LSheets = Lumina.Excel.Sheets;
namespace Dalamud.Game.Text.Noun;
/// <summary>
/// Parameters for noun processing.
/// </summary>
internal record struct NounParams()
{
/// <summary>
/// The language of the sheet to be processed.
/// </summary>
public required ClientLanguage Language;
/// <summary>
/// The name of the sheet containing the row to process.
/// </summary>
public required string SheetName = string.Empty;
/// <summary>
/// The row id within the sheet to process.
/// </summary>
public required uint RowId;
/// <summary>
/// The quantity of the entity (default is 1). Used to determine grammatical number (e.g., singular or plural).
/// </summary>
public int Quantity = 1;
/// <summary>
/// The article type.
/// </summary>
/// <remarks>
/// Depending on the <see cref="Language"/>, this has different meanings.<br/>
/// See <see cref="JapaneseArticleType"/>, <see cref="GermanArticleType"/>, <see cref="FrenchArticleType"/>, <see cref="EnglishArticleType"/>.
/// </remarks>
public int ArticleType = 1;
/// <summary>
/// The grammatical case (e.g., Nominative, Genitive, Dative, Accusative) used for German texts (default is 0).
/// </summary>
public int GrammaticalCase = 0;
/// <summary>
/// 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 "//").
/// </summary>
public ReadOnlySeString LinkMarker = default;
/// <summary>
/// An indicator that this noun will be processed from an Action sheet. Only used for German texts.
/// </summary>
public bool IsActionSheet;
/// <summary>
/// Gets the column offset.
/// </summary>
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,
};
}

View file

@ -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] = ?
*/
/// <summary>
/// Provides functionality to process texts from sheets containing grammatical placeholders.
/// </summary>
[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<DataManager>.Get();
[ServiceManager.ServiceDependency]
private readonly DalamudConfiguration dalamudConfiguration = Service<DalamudConfiguration>.Get();
private readonly ConcurrentDictionary<NounParams, ReadOnlySeString> cache = [];
[ServiceManager.ServiceConstructor]
private NounProcessor()
{
}
/// <summary>
/// Processes a specific row from a sheet and generates a formatted string based on grammatical and language-specific rules.
/// </summary>
/// <param name="nounParams">Parameters for processing.</param>
/// <returns>A ReadOnlySeString representing the processed text.</returns>
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;
}
/// <summary>
/// Resolves noun placeholders in Japanese text.
/// </summary>
/// <param name="nounParams">Parameters for processing.</param>
/// <returns>A ReadOnlySeString representing the processed text.</returns>
/// <remarks>
/// This is a C# implementation of Component::Text::Localize::NounJa.Resolve.
/// </remarks>
private ReadOnlySeString ResolveNounJa(NounParams nounParams)
{
var sheet = this.dataManager.Excel.GetSheet<RawRow>(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<RawRow>(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;
}
/// <summary>
/// Resolves noun placeholders in English text.
/// </summary>
/// <param name="nounParams">Parameters for processing.</param>
/// <returns>A ReadOnlySeString representing the processed text.</returns>
/// <remarks>
/// This is a C# implementation of Component::Text::Localize::NounEn.Resolve.
/// </remarks>
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<RawRow>(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<RawRow>(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;
}
/// <summary>
/// Resolves noun placeholders in German text.
/// </summary>
/// <param name="nounParams">Parameters for processing.</param>
/// <returns>A ReadOnlySeString representing the processed text.</returns>
/// <remarks>
/// This is a C# implementation of Component::Text::Localize::NounDe.Resolve.
/// </remarks>
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<RawRow>(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<RawRow>(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;
}
/// <summary>
/// Resolves noun placeholders in French text.
/// </summary>
/// <param name="nounParams">Parameters for processing.</param>
/// <returns>A ReadOnlySeString representing the processed text.</returns>
/// <remarks>
/// This is a C# implementation of Component::Text::Localize::NounFr.Resolve.
/// </remarks>
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<RawRow>(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<RawRow>(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;
}
}

View file

@ -4,6 +4,8 @@ using System.Linq;
using System.Text; using System.Text;
using Dalamud.Data; using Dalamud.Data;
using Dalamud.Utility;
using Lumina.Excel; using Lumina.Excel;
using Lumina.Excel.Sheets; using Lumina.Excel.Sheets;
using Newtonsoft.Json; using Newtonsoft.Json;
@ -73,6 +75,7 @@ public class ItemPayload : Payload
/// <summary> /// <summary>
/// Kinds of items that can be fetched from this payload. /// Kinds of items that can be fetched from this payload.
/// </summary> /// </summary>
[Api12ToDo("Move this out of ItemPayload. It's used in other classes too.")]
public enum ItemKind : uint public enum ItemKind : uint
{ {
/// <summary> /// <summary>
@ -121,7 +124,7 @@ public class ItemPayload : Payload
/// Gets the actual item ID of this payload. /// Gets the actual item ID of this payload.
/// </summary> /// </summary>
[JsonIgnore] [JsonIgnore]
public uint ItemId => GetAdjustedId(this.rawItemId).ItemId; public uint ItemId => ItemUtil.GetBaseId(this.rawItemId).ItemId;
/// <summary> /// <summary>
/// Gets the raw, unadjusted item ID of this payload. /// Gets the raw, unadjusted item ID of this payload.
@ -161,7 +164,7 @@ public class ItemPayload : Payload
/// <returns>The created item payload.</returns> /// <returns>The created item payload.</returns>
public static ItemPayload FromRaw(uint rawItemId, string? displayNameOverride = null) 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); return new ItemPayload(id, kind, displayNameOverride);
} }
@ -230,7 +233,7 @@ public class ItemPayload : Payload
protected override void DecodeImpl(BinaryReader reader, long endOfStream) protected override void DecodeImpl(BinaryReader reader, long endOfStream)
{ {
this.rawItemId = GetInteger(reader); this.rawItemId = GetInteger(reader);
this.Kind = GetAdjustedId(this.rawItemId).Kind; this.Kind = ItemUtil.GetBaseId(this.rawItemId).Kind;
if (reader.BaseStream.Position + 3 < endOfStream) if (reader.BaseStream.Position + 3 < endOfStream)
{ {
@ -255,15 +258,4 @@ public class ItemPayload : Payload
this.displayName = Encoding.UTF8.GetString(itemNameBytes); 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),
};
}
} }

View file

@ -5,11 +5,16 @@ using System.Runtime.InteropServices;
using System.Text; using System.Text;
using Dalamud.Data; using Dalamud.Data;
using Dalamud.Game.Text.Evaluator;
using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Utility; using Dalamud.Utility;
using Lumina.Excel.Sheets; using Lumina.Excel.Sheets;
using Newtonsoft.Json; using Newtonsoft.Json;
using LSeStringBuilder = Lumina.Text.SeStringBuilder;
namespace Dalamud.Game.Text.SeStringHandling; namespace Dalamud.Game.Text.SeStringHandling;
/// <summary> /// <summary>
@ -187,57 +192,32 @@ public class SeString
/// <returns>An SeString containing all the payloads necessary to display an item link in the chat log.</returns> /// <returns>An SeString containing all the payloads necessary to display an item link in the chat log.</returns>
public static SeString CreateItemLink(uint itemId, ItemPayload.ItemKind kind = ItemPayload.ItemKind.Normal, string? displayNameOverride = null) public static SeString CreateItemLink(uint itemId, ItemPayload.ItemKind kind = ItemPayload.ItemKind.Normal, string? displayNameOverride = null)
{ {
var data = Service<DataManager>.Get(); var clientState = Service<ClientState.ClientState>.Get();
var seStringEvaluator = Service<SeStringEvaluator>.Get();
var displayName = displayNameOverride; var rawId = ItemUtil.GetRawId(itemId, kind);
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<Item>()?.GetRowOrDefault(itemId);
displayName = item?.Name.ExtractText();
rarity = item?.Rarity ?? 1;
break;
case ItemPayload.ItemKind.EventItem:
displayName = data.GetExcelSheet<EventItem>()?.GetRowOrDefault(itemId)?.Name.ExtractText();
break;
default:
throw new ArgumentOutOfRangeException(nameof(kind), kind, null);
}
}
if (displayName == null) var displayName = displayNameOverride ?? ItemUtil.GetItemName(rawId);
{ if (displayName.IsEmpty)
throw new Exception("Invalid item ID specified, could not determine item name."); throw new Exception("Invalid item ID specified, could not determine item name.");
}
if (kind == ItemPayload.ItemKind.Hq) var copyName = ItemUtil.GetItemName(rawId, false).ExtractText();
{ var textColor = ItemUtil.GetItemRarityColorType(rawId);
displayName += $" {(char)SeIconChar.HighQuality}"; var textEdgeColor = textColor + 1u;
}
else if (kind == ItemPayload.ItemKind.Collectible)
{
displayName += $" {(char)SeIconChar.Collectible}";
}
var textColor = (ushort)(549 + ((rarity - 1) * 2)); var sb = LSeStringBuilder.SharedPool.Get();
var textGlowColor = (ushort)(textColor + 1); 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 SeString.Parse(seStringEvaluator.EvaluateFromAddon(371, [itemLink], clientState.ClientLanguage));
return new SeStringBuilder()
.AddUiForeground(textColor)
.AddUiGlow(textGlowColor)
.Add(new ItemPayload(itemId, kind))
.Append(TextArrowPayloads)
.AddText(displayName)
.AddUiGlowOff()
.AddUiForegroundOff()
.Add(RawPayload.LinkTerminator)
.Build();
} }
/// <summary> /// <summary>
@ -301,7 +281,7 @@ public class SeString
public static SeString CreateMapLink( public static SeString CreateMapLink(
uint territoryId, uint mapId, float xCoord, float yCoord, float fudgeFactor = 0.05f) => uint territoryId, uint mapId, float xCoord, float yCoord, float fudgeFactor = 0.05f) =>
CreateMapLinkWithInstance(territoryId, mapId, null, xCoord, yCoord, fudgeFactor); CreateMapLinkWithInstance(territoryId, mapId, null, xCoord, yCoord, fudgeFactor);
/// <summary> /// <summary>
/// Creates an SeString representing an entire Payload chain that can be used to link a map position in the chat log. /// Creates an SeString representing an entire Payload chain that can be used to link a map position in the chat log.
/// </summary> /// </summary>
@ -340,7 +320,7 @@ public class SeString
/// <returns>An SeString containing all of the payloads necessary to display a map link in the chat log.</returns> /// <returns>An SeString containing all of the payloads necessary to display a map link in the chat log.</returns>
public static SeString? CreateMapLink(string placeName, float xCoord, float yCoord, float fudgeFactor = 0.05f) => public static SeString? CreateMapLink(string placeName, float xCoord, float yCoord, float fudgeFactor = 0.05f) =>
CreateMapLinkWithInstance(placeName, null, xCoord, yCoord, fudgeFactor); CreateMapLinkWithInstance(placeName, null, xCoord, yCoord, fudgeFactor);
/// <summary> /// <summary>
/// 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. /// 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. /// Returns null if no corresponding PlaceName was found.
@ -511,7 +491,7 @@ public class SeString
{ {
messageBytes.AddRange(p.Encode()); messageBytes.AddRange(p.Encode());
} }
// Add Null Terminator // Add Null Terminator
messageBytes.Add(0); messageBytes.Add(0);
@ -526,7 +506,7 @@ public class SeString
{ {
return this.TextValue; return this.TextValue;
} }
private static string GetMapLinkNameString(string placeName, int? instance, string coordinateString) private static string GetMapLinkNameString(string placeName, int? instance, string coordinateString)
{ {
var instanceString = string.Empty; var instanceString = string.Empty;
@ -534,7 +514,7 @@ public class SeString
{ {
instanceString = (SeIconChar.Instance1 + instance.Value - 1).ToIconString(); instanceString = (SeIconChar.Instance1 + instance.Value - 1).ToIconString();
} }
return $"{placeName}{instanceString} {coordinateString}"; return $"{placeName}{instanceString} {coordinateString}";
} }
} }

View file

@ -7,7 +7,6 @@ using BitFaster.Caching.Lru;
using Dalamud.Data; using Dalamud.Data;
using Dalamud.Game; using Dalamud.Game;
using Dalamud.Game.Config;
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Interface.ImGuiSeStringRenderer.Internal.TextProcessing; using Dalamud.Interface.ImGuiSeStringRenderer.Internal.TextProcessing;
using Dalamud.Interface.Utility; using Dalamud.Interface.Utility;
@ -44,9 +43,6 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
/// of this placeholder. On its own, usually displayed like <c>[OBJ]</c>.</summary> /// of this placeholder. On its own, usually displayed like <c>[OBJ]</c>.</summary>
private const int ObjectReplacementCharacter = '\uFFFC'; private const int ObjectReplacementCharacter = '\uFFFC';
[ServiceManager.ServiceDependency]
private readonly GameConfig gameConfig = Service<GameConfig>.Get();
/// <summary>Cache of compiled SeStrings from <see cref="CompileAndCache"/>.</summary> /// <summary>Cache of compiled SeStrings from <see cref="CompileAndCache"/>.</summary>
private readonly ConcurrentLru<string, ReadOnlySeString> cache = new(1024); private readonly ConcurrentLru<string, ReadOnlySeString> cache = new(1024);
@ -570,70 +566,16 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
// Apply gamepad key mapping to icons. // Apply gamepad key mapping to icons.
case MacroCode.Icon2 case MacroCode.Icon2
when payload.TryGetExpression(out var icon) && icon.TryGetInt(out var iconId): 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, if (iconMapping[i].IconId == iconId)
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
{ {
ControllerAnalogLeftStick => ControllerAnalogLeftStick, return (BitmapFontIcon)iconMapping[i].RemappedIconId;
ControllerAnalogLeftStickIn => ControllerAnalogLeftStickIn, }
ControllerAnalogLeftStickUpDown => ControllerAnalogLeftStickUpDown, }
ControllerAnalogLeftStickLeftRight => ControllerAnalogLeftStickLeftRight,
ControllerAnalogRightStick => ControllerAnalogLeftStick, return (BitmapFontIcon)iconId;
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 None; return None;

View file

@ -47,11 +47,13 @@ internal class DataWindow : Window, IDisposable
new KeyStateWidget(), new KeyStateWidget(),
new MarketBoardWidget(), new MarketBoardWidget(),
new NetworkMonitorWidget(), new NetworkMonitorWidget(),
new NounProcessorWidget(),
new ObjectTableWidget(), new ObjectTableWidget(),
new PartyListWidget(), new PartyListWidget(),
new PluginIpcWidget(), new PluginIpcWidget(),
new SeFontTestWidget(), new SeFontTestWidget(),
new ServicesWidget(), new ServicesWidget(),
new SeStringCreatorWidget(),
new SeStringRendererTestWidget(), new SeStringRendererTestWidget(),
new StartInfoWidget(), new StartInfoWidget(),
new TargetWidget(), new TargetWidget(),
@ -68,6 +70,7 @@ internal class DataWindow : Window, IDisposable
private bool isExcept; private bool isExcept;
private bool selectionCollapsed; private bool selectionCollapsed;
private IDataWindowWidget currentWidget; private IDataWindowWidget currentWidget;
private bool isLoaded;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="DataWindow"/> class. /// Initializes a new instance of the <see cref="DataWindow"/> class.
@ -81,8 +84,6 @@ internal class DataWindow : Window, IDisposable
this.RespectCloseHotkey = false; this.RespectCloseHotkey = false;
this.orderedModules = this.modules.OrderBy(module => module.DisplayName); this.orderedModules = this.modules.OrderBy(module => module.DisplayName);
this.currentWidget = this.orderedModules.First(); this.currentWidget = this.orderedModules.First();
this.Load();
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -91,6 +92,7 @@ internal class DataWindow : Window, IDisposable
/// <inheritdoc/> /// <inheritdoc/>
public override void OnOpen() public override void OnOpen()
{ {
this.Load();
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -183,6 +185,7 @@ internal class DataWindow : Window, IDisposable
if (ImGuiComponents.IconButton("forceReload", FontAwesomeIcon.Sync)) if (ImGuiComponents.IconButton("forceReload", FontAwesomeIcon.Sync))
{ {
this.isLoaded = false;
this.Load(); this.Load();
} }
@ -236,6 +239,11 @@ internal class DataWindow : Window, IDisposable
private void Load() private void Load()
{ {
if (this.isLoaded)
return;
this.isLoaded = true;
foreach (var widget in this.modules) foreach (var widget in this.modules)
{ {
widget.Load(); widget.Load();

View file

@ -0,0 +1,34 @@
using Dalamud.Interface.Utility;
using ImGuiNET;
namespace Dalamud.Interface.Internal.Windows.Data;
/// <summary>
/// Common utilities used in Widgets.
/// </summary>
internal class WidgetUtil
{
/// <summary>
/// Draws text that can be copied on click.
/// </summary>
/// <param name="text">The text shown and to be copied.</param>
/// <param name="tooltipText">The text in the tooltip.</param>
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);
}
}
}

View file

@ -57,24 +57,6 @@ internal unsafe class AtkArrayDataBrowserWidget : IDataWindowWidget
this.DrawExtendArrayTab(); 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) 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)); 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.SameLine();
ImGui.TextUnformatted("Address: "); ImGui.TextUnformatted("Address: ");
ImGui.SameLine(0, 0); ImGui.SameLine(0, 0);
DrawCopyableText($"0x{(nint)array:X}", "Copy address"); WidgetUtil.DrawCopyableText($"0x{(nint)array:X}", "Copy address");
if (array->SubscribedAddonsCount > 0) if (array->SubscribedAddonsCount > 0)
{ {
@ -238,22 +220,22 @@ internal unsafe class AtkArrayDataBrowserWidget : IDataWindowWidget
var ptr = &array->IntArray[i]; var ptr = &array->IntArray[i];
ImGui.TableNextColumn(); // Address ImGui.TableNextColumn(); // Address
DrawCopyableText($"0x{(nint)ptr:X}", "Copy entry address"); WidgetUtil.DrawCopyableText($"0x{(nint)ptr:X}", "Copy entry address");
ImGui.TableNextColumn(); // Integer ImGui.TableNextColumn(); // Integer
DrawCopyableText((*ptr).ToString(), "Copy value"); WidgetUtil.DrawCopyableText((*ptr).ToString(), "Copy value");
ImGui.TableNextColumn(); // Short ImGui.TableNextColumn(); // Short
DrawCopyableText((*(short*)ptr).ToString(), "Copy as short"); WidgetUtil.DrawCopyableText((*(short*)ptr).ToString(), "Copy as short");
ImGui.TableNextColumn(); // Byte ImGui.TableNextColumn(); // Byte
DrawCopyableText((*(byte*)ptr).ToString(), "Copy as byte"); WidgetUtil.DrawCopyableText((*(byte*)ptr).ToString(), "Copy as byte");
ImGui.TableNextColumn(); // Float ImGui.TableNextColumn(); // Float
DrawCopyableText((*(float*)ptr).ToString(), "Copy as float"); WidgetUtil.DrawCopyableText((*(float*)ptr).ToString(), "Copy as float");
ImGui.TableNextColumn(); // Hex 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 (this.showTextAddress)
{ {
if (!isNull) if (!isNull)
DrawCopyableText($"0x{(nint)array->StringArray[i]:X}", "Copy text address"); WidgetUtil.DrawCopyableText($"0x{(nint)array->StringArray[i]:X}", "Copy text address");
} }
else 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 ImGui.TableNextColumn(); // Managed
@ -351,7 +333,7 @@ internal unsafe class AtkArrayDataBrowserWidget : IDataWindowWidget
{ {
if (this.showMacroString) if (this.showMacroString)
{ {
DrawCopyableText(new ReadOnlySeStringSpan(array->StringArray[i]).ToString(), "Copy text"); WidgetUtil.DrawCopyableText(new ReadOnlySeStringSpan(array->StringArray[i]).ToString(), "Copy text");
} }
else else
{ {
@ -408,11 +390,11 @@ internal unsafe class AtkArrayDataBrowserWidget : IDataWindowWidget
ImGui.TextUnformatted($"#{i}"); ImGui.TextUnformatted($"#{i}");
ImGui.TableNextColumn(); // Address 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 ImGui.TableNextColumn(); // Pointer
if (!isNull) if (!isNull)
DrawCopyableText($"0x{(nint)array->DataArray[i]:X}", "Copy address"); WidgetUtil.DrawCopyableText($"0x{(nint)array->DataArray[i]:X}", "Copy address");
} }
} }
} }

View file

@ -69,10 +69,10 @@ internal class FateTableWidget : IDataWindowWidget
ImGui.TextUnformatted($"#{i}"); ImGui.TextUnformatted($"#{i}");
ImGui.TableNextColumn(); // Address 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 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.TableNextColumn(); // State
ImGui.TextUnformatted(fate.State.ToString()); ImGui.TextUnformatted(fate.State.ToString());
@ -140,7 +140,7 @@ internal class FateTableWidget : IDataWindowWidget
ImGui.TableNextColumn(); // Name ImGui.TableNextColumn(); // Name
DrawCopyableText(fate.Name.ToString(), "Click to copy Name"); WidgetUtil.DrawCopyableText(fate.Name.ToString(), "Click to copy Name");
ImGui.TableNextColumn(); // Progress ImGui.TableNextColumn(); // Progress
ImGui.TextUnformatted($"{fate.Progress}%"); ImGui.TextUnformatted($"{fate.Progress}%");
@ -156,28 +156,10 @@ internal class FateTableWidget : IDataWindowWidget
ImGui.TextUnformatted(fate.HasBonus.ToString()); ImGui.TextUnformatted(fate.HasBonus.ToString());
ImGui.TableNextColumn(); // Position ImGui.TableNextColumn(); // Position
DrawCopyableText(fate.Position.ToString(), "Click to copy Position"); WidgetUtil.DrawCopyableText(fate.Position.ToString(), "Click to copy Position");
ImGui.TableNextColumn(); // Radius ImGui.TableNextColumn(); // Radius
DrawCopyableText(fate.Radius.ToString(), "Click to copy Radius"); WidgetUtil.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);
} }
} }
} }

View file

@ -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;
/// <summary>
/// Widget for the NounProcessor service.
/// </summary>
internal class NounProcessorWidget : IDataWindowWidget
{
/// <summary>A list of German grammatical cases.</summary>
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;
/// <inheritdoc/>
public string[]? CommandShortcuts { get; init; } = { "noun" };
/// <inheritdoc/>
public string DisplayName { get; init; } = "Noun Processor";
/// <inheritdoc/>
public bool Ready { get; set; }
/// <inheritdoc/>
public void Load()
{
this.languages = Enum.GetValues<ClientLanguage>();
this.languageNames = Enum.GetNames<ClientLanguage>();
this.selectedLanguageIndex = (int)Service<ClientState>.Get().ClientLanguage;
this.Ready = true;
}
/// <inheritdoc/>
public void Draw()
{
var nounProcessor = Service<NounProcessor>.Get();
var dataManager = Service<DataManager>.Get();
var clientState = Service<ClientState>.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<RawRow>(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());
}
}
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -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; namespace Dalamud.Interface.Internal.Windows.SelfTest.AgingSteps;
@ -17,11 +22,34 @@ internal class GamepadStateAgingStep : IAgingStep
{ {
var gamepadState = Service<GamepadState>.Get(); var gamepadState = Service<GamepadState>.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 var builder = LSeStringBuilder.SharedPool.Get();
&& gamepadState.Raw(GamepadButtons.East) == 1
&& gamepadState.Raw(GamepadButtons.L1) == 1) 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; return SelfTestStepResult.Pass;
} }

View file

@ -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;
/// <summary>
/// Test setup for NounProcessor.
/// </summary>
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,
}
/// <inheritdoc/>
public string Name => "Test NounProcessor";
/// <inheritdoc/>
public unsafe SelfTestStepResult RunStep()
{
var nounProcessor = Service<NounProcessor>.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;
}
/// <inheritdoc/>
public void CleanUp()
{
// ignored
}
private record struct NounTestEntry(
string SheetName,
uint RowId,
ClientLanguage Language,
int Quantity,
int ArticleType,
int GrammaticalCase,
string ExpectedResult);
}

View file

@ -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;
/// <summary>
/// Test setup for SeStringEvaluator.
/// </summary>
internal class SeStringEvaluatorAgingStep : IAgingStep
{
private int step = 0;
/// <inheritdoc/>
public string Name => "Test SeStringEvaluator";
/// <inheritdoc/>
public SelfTestStepResult RunStep()
{
var seStringEvaluator = Service<SeStringEvaluator>.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<ClientState>.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("<pcname(lnum1)>"), [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;
}
/// <inheritdoc/>
public void CleanUp()
{
// ignored
this.step = 0;
}
}

View file

@ -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;
/// <summary>
/// Test setup for SheetRedirectResolver.
/// </summary>
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);
/// <inheritdoc/>
public string Name => "Test SheetRedirectResolver";
/// <inheritdoc/>
public unsafe SelfTestStepResult RunStep()
{
// Client::UI::Misc::RaptureTextModule_ResolveSheetRedirect
if (!Service<TargetSigScanner>.Get().TryScanText("E8 ?? ?? ?? ?? 44 8B E8 A8 10", out var addr))
return SelfTestStepResult.Fail;
var sheetRedirectResolver = Service<SheetRedirectResolver>.Get();
var resolveSheetRedirect = Marshal.GetDelegateForFunctionPointer<ResolveSheetRedirect>(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);
}
}
/// <inheritdoc/>
public void CleanUp()
{
// ignored
}
private record struct RedirectEntry(string SheetName, uint RowId, SheetRedirectFlags Flags = SheetRedirectFlags.None);
}

View file

@ -50,6 +50,9 @@ internal class SelfTestWindow : Window
new DutyStateAgingStep(), new DutyStateAgingStep(),
new GameConfigAgingStep(), new GameConfigAgingStep(),
new MarketBoardAgingStep(), new MarketBoardAgingStep(),
new SheetRedirectResolverAgingStep(),
new NounProcessorAgingStep(),
new SeStringEvaluatorAgingStep(),
new LogoutEventAgingStep(), new LogoutEventAgingStep(),
}; };

View file

@ -210,8 +210,6 @@ public static class ImGuiHelpers
/// <param name="imGuiId">ImGui ID, if link functionality is desired.</param> /// <param name="imGuiId">ImGui ID, if link functionality is desired.</param>
/// <param name="buttonFlags">Button flags to use on link interaction.</param> /// <param name="buttonFlags">Button flags to use on link interaction.</param>
/// <returns>Interaction result of the rendered text.</returns> /// <returns>Interaction result of the rendered text.</returns>
/// <remarks>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.</remarks>
public static SeStringDrawResult SeStringWrapped( public static SeStringDrawResult SeStringWrapped(
ReadOnlySpan<byte> sss, ReadOnlySpan<byte> sss,
scoped in SeStringDrawParams style = default, scoped in SeStringDrawParams style = default,
@ -226,8 +224,6 @@ public static class ImGuiHelpers
/// <param name="imGuiId">ImGui ID, if link functionality is desired.</param> /// <param name="imGuiId">ImGui ID, if link functionality is desired.</param>
/// <param name="buttonFlags">Button flags to use on link interaction.</param> /// <param name="buttonFlags">Button flags to use on link interaction.</param>
/// <returns>Interaction result of the rendered text.</returns> /// <returns>Interaction result of the rendered text.</returns>
/// <remarks>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.</remarks>
public static SeStringDrawResult CompileSeStringWrapped( public static SeStringDrawResult CompileSeStringWrapped(
string text, string text,
scoped in SeStringDrawParams style = default, scoped in SeStringDrawParams style = default,

View file

@ -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;
/// <summary>
/// Defines a service for retrieving localized text for various in-game entities.
/// </summary>
[Experimental("SeStringEvaluator")]
public interface ISeStringEvaluator
{
/// <summary>
/// Evaluates macros in a <see cref="ReadOnlySeString"/>.
/// </summary>
/// <param name="str">The string containing macros.</param>
/// <param name="localParameters">An optional list of local parameters.</param>
/// <param name="language">An optional language override.</param>
/// <returns>An evaluated <see cref="ReadOnlySeString"/>.</returns>
ReadOnlySeString Evaluate(ReadOnlySeString str, Span<SeStringParameter> localParameters = default, ClientLanguage? language = null);
/// <summary>
/// Evaluates macros in a <see cref="ReadOnlySeStringSpan"/>.
/// </summary>
/// <param name="str">The string containing macros.</param>
/// <param name="localParameters">An optional list of local parameters.</param>
/// <param name="language">An optional language override.</param>
/// <returns>An evaluated <see cref="ReadOnlySeString"/>.</returns>
ReadOnlySeString Evaluate(ReadOnlySeStringSpan str, Span<SeStringParameter> localParameters = default, ClientLanguage? language = null);
/// <summary>
/// Evaluates macros in text from the Addon sheet.
/// </summary>
/// <param name="addonId">The row id of the Addon sheet.</param>
/// <param name="localParameters">An optional list of local parameters.</param>
/// <param name="language">An optional language override.</param>
/// <returns>An evaluated <see cref="ReadOnlySeString"/>.</returns>
ReadOnlySeString EvaluateFromAddon(uint addonId, Span<SeStringParameter> localParameters = default, ClientLanguage? language = null);
/// <summary>
/// Evaluates macros in text from the Lobby sheet.
/// </summary>
/// <param name="lobbyId">The row id of the Lobby sheet.</param>
/// <param name="localParameters">An optional list of local parameters.</param>
/// <param name="language">An optional language override.</param>
/// <returns>An evaluated <see cref="ReadOnlySeString"/>.</returns>
ReadOnlySeString EvaluateFromLobby(uint lobbyId, Span<SeStringParameter> localParameters = default, ClientLanguage? language = null);
/// <summary>
/// Evaluates macros in text from the LogMessage sheet.
/// </summary>
/// <param name="logMessageId">The row id of the LogMessage sheet.</param>
/// <param name="localParameters">An optional list of local parameters.</param>
/// <param name="language">An optional language override.</param>
/// <returns>An evaluated <see cref="ReadOnlySeString"/>.</returns>
ReadOnlySeString EvaluateFromLogMessage(uint logMessageId, Span<SeStringParameter> localParameters = default, ClientLanguage? language = null);
/// <summary>
/// Evaluates ActStr from the given ActionKind and id.
/// </summary>
/// <param name="actionKind">The ActionKind.</param>
/// <param name="id">The action id.</param>
/// <param name="language">An optional language override.</param>
/// <returns>The name of the action.</returns>
string EvaluateActStr(ActionKind actionKind, uint id, ClientLanguage? language = null);
/// <summary>
/// Evaluates ObjStr from the given ObjectKind and id.
/// </summary>
/// <param name="objectKind">The ObjectKind.</param>
/// <param name="id">The object id.</param>
/// <param name="language">An optional language override.</param>
/// <returns>The singular name of the object.</returns>
string EvaluateObjStr(ObjectKind objectKind, uint id, ClientLanguage? language = null);
}

View file

@ -0,0 +1,26 @@
using Dalamud.Game;
namespace Dalamud.Utility;
/// <summary>
/// Extension methods for the <see cref="ActionKind"/> enum.
/// </summary>
public static class ActionKindExtensions
{
/// <summary>
/// Converts the id of an ActionKind to the id used in the ActStr sheet redirect.
/// </summary>
/// <param name="actionKind">The ActionKind this id is for.</param>
/// <param name="id">The id.</param>
/// <returns>An id that can be used in the ActStr sheet redirect.</returns>
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;
}
}

View file

@ -23,4 +23,40 @@ public static class ClientLanguageExtensions
_ => throw new ArgumentOutOfRangeException(nameof(language)), _ => throw new ArgumentOutOfRangeException(nameof(language)),
}; };
} }
/// <summary>
/// Gets the language code from a ClientLanguage.
/// </summary>
/// <param name="value">The ClientLanguage to convert.</param>
/// <returns>The language code (ja, en, de, fr).</returns>
/// <exception cref="ArgumentOutOfRangeException">An exception that is thrown when no valid ClientLanguage was given.</exception>
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)),
};
}
/// <summary>
/// Gets the ClientLanguage from a language code.
/// </summary>
/// <param name="value">The language code to convert (ja, en, de, fr).</param>
/// <returns>The ClientLanguage.</returns>
/// <exception cref="ArgumentOutOfRangeException">An exception that is thrown when no valid language code was given.</exception>
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)),
};
}
} }

159
Dalamud/Utility/ItemUtil.cs Normal file
View file

@ -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;
/// <summary>
/// Utilities related to Items.
/// </summary>
internal static class ItemUtil
{
private static int? eventItemRowCount;
/// <summary>Converts raw item ID to item ID with its classification.</summary>
/// <param name="rawItemId">Raw item ID.</param>
/// <returns>Item ID and its classification.</returns>
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);
}
/// <summary>Converts item ID with its classification to raw item ID.</summary>
/// <param name="itemId">Item ID.</param>
/// <param name="kind">Item classification.</param>
/// <returns>Raw Item ID.</returns>
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,
};
}
/// <summary>
/// Checks if the item id belongs to a normal item.
/// </summary>
/// <param name="itemId">The item id to check.</param>
/// <returns><c>true</c> when the item id belongs to a normal item.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static bool IsNormalItem(uint itemId)
{
return itemId < 500_000;
}
/// <summary>
/// Checks if the item id belongs to a collectible item.
/// </summary>
/// <param name="itemId">The item id to check.</param>
/// <returns><c>true</c> when the item id belongs to a collectible item.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static bool IsCollectible(uint itemId)
{
return itemId is >= 500_000 and < 1_000_000;
}
/// <summary>
/// Checks if the item id belongs to a high quality item.
/// </summary>
/// <param name="itemId">The item id to check.</param>
/// <returns><c>true</c> when the item id belongs to a high quality item.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static bool IsHighQuality(uint itemId)
{
return itemId is >= 1_000_000 and < 2_000_000;
}
/// <summary>
/// Checks if the item id belongs to an event item.
/// </summary>
/// <param name="itemId">The item id to check.</param>
/// <returns><c>true</c> when the item id belongs to an event item.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static bool IsEventItem(uint itemId)
{
return itemId >= 2_000_000 && itemId - 2_000_000 < (eventItemRowCount ??= Service<DataManager>.Get().GetExcelSheet<EventItem>().Count);
}
/// <summary>
/// Gets the name of an item.
/// </summary>
/// <param name="itemId">The raw item id.</param>
/// <param name="includeIcon">Whether to include the High Quality or Collectible icon.</param>
/// <param name="language">An optional client language override.</param>
/// <returns>The item name.</returns>
internal static ReadOnlySeString GetItemName(uint itemId, bool includeIcon = true, ClientLanguage? language = null)
{
var dataManager = Service<DataManager>.Get();
if (IsEventItem(itemId))
{
return dataManager
.GetExcelSheet<EventItem>(language)
.TryGetRow(itemId, out var eventItem)
? eventItem.Name
: default;
}
var (baseId, kind) = GetBaseId(itemId);
if (!dataManager
.GetExcelSheet<Item>(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;
}
/// <summary>
/// Gets the color row id for an item name.
/// </summary>
/// <param name="itemId">The raw item Id.</param>
/// <param name="isEdgeColor">Wheather this color is used as edge color.</param>
/// <returns>The Color row id.</returns>
internal static uint GetItemRarityColorType(uint itemId, bool isEdgeColor = false)
{
var rarity = 1u;
if (!IsEventItem(itemId) && Service<DataManager>.Get().GetExcelSheet<Item>().TryGetRow(GetBaseId(itemId).ItemId, out var item))
rarity = item.Rarity;
return (isEdgeColor ? 548u : 547u) + (rarity * 2u);
}
}

View file

@ -0,0 +1,33 @@
using Dalamud.Game.ClientState.Objects.Enums;
namespace Dalamud.Utility;
/// <summary>
/// Extension methods for the <see cref="ObjectKind"/> enum.
/// </summary>
public static class ObjectKindExtensions
{
/// <summary>
/// Converts the id of an ObjectKind to the id used in the ObjStr sheet redirect.
/// </summary>
/// <param name="objectKind">The ObjectKind this id is for.</param>
/// <param name="id">The id.</param>
/// <returns>An id that can be used in the ObjStr sheet redirect.</returns>
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,
};
}
}

View file

@ -1,3 +1,5 @@
using System.Linq;
using Lumina.Text.Parse; using Lumina.Text.Parse;
using Lumina.Text.ReadOnly; using Lumina.Text.ReadOnly;
@ -74,4 +76,154 @@ public static class SeStringExtensions
/// <param name="value">character name to validate.</param> /// <param name="value">character name to validate.</param>
/// <returns>indicator if character is name is valid.</returns> /// <returns>indicator if character is name is valid.</returns>
public static bool IsValidCharacterName(this DSeString value) => value.ToString().IsValidCharacterName(); public static bool IsValidCharacterName(this DSeString value) => value.ToString().IsValidCharacterName();
/// <summary>
/// Determines whether the <see cref="ReadOnlySeString"/> contains only text payloads.
/// </summary>
/// <param name="ross">The <see cref="ReadOnlySeString"/> to check.</param>
/// <returns><c>true</c> if the string contains only text payloads; otherwise, <c>false</c>.</returns>
public static bool IsTextOnly(this ReadOnlySeString ross)
{
return ross.AsSpan().IsTextOnly();
}
/// <summary>
/// Determines whether the <see cref="ReadOnlySeStringSpan"/> contains only text payloads.
/// </summary>
/// <param name="rosss">The <see cref="ReadOnlySeStringSpan"/> to check.</param>
/// <returns><c>true</c> if the span contains only text payloads; otherwise, <c>false</c>.</returns>
public static bool IsTextOnly(this ReadOnlySeStringSpan rosss)
{
foreach (var payload in rosss)
{
if (payload.Type != ReadOnlySePayloadType.Text)
return false;
}
return true;
}
/// <summary>
/// Determines whether the <see cref="ReadOnlySeString"/> contains the specified text.
/// </summary>
/// <param name="ross">The <see cref="ReadOnlySeString"/> to search.</param>
/// <param name="needle">The text to find.</param>
/// <returns><c>true</c> if the text is found; otherwise, <c>false</c>.</returns>
public static bool ContainsText(this ReadOnlySeString ross, ReadOnlySpan<byte> needle)
{
return ross.AsSpan().ContainsText(needle);
}
/// <summary>
/// Determines whether the <see cref="ReadOnlySeStringSpan"/> contains the specified text.
/// </summary>
/// <param name="rosss">The <see cref="ReadOnlySeStringSpan"/> to search.</param>
/// <param name="needle">The text to find.</param>
/// <returns><c>true</c> if the text is found; otherwise, <c>false</c>.</returns>
public static bool ContainsText(this ReadOnlySeStringSpan rosss, ReadOnlySpan<byte> needle)
{
foreach (var payload in rosss)
{
if (payload.Type != ReadOnlySePayloadType.Text)
continue;
if (payload.Body.IndexOf(needle) != -1)
return true;
}
return false;
}
/// <summary>
/// Determines whether the <see cref="LSeStringBuilder"/> contains the specified text.
/// </summary>
/// <param name="builder">The builder to search.</param>
/// <param name="needle">The text to find.</param>
/// <returns><c>true</c> if the text is found; otherwise, <c>false</c>.</returns>
public static bool ContainsText(this LSeStringBuilder builder, ReadOnlySpan<byte> needle)
{
return builder.ToReadOnlySeString().ContainsText(needle);
}
/// <summary>
/// Replaces occurrences of a specified text in a <see cref="ReadOnlySeString"/> with another text.
/// </summary>
/// <param name="ross">The original string.</param>
/// <param name="toFind">The text to find.</param>
/// <param name="replacement">The replacement text.</param>
/// <returns>A new <see cref="ReadOnlySeString"/> with the replacements made.</returns>
public static ReadOnlySeString ReplaceText(
this ReadOnlySeString ross,
ReadOnlySpan<byte> toFind,
ReadOnlySpan<byte> 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;
}
/// <summary>
/// Replaces occurrences of a specified text in an <see cref="LSeStringBuilder"/> with another text.
/// </summary>
/// <param name="builder">The builder to modify.</param>
/// <param name="toFind">The text to find.</param>
/// <param name="replacement">The replacement text.</param>
public static void ReplaceText(
this LSeStringBuilder builder,
ReadOnlySpan<byte> toFind,
ReadOnlySpan<byte> 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);
}
} }

View file

@ -1,4 +1,5 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Client.UI;
@ -43,4 +44,48 @@ public static class StringExtensions
if (!UIGlobals.IsValidPlayerCharacterName(value)) return false; if (!UIGlobals.IsValidPlayerCharacterName(value)) return false;
return includeLegacy || value.Length <= 21; return includeLegacy || value.Length <= 21;
} }
/// <summary>
/// Converts the first character of the string to uppercase while leaving the rest of the string unchanged.
/// </summary>
/// <param name="input">The input string.</param>
/// <param name="culture"><inheritdoc cref="string.ToLower(CultureInfo)" path="/param[@name='cultureInfo']"/></param>
/// <returns>A new string with the first character converted to uppercase.</returns>
[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)}";
/// <summary>
/// Converts the first character of the string to lowercase while leaving the rest of the string unchanged.
/// </summary>
/// <param name="input">The input string.</param>
/// <param name="culture"><inheritdoc cref="string.ToLower(CultureInfo)" path="/param[@name='cultureInfo']"/></param>
/// <returns>A new string with the first character converted to lowercase.</returns>
[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)}";
/// <summary>
/// Removes soft hyphen characters (U+00AD) from the input string.
/// </summary>
/// <param name="input">The input string to remove soft hyphen characters from.</param>
/// <returns>A string with all soft hyphens removed.</returns>
public static string StripSoftHyphen(this string input) => input.Replace("\u00AD", string.Empty);
/// <summary>
/// Truncates the given string to the specified maximum number of characters,
/// appending an ellipsis if truncation occurs.
/// </summary>
/// <param name="input">The string to truncate.</param>
/// <param name="maxChars">The maximum allowed length of the string.</param>
/// <param name="ellipses">The string to append if truncation occurs (defaults to "...").</param>
/// <returns>The truncated string, or the original string if no truncation is needed.</returns>
public static string? Truncate(this string input, int maxChars, string ellipses = "...")
{
return string.IsNullOrEmpty(input) || input.Length <= maxChars ? input : input[..maxChars] + ellipses;
}
} }