mirror of
https://github.com/goatcorp/Dalamud.git
synced 2026-02-15 04:17:43 +01:00
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:
parent
7cac19ce81
commit
fdbfdbb2cd
36 changed files with 5831 additions and 196 deletions
89
Dalamud/Game/ActionKind.cs
Normal file
89
Dalamud/Game/ActionKind.cs
Normal 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,
|
||||
}
|
||||
|
|
@ -323,7 +323,7 @@ internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui
|
|||
return ret;
|
||||
}
|
||||
|
||||
private void HandleActionHoverDetour(AgentActionDetail* hoverState, ActionKind actionKind, uint actionId, int a4, byte a5)
|
||||
private void HandleActionHoverDetour(AgentActionDetail* hoverState, FFXIVClientStructs.FFXIV.Client.UI.Agent.ActionKind actionKind, uint actionId, int a4, byte a5)
|
||||
{
|
||||
this.handleActionHoverHook.Original(hoverState, actionKind, actionId, a4, a5);
|
||||
this.HoveredAction.ActionKind = (HoverActionKind)actionKind;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
83
Dalamud/Game/Text/Evaluator/Internal/SeStringContext.cs
Normal file
83
Dalamud/Game/Text/Evaluator/Internal/SeStringContext.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
49
Dalamud/Game/Text/Evaluator/Internal/SheetRedirectFlags.cs
Normal file
49
Dalamud/Game/Text/Evaluator/Internal/SheetRedirectFlags.cs
Normal 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,
|
||||
}
|
||||
232
Dalamud/Game/Text/Evaluator/Internal/SheetRedirectResolver.cs
Normal file
232
Dalamud/Game/Text/Evaluator/Internal/SheetRedirectResolver.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
1995
Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs
Normal file
1995
Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs
Normal file
File diff suppressed because it is too large
Load diff
79
Dalamud/Game/Text/Evaluator/SeStringParameter.cs
Normal file
79
Dalamud/Game/Text/Evaluator/SeStringParameter.cs
Normal 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);
|
||||
}
|
||||
17
Dalamud/Game/Text/Noun/Enums/EnglishArticleType.cs
Normal file
17
Dalamud/Game/Text/Noun/Enums/EnglishArticleType.cs
Normal 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,
|
||||
}
|
||||
32
Dalamud/Game/Text/Noun/Enums/FrenchArticleType.cs
Normal file
32
Dalamud/Game/Text/Noun/Enums/FrenchArticleType.cs
Normal 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,
|
||||
}
|
||||
37
Dalamud/Game/Text/Noun/Enums/GermanArticleType.cs
Normal file
37
Dalamud/Game/Text/Noun/Enums/GermanArticleType.cs
Normal 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,
|
||||
}
|
||||
17
Dalamud/Game/Text/Noun/Enums/JapaneseArticleType.cs
Normal file
17
Dalamud/Game/Text/Noun/Enums/JapaneseArticleType.cs
Normal 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,
|
||||
}
|
||||
73
Dalamud/Game/Text/Noun/NounParams.cs
Normal file
73
Dalamud/Game/Text/Noun/NounParams.cs
Normal 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,
|
||||
};
|
||||
}
|
||||
461
Dalamud/Game/Text/Noun/NounProcessor.cs
Normal file
461
Dalamud/Game/Text/Noun/NounProcessor.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,8 @@ using System.Linq;
|
|||
using System.Text;
|
||||
|
||||
using Dalamud.Data;
|
||||
using Dalamud.Utility;
|
||||
|
||||
using Lumina.Excel;
|
||||
using Lumina.Excel.Sheets;
|
||||
using Newtonsoft.Json;
|
||||
|
|
@ -73,6 +75,7 @@ public class ItemPayload : Payload
|
|||
/// <summary>
|
||||
/// Kinds of items that can be fetched from this payload.
|
||||
/// </summary>
|
||||
[Api12ToDo("Move this out of ItemPayload. It's used in other classes too.")]
|
||||
public enum ItemKind : uint
|
||||
{
|
||||
/// <summary>
|
||||
|
|
@ -121,7 +124,7 @@ public class ItemPayload : Payload
|
|||
/// Gets the actual item ID of this payload.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public uint ItemId => GetAdjustedId(this.rawItemId).ItemId;
|
||||
public uint ItemId => ItemUtil.GetBaseId(this.rawItemId).ItemId;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the raw, unadjusted item ID of this payload.
|
||||
|
|
@ -161,7 +164,7 @@ public class ItemPayload : Payload
|
|||
/// <returns>The created item payload.</returns>
|
||||
public static ItemPayload FromRaw(uint rawItemId, string? displayNameOverride = null)
|
||||
{
|
||||
var (id, kind) = GetAdjustedId(rawItemId);
|
||||
var (id, kind) = ItemUtil.GetBaseId(rawItemId);
|
||||
return new ItemPayload(id, kind, displayNameOverride);
|
||||
}
|
||||
|
||||
|
|
@ -230,7 +233,7 @@ public class ItemPayload : Payload
|
|||
protected override void DecodeImpl(BinaryReader reader, long endOfStream)
|
||||
{
|
||||
this.rawItemId = GetInteger(reader);
|
||||
this.Kind = GetAdjustedId(this.rawItemId).Kind;
|
||||
this.Kind = ItemUtil.GetBaseId(this.rawItemId).Kind;
|
||||
|
||||
if (reader.BaseStream.Position + 3 < endOfStream)
|
||||
{
|
||||
|
|
@ -255,15 +258,4 @@ public class ItemPayload : Payload
|
|||
this.displayName = Encoding.UTF8.GetString(itemNameBytes);
|
||||
}
|
||||
}
|
||||
|
||||
private static (uint ItemId, ItemKind Kind) GetAdjustedId(uint rawItemId)
|
||||
{
|
||||
return rawItemId switch
|
||||
{
|
||||
> 500_000 and < 1_000_000 => (rawItemId - 500_000, ItemKind.Collectible),
|
||||
> 1_000_000 and < 2_000_000 => (rawItemId - 1_000_000, ItemKind.Hq),
|
||||
> 2_000_000 => (rawItemId, ItemKind.EventItem), // EventItem IDs are NOT adjusted
|
||||
_ => (rawItemId, ItemKind.Normal),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,11 +5,16 @@ using System.Runtime.InteropServices;
|
|||
using System.Text;
|
||||
|
||||
using Dalamud.Data;
|
||||
using Dalamud.Game.Text.Evaluator;
|
||||
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
||||
using Dalamud.Utility;
|
||||
|
||||
using Lumina.Excel.Sheets;
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
using LSeStringBuilder = Lumina.Text.SeStringBuilder;
|
||||
|
||||
namespace Dalamud.Game.Text.SeStringHandling;
|
||||
|
||||
/// <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>
|
||||
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 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);
|
||||
}
|
||||
}
|
||||
var rawId = ItemUtil.GetRawId(itemId, kind);
|
||||
|
||||
if (displayName == null)
|
||||
{
|
||||
var displayName = displayNameOverride ?? ItemUtil.GetItemName(rawId);
|
||||
if (displayName.IsEmpty)
|
||||
throw new Exception("Invalid item ID specified, could not determine item name.");
|
||||
}
|
||||
|
||||
if (kind == ItemPayload.ItemKind.Hq)
|
||||
{
|
||||
displayName += $" {(char)SeIconChar.HighQuality}";
|
||||
}
|
||||
else if (kind == ItemPayload.ItemKind.Collectible)
|
||||
{
|
||||
displayName += $" {(char)SeIconChar.Collectible}";
|
||||
}
|
||||
var copyName = ItemUtil.GetItemName(rawId, false).ExtractText();
|
||||
var textColor = ItemUtil.GetItemRarityColorType(rawId);
|
||||
var textEdgeColor = textColor + 1u;
|
||||
|
||||
var textColor = (ushort)(549 + ((rarity - 1) * 2));
|
||||
var textGlowColor = (ushort)(textColor + 1);
|
||||
var sb = LSeStringBuilder.SharedPool.Get();
|
||||
var itemLink = sb
|
||||
.PushColorType(textColor)
|
||||
.PushEdgeColorType(textEdgeColor)
|
||||
.PushLinkItem(rawId, copyName)
|
||||
.Append(displayName)
|
||||
.PopLink()
|
||||
.PopEdgeColorType()
|
||||
.PopColorType()
|
||||
.ToReadOnlySeString();
|
||||
LSeStringBuilder.SharedPool.Return(sb);
|
||||
|
||||
// Note: `SeStringBuilder.AddItemLink` uses this function, so don't call it here!
|
||||
return new SeStringBuilder()
|
||||
.AddUiForeground(textColor)
|
||||
.AddUiGlow(textGlowColor)
|
||||
.Add(new ItemPayload(itemId, kind))
|
||||
.Append(TextArrowPayloads)
|
||||
.AddText(displayName)
|
||||
.AddUiGlowOff()
|
||||
.AddUiForegroundOff()
|
||||
.Add(RawPayload.LinkTerminator)
|
||||
.Build();
|
||||
return SeString.Parse(seStringEvaluator.EvaluateFromAddon(371, [itemLink], clientState.ClientLanguage));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -301,7 +281,7 @@ public class SeString
|
|||
public static SeString CreateMapLink(
|
||||
uint territoryId, uint mapId, float xCoord, float yCoord, float fudgeFactor = 0.05f) =>
|
||||
CreateMapLinkWithInstance(territoryId, mapId, null, xCoord, yCoord, fudgeFactor);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Creates an SeString representing an entire Payload chain that can be used to link a map position in the chat log.
|
||||
/// </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>
|
||||
public static SeString? CreateMapLink(string placeName, float xCoord, float yCoord, float fudgeFactor = 0.05f) =>
|
||||
CreateMapLinkWithInstance(placeName, null, xCoord, yCoord, fudgeFactor);
|
||||
|
||||
|
||||
/// <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.
|
||||
/// Returns null if no corresponding PlaceName was found.
|
||||
|
|
@ -511,7 +491,7 @@ public class SeString
|
|||
{
|
||||
messageBytes.AddRange(p.Encode());
|
||||
}
|
||||
|
||||
|
||||
// Add Null Terminator
|
||||
messageBytes.Add(0);
|
||||
|
||||
|
|
@ -526,7 +506,7 @@ public class SeString
|
|||
{
|
||||
return this.TextValue;
|
||||
}
|
||||
|
||||
|
||||
private static string GetMapLinkNameString(string placeName, int? instance, string coordinateString)
|
||||
{
|
||||
var instanceString = string.Empty;
|
||||
|
|
@ -534,7 +514,7 @@ public class SeString
|
|||
{
|
||||
instanceString = (SeIconChar.Instance1 + instance.Value - 1).ToIconString();
|
||||
}
|
||||
|
||||
|
||||
return $"{placeName}{instanceString} {coordinateString}";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue