mirror of
https://github.com/goatcorp/Dalamud.git
synced 2026-01-03 14:23:40 +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
26
Dalamud/Utility/ActionKindExtensions.cs
Normal file
26
Dalamud/Utility/ActionKindExtensions.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -23,4 +23,40 @@ public static class ClientLanguageExtensions
|
|||
_ => 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
159
Dalamud/Utility/ItemUtil.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
33
Dalamud/Utility/ObjectKindExtensions.cs
Normal file
33
Dalamud/Utility/ObjectKindExtensions.cs
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
using System.Linq;
|
||||
|
||||
using Lumina.Text.Parse;
|
||||
|
||||
using Lumina.Text.ReadOnly;
|
||||
|
|
@ -74,4 +76,154 @@ public static class SeStringExtensions
|
|||
/// <param name="value">character name to validate.</param>
|
||||
/// <returns>indicator if character is name is valid.</returns>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||
|
||||
|
|
@ -43,4 +44,48 @@ public static class StringExtensions
|
|||
if (!UIGlobals.IsValidPlayerCharacterName(value)) return false;
|
||||
return includeLegacy || value.Length <= 21;
|
||||
}
|
||||
|
||||
/// <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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue