From c6c0a13c92fec11178214e10db47e851148b98ec Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Wed, 10 Apr 2024 04:21:26 +0900 Subject: [PATCH 1/7] Add read-only value-typed views of SeString family and its builder --- src/Lumina.Tests/Lumina.Tests.csproj | 2 +- src/Lumina.Tests/LuminaTests.cs | 31 +- .../RequiresGameInstallationFact.cs | 15 + src/Lumina.Tests/SeStringBuilderTests.cs | 427 +++++++++++++ src/Lumina/Lumina.csproj | 2 +- .../Text/Expressions/ExpressionArity.cs | 19 + .../Expressions/ExpressionDataAttribute.cs | 23 + src/Lumina/Text/Expressions/ExpressionType.cs | 126 +++- .../Expressions/ExpressionTypeExtensions.cs | 35 ++ src/Lumina/Text/Payloads/BasePayload.cs | 29 +- src/Lumina/Text/Payloads/MacroCode.cs | 146 +++++ .../Text/Payloads/MacroCodeDataAttribute.cs | 20 + .../Text/Payloads/MacroCodeExtensions.cs | 27 + src/Lumina/Text/Payloads/MacroCodes.cs | 59 -- .../Text/ReadOnly/ReadOnlySeExpression.cs | 126 ++++ .../Text/ReadOnly/ReadOnlySeExpressionSpan.cs | 221 +++++++ src/Lumina/Text/ReadOnly/ReadOnlySePayload.cs | 534 +++++++++++++++++ .../Text/ReadOnly/ReadOnlySePayloadSpan.cs | 561 ++++++++++++++++++ .../Text/ReadOnly/ReadOnlySePayloadType.cs | 16 + src/Lumina/Text/ReadOnly/ReadOnlySeString.cs | 389 ++++++++++++ .../Text/ReadOnly/ReadOnlySeStringSpan.cs | 461 ++++++++++++++ src/Lumina/Text/SeExpressionUtilities.cs | 545 +++++++++++++++++ src/Lumina/Text/SeString.cs | 7 + src/Lumina/Text/SeStringBuilder.Append.cs | 277 +++++++++ .../Text/SeStringBuilder.Expressions.cs | 169 ++++++ src/Lumina/Text/SeStringBuilder.Presets.cs | 141 +++++ src/Lumina/Text/SeStringBuilder.cs | 202 +++++++ 27 files changed, 4483 insertions(+), 127 deletions(-) create mode 100644 src/Lumina.Tests/RequiresGameInstallationFact.cs create mode 100644 src/Lumina.Tests/SeStringBuilderTests.cs create mode 100644 src/Lumina/Text/Expressions/ExpressionArity.cs create mode 100644 src/Lumina/Text/Expressions/ExpressionDataAttribute.cs create mode 100644 src/Lumina/Text/Expressions/ExpressionTypeExtensions.cs create mode 100644 src/Lumina/Text/Payloads/MacroCode.cs create mode 100644 src/Lumina/Text/Payloads/MacroCodeDataAttribute.cs create mode 100644 src/Lumina/Text/Payloads/MacroCodeExtensions.cs delete mode 100644 src/Lumina/Text/Payloads/MacroCodes.cs create mode 100644 src/Lumina/Text/ReadOnly/ReadOnlySeExpression.cs create mode 100644 src/Lumina/Text/ReadOnly/ReadOnlySeExpressionSpan.cs create mode 100644 src/Lumina/Text/ReadOnly/ReadOnlySePayload.cs create mode 100644 src/Lumina/Text/ReadOnly/ReadOnlySePayloadSpan.cs create mode 100644 src/Lumina/Text/ReadOnly/ReadOnlySePayloadType.cs create mode 100644 src/Lumina/Text/ReadOnly/ReadOnlySeString.cs create mode 100644 src/Lumina/Text/ReadOnly/ReadOnlySeStringSpan.cs create mode 100644 src/Lumina/Text/SeExpressionUtilities.cs create mode 100644 src/Lumina/Text/SeStringBuilder.Append.cs create mode 100644 src/Lumina/Text/SeStringBuilder.Expressions.cs create mode 100644 src/Lumina/Text/SeStringBuilder.Presets.cs create mode 100644 src/Lumina/Text/SeStringBuilder.cs diff --git a/src/Lumina.Tests/Lumina.Tests.csproj b/src/Lumina.Tests/Lumina.Tests.csproj index 1a49d208..1abee6f6 100644 --- a/src/Lumina.Tests/Lumina.Tests.csproj +++ b/src/Lumina.Tests/Lumina.Tests.csproj @@ -1,7 +1,7 @@  - net6.0 + net8.0 false diff --git a/src/Lumina.Tests/LuminaTests.cs b/src/Lumina.Tests/LuminaTests.cs index 34c37dae..f05fc71c 100644 --- a/src/Lumina.Tests/LuminaTests.cs +++ b/src/Lumina.Tests/LuminaTests.cs @@ -1,22 +1,21 @@ using Xunit; -namespace Lumina.Tests +namespace Lumina.Tests; + +public class LuminaTests { - public class LuminaTests + [Theory] + [InlineData( "bg", "ex3", 13885885343001753777, "bg/ex3/01_nvt_n4/twn/n4t1/bgparts/n4t1_a1_chr03.mdl" )] + [InlineData( "music", "ex2", 16573140193792963234, "music/ex2/bgm_ex2_system_title.scd" )] + [InlineData( "chara", "ffxiv", 8982245735269998910, "chara/weapon/w0501/obj/body/b0018/vfx/texture/uv_cryst_128s.atex" )] + [InlineData( "sound", "ffxiv", 7568289509259556905, "sound/vfx/ability/se_vfx_abi_berserk_c.scd" )] + [InlineData( "exd", "ffxiv", 16400836168543909290, "exd/exportedsg.exh" )] + public void FilePathsAreParsedCorrectly( string category, string repo, ulong hash, string path ) { - [Theory] - [InlineData( "bg", "ex3", 13885885343001753777, "bg/ex3/01_nvt_n4/twn/n4t1/bgparts/n4t1_a1_chr03.mdl" )] - [InlineData( "music", "ex2", 16573140193792963234, "music/ex2/bgm_ex2_system_title.scd" )] - [InlineData( "chara", "ffxiv", 8982245735269998910, "chara/weapon/w0501/obj/body/b0018/vfx/texture/uv_cryst_128s.atex" )] - [InlineData( "sound", "ffxiv", 7568289509259556905, "sound/vfx/ability/se_vfx_abi_berserk_c.scd")] - [InlineData( "exd", "ffxiv", 16400836168543909290, "exd/exportedsg.exh")] - public void FilePathsAreParsedCorrectly( string category, string repo, ulong hash, string path ) - { - var parsed = GameData.ParseFilePath( path ); - - Assert.Equal( category, parsed.Category ); - Assert.Equal( repo, parsed.Repository ); - Assert.Equal( hash, parsed.IndexHash ); - } + var parsed = GameData.ParseFilePath( path )!; + + Assert.Equal( category, parsed.Category ); + Assert.Equal( repo, parsed.Repository ); + Assert.Equal( hash, parsed.IndexHash ); } } \ No newline at end of file diff --git a/src/Lumina.Tests/RequiresGameInstallationFact.cs b/src/Lumina.Tests/RequiresGameInstallationFact.cs new file mode 100644 index 00000000..3ff252af --- /dev/null +++ b/src/Lumina.Tests/RequiresGameInstallationFact.cs @@ -0,0 +1,15 @@ +using System.IO; +using Xunit; + +namespace Lumina.Tests; + +public sealed class RequiresGameInstallationFact : FactAttribute +{ + private const string path = @"C:\Program Files (x86)\SquareEnix\FINAL FANTASY XIV - A Realm Reborn\game\sqpack"; + + public RequiresGameInstallationFact() + { + if( !Directory.Exists( path ) ) + Skip = "Game installation is not found at the default path."; + } +} \ No newline at end of file diff --git a/src/Lumina.Tests/SeStringBuilderTests.cs b/src/Lumina.Tests/SeStringBuilderTests.cs new file mode 100644 index 00000000..55d12fec --- /dev/null +++ b/src/Lumina.Tests/SeStringBuilderTests.cs @@ -0,0 +1,427 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Lumina.Text; +using Lumina.Text.Expressions; +using Lumina.Text.Payloads; +using Lumina.Text.ReadOnly; +using Xunit; +using Xunit.Abstractions; + +namespace Lumina.Tests; + +public class SeStringBuilderTests +{ + private readonly ITestOutputHelper _outputHelper; + + public SeStringBuilderTests( ITestOutputHelper outputHelper ) + { + _outputHelper = outputHelper; + } + + [Fact] + public void InvalidCharacterTest() + { + Assert.Throws< ArgumentException >( + () => + new SeStringBuilder() + .Append( "NUL: \0" ) ); + + Assert.Throws< ArgumentException >( + () => + new SeStringBuilder() + .Append( "STX: \x02" ) ); + + Assert.Throws< ArgumentException >( + () => + new SeStringBuilder() + .Append( "NUL: \0"u8 ) ); + + Assert.Throws< ArgumentException >( + () => + new SeStringBuilder() + .Append( "STX: \x02"u8 ) ); + + var nulsb = new StringBuilder() + .Append( "NUL: " ) + .Append( '\0' ); + var stxsb = new StringBuilder() + .Append( "STX: " ) + .Append( '\x02' ); + + Assert.Throws< ArgumentException >( + () => + new SeStringBuilder() + .Append( nulsb ) ); + + Assert.Throws< ArgumentException >( + () => + new SeStringBuilder() + .Append( stxsb ) ); + + Assert.True( + new SeStringBuilder() + .Append( nulsb, 0, 4 ) + .ToArray() + .AsSpan() + .SequenceEqual( "NUL:"u8 ) ); + + Assert.Throws< ArgumentException >( + () => + new SeStringBuilder() + .Append( nulsb, 5, 1 ) ); + + new SeStringBuilder() + .Append( stxsb, 0, 4 ); + + Assert.Throws< ArgumentException >( + () => + new SeStringBuilder() + .Append( stxsb, 5, 1 ) ); + } + + [Fact] + public void PayloadStateTest() + { + new SeStringBuilder() + .BeginMacro( MacroCode.Bold ) + .EndMacro() + .ToReadOnlySeString(); + + new SeStringBuilder() + .BeginMacro( MacroCode.Bold ) + .BeginStringExpression() + .BeginMacro( MacroCode.Italic ) + .EndMacro() + .EndExpression() + .EndMacro() + .ToReadOnlySeString(); + + Assert.Throws< ArgumentOutOfRangeException >( + () => + new SeStringBuilder() + .BeginMacro( 0 ) ); + + Assert.Throws< InvalidOperationException >( + () => + new SeStringBuilder() + .BeginMacro( MacroCode.Bold ) + .ToReadOnlySeString() ); + + Assert.Throws< InvalidOperationException >( + () => + new SeStringBuilder() + .BeginMacro( MacroCode.Bold ) + .BeginMacro( MacroCode.Bold ) ); + + Assert.Throws< InvalidOperationException >( + () => + new SeStringBuilder() + .BeginMacro( MacroCode.Bold ) + .EndMacro() + .EndMacro() ); + } + + [Fact] + public void ExpressionStateTest() + { + new SeStringBuilder() + .BeginMacro( MacroCode.Bold ) + .AppendUIntExpression( 0 ) + .BeginUnaryExpression( ExpressionType.LocalNumber ) + .AppendUIntExpression( 0 ) + .EndExpression() + .BeginBinaryExpression( ExpressionType.GreaterThan ) + .AppendUIntExpression( 0 ) + .AppendUIntExpression( 1 ) + .EndExpression() + .BeginBinaryExpression( ExpressionType.GreaterThan ) + .BeginUnaryExpression( ExpressionType.LocalNumber ) + .AppendUIntExpression( 0 ) + .EndExpression() + .AppendNullaryExpression( ExpressionType.Day ) + .EndExpression() + .EndMacro(); + + Assert.Throws< ArgumentOutOfRangeException >( + () => + new SeStringBuilder() + .BeginMacro( MacroCode.Bold ) + .AppendNullaryExpression( 0 ) ); + + Assert.Throws< ArgumentOutOfRangeException >( + () => + new SeStringBuilder() + .BeginMacro( MacroCode.Bold ) + .BeginUnaryExpression( 0 ) ); + + Assert.Throws< ArgumentOutOfRangeException >( + () => + new SeStringBuilder() + .BeginMacro( MacroCode.Bold ) + .BeginBinaryExpression( 0 ) ); + + Assert.Throws< InvalidOperationException >( + () => new SeStringBuilder() + .BeginUnaryExpression( ExpressionType.GlobalString ) + .AppendUIntExpression( 0 ) ); + + Assert.Throws< InvalidOperationException >( + () => new SeStringBuilder() + .BeginBinaryExpression( ExpressionType.NotEqual ) + .AppendUIntExpression( 0 ) ); + + Assert.Throws< InvalidOperationException >( + () => new SeStringBuilder() + .BeginMacro( MacroCode.Bold ) + .BeginBinaryExpression( ExpressionType.LessThan ) + .AppendUIntExpression( 0 ) + .AppendUIntExpression( 0 ) + .AppendUIntExpression( 0 ) ); + + Assert.Throws< InvalidOperationException >( + () => new SeStringBuilder() + .BeginMacro( MacroCode.Bold ) + .EndExpression() ); + } + + [Fact] + public void ExpressionArityTestUnaryInsufficient() => + Assert.Throws< InvalidOperationException >( + () => new SeStringBuilder() + .BeginMacro( MacroCode.Bold ) + .BeginUnaryExpression( ExpressionType.LocalNumber ) + .EndExpression() + .ToReadOnlySeString() ); + + [Fact] + public void ExpressionArityTestUnaryCorrect() => + new SeStringBuilder() + .BeginMacro( MacroCode.Bold ) + .BeginUnaryExpression( ExpressionType.LocalNumber ) + .AppendIntExpression( 0 ) + .EndExpression() + .EndMacro() + .ToReadOnlySeString(); + + [Fact] + public void ExpressionArityTestUnaryOverfed() => + Assert.Throws< InvalidOperationException >( + () => new SeStringBuilder() + .BeginMacro( MacroCode.Bold ) + .BeginUnaryExpression( ExpressionType.LocalNumber ) + .AppendIntExpression( 0 ) + .AppendIntExpression( 1 ) + .EndExpression() + .EndMacro() + .ToReadOnlySeString() ); + + [Fact] + public void ExpressionArityTestBinaryInsufficient() + { + Assert.Throws< InvalidOperationException >( () => new SeStringBuilder() + .BeginMacro( MacroCode.Bold ) + .BeginBinaryExpression( ExpressionType.Equal ) + .EndExpression() + .EndMacro() + .ToReadOnlySeString() ); + Assert.Throws< InvalidOperationException >( () => new SeStringBuilder() + .BeginMacro( MacroCode.Bold ) + .BeginBinaryExpression( ExpressionType.Equal ) + .AppendIntExpression( 0 ) + .EndExpression() + .EndMacro() + .ToReadOnlySeString() ); + } + + [Fact] + public void ExpressionArityTestBinaryCorrect() => + new SeStringBuilder() + .BeginMacro( MacroCode.Bold ) + .BeginBinaryExpression( ExpressionType.Equal ) + .AppendIntExpression( 0 ) + .AppendIntExpression( 1 ) + .EndExpression() + .EndMacro() + .ToReadOnlySeString(); + + [Fact] + public void ExpressionArityTestBinaryOverfed() => + Assert.Throws< InvalidOperationException >( + () => new SeStringBuilder() + .BeginMacro( MacroCode.Bold ) + .BeginBinaryExpression( ExpressionType.Equal ) + .AppendIntExpression( 0 ) + .AppendIntExpression( 1 ) + .AppendIntExpression( 2 ) + .EndExpression() + .EndMacro() + .ToReadOnlySeString() ); + + [Fact] + public void ComplicatedTest() + { + var test = + "" + + ReadOnlySePayloadSpan.FromText( "Testing" ) + + "_" + + new SeStringBuilder() + .Append( "asdf: " ) + .Append( 12345 ) + .BeginMacro( MacroCode.If ) + .BeginBinaryExpression( ExpressionType.Equal ) + .AppendIntExpression( 0x55 ) + .AppendNullaryExpression( ExpressionType.Day ) + .EndExpression() + .BeginStringExpression() + .AppendItalicized( "TRUE" ) + .EndExpression() + .BeginStringExpression() + .AppendBold( "false"u8 ) + .EndExpression() + .EndMacro() + .Append( " "u8 ) + .PushColorType( 510 ) + .PushEdgeColorType( 510 ) + .Append( "Colored text" ) + .PopColorType() + .PopEdgeColorType() + .ToReadOnlySeString(); + _outputHelper.WriteLine( test.ToString() ); + } + + [RequiresGameInstallationFact] + public void AddonIsParsedCorrectly() + { + var gameData = new GameData( @"C:\Program Files (x86)\SquareEnix\FINAL FANTASY XIV - A Realm Reborn\game\sqpack" ); + var addon = gameData.Excel.GetSheetRaw( "Addon" )!; + var ssb = new SeStringBuilder(); + var expected = new Dictionary< uint, ReadOnlySeString > + { + [ 6 ] = ssb + .Clear() + .BeginMacro( MacroCode.Switch ) + .AppendLocalNumberExpression( 1 ) + .BeginStringExpression().PushColorType( 549 ).PushEdgeColorType( 550 ).EndExpression() + .BeginStringExpression().PushColorType( 551 ).PushEdgeColorType( 552 ).EndExpression() + .BeginStringExpression().PushColorType( 553 ).PushEdgeColorType( 554 ).EndExpression() + .BeginStringExpression().PushColorType( 555 ).PushEdgeColorType( 556 ).EndExpression() + .BeginStringExpression().PushColorType( 557 ).PushEdgeColorType( 558 ).EndExpression() + .BeginStringExpression().PushColorType( 559 ).PushEdgeColorType( 560 ).EndExpression() + .BeginStringExpression().PushColorType( 561 ).PushEdgeColorType( 562 ).EndExpression() + .BeginStringExpression().PushColorType( 563 ).PushEdgeColorType( 564 ).EndExpression() + .EndMacro() + .ToReadOnlySeString(), + [ 15 ] = ssb + .Clear() + .BeginMacro( MacroCode.If ) + .BeginBinaryExpression( ExpressionType.Equal ) + .AppendGlobalNumberExpression( 78 ) + .AppendIntExpression( 99 ) + .EndExpression() + .AppendStringExpression( "Online ID" ) + .AppendStringExpression( "Gamertag" ) + .EndMacro() + .Append( ": "u8 ) + .BeginMacro( MacroCode.String ) + .AppendLocalStringExpression( 1 ) + .EndMacro() + .ToReadOnlySeString(), + [ 28 ] = ssb + .Clear() + .BeginMacro( MacroCode.SetTime ) + .AppendLocalNumberExpression( 1 ) + .EndMacro() + .BeginMacro( MacroCode.If ) + .AppendNullaryExpression( ExpressionType.Hour ) + .BeginStringExpression() + .BeginMacro( MacroCode.Switch ) + .AppendNullaryExpression( ExpressionType.Hour ) + .AppendIntExpression( 1 ) + .AppendIntExpression( 2 ) + .AppendIntExpression( 3 ) + .AppendIntExpression( 4 ) + .AppendIntExpression( 5 ) + .AppendIntExpression( 6 ) + .AppendIntExpression( 7 ) + .AppendIntExpression( 8 ) + .AppendIntExpression( 9 ) + .AppendIntExpression( 10 ) + .AppendIntExpression( 11 ) + .AppendIntExpression( 12 ) + .AppendIntExpression( 1 ) + .AppendIntExpression( 2 ) + .AppendIntExpression( 3 ) + .AppendIntExpression( 4 ) + .AppendIntExpression( 5 ) + .AppendIntExpression( 6 ) + .AppendIntExpression( 7 ) + .AppendIntExpression( 8 ) + .AppendIntExpression( 9 ) + .AppendIntExpression( 10 ) + .AppendIntExpression( 11 ) + .EndMacro() + .EndExpression() + .AppendIntExpression( 12 ) + .EndMacro() + .Append( ":" ) + .BeginMacro( MacroCode.Sec ) + .AppendNullaryExpression( ExpressionType.Minute ) + .EndMacro() + .Append( " " ) + .BeginMacro( MacroCode.If ) + .BeginBinaryExpression( ExpressionType.GreaterThanOrEqualTo ) + .AppendNullaryExpression( ExpressionType.Hour ) + .AppendIntExpression( 12 ) + .EndExpression() + .AppendStringExpression( "p.m." ) + .AppendStringExpression( "a.m." ) + .EndMacro() + .ToReadOnlySeString(), + [ 110 ] = ssb + .Clear() + .PushColorType( 508 ) + .PushEdgeColorType( 509 ) + .Append("Discard "u8) + .BeginMacro( MacroCode.If ) + .BeginBinaryExpression( ExpressionType.Equal ) + .AppendLocalNumberExpression( 2 ) + .AppendIntExpression( 1 ) + .EndExpression() + .BeginStringExpression() + .BeginMacro( MacroCode.EnNoun ) + .AppendStringExpression( "Item" ) + .AppendIntExpression( 2 ) + .AppendLocalNumberExpression( 1 ) + .AppendIntExpression( 1 ) + .AppendIntExpression( 1 ) + .EndMacro() + .EndExpression() + .BeginStringExpression() + .BeginMacro( MacroCode.Num ) + .AppendLocalNumberExpression( 2 ) + .EndMacro() + .Append(" " ) + .BeginMacro( MacroCode.EnNoun ) + .AppendStringExpression( "Item" ) + .AppendIntExpression( 3 ) + .AppendLocalNumberExpression( 1 ) + .AppendLocalNumberExpression( 2 ) + .AppendIntExpression( 1 ) + .EndMacro() + .EndExpression() + .EndMacro() + .Append("?" ) + .PopEdgeColorType() + .PopColorType() + .ToReadOnlySeString(), + }; + foreach( var row in addon ) + { + var r = row.ReadColumn< SeString >( 0 ).AsReadOnly(); + _outputHelper.WriteLine( $"{row.RowId}\t{r.ExtractText()}\t{r}" ); + if( expected.TryGetValue( row.RowId, out var expectedSeString ) ) + Assert.True( expectedSeString == r, $"{row.RowId} does not match; expected {expectedSeString}" ); + } + } +} \ No newline at end of file diff --git a/src/Lumina/Lumina.csproj b/src/Lumina/Lumina.csproj index 9c396166..60546230 100644 --- a/src/Lumina/Lumina.csproj +++ b/src/Lumina/Lumina.csproj @@ -1,7 +1,7 @@  - net6.0;net7.0 + net6.0;net7.0;net8.0 latestmajor true NotAdam diff --git a/src/Lumina/Text/Expressions/ExpressionArity.cs b/src/Lumina/Text/Expressions/ExpressionArity.cs new file mode 100644 index 00000000..29187d4f --- /dev/null +++ b/src/Lumina/Text/Expressions/ExpressionArity.cs @@ -0,0 +1,19 @@ +namespace Lumina.Text.Expressions; + +/// +/// Number of sub-expressions for an expression. +/// +public enum ExpressionArity +{ + /// The expression does not have a use for concept of arity. + Invalid, + + /// The expression does not have any sub-expression. + Nullary, + + /// The expression has one sub-expression. + Unary, + + /// The expression has two sub-expressions. + Binary, +} \ No newline at end of file diff --git a/src/Lumina/Text/Expressions/ExpressionDataAttribute.cs b/src/Lumina/Text/Expressions/ExpressionDataAttribute.cs new file mode 100644 index 00000000..1bcf26b2 --- /dev/null +++ b/src/Lumina/Text/Expressions/ExpressionDataAttribute.cs @@ -0,0 +1,23 @@ +using System; + +namespace Lumina.Text.Expressions; + +/// Attaches extra data for . +[AttributeUsage( AttributeTargets.Field )] +internal sealed class ExpressionDataAttribute : Attribute +{ + /// Initializes a new instance of class. + /// The arity. + /// The native name. + public ExpressionDataAttribute( ExpressionArity arity, string nativeName ) + { + Arity = arity; + NativeName = nativeName; + } + + /// Gets the arity. + public ExpressionArity Arity { get; } + + /// Gets the native name. + public string NativeName { get; } +} \ No newline at end of file diff --git a/src/Lumina/Text/Expressions/ExpressionType.cs b/src/Lumina/Text/Expressions/ExpressionType.cs index d4c01f1d..35999599 100644 --- a/src/Lumina/Text/Expressions/ExpressionType.cs +++ b/src/Lumina/Text/Expressions/ExpressionType.cs @@ -1,34 +1,102 @@ +using System; + namespace Lumina.Text.Expressions; +/// Named types for expressions. public enum ExpressionType : byte { - // Placeholder expressions (Zero arity): 0xD8 ~ 0xDF - Millisecond = 0xD8, // t_msec - Second = 0xD9, // t_sec - Minute = 0xDA, // t_min - Hour = 0xDB, // t_hour 0 ~ 23 - Day = 0xDC, // t_day - Weekday = 0xDD, // t_wday 1(sunday) ~ 7(saturday) - Month = 0xDE, // t_mon - Year = 0xDF, // t_year - - // Binary expressions - GreaterThanOrEqualTo = 0xE0, // [gteq] - GreaterThan = 0xE1, // [gt] - LessThanOrEqualTo = 0xE2, // [lteq] - LessThan = 0xE3, // [lt] - Equal = 0xE4, // [eq] - NotEqual = 0xE5, // [neq] - - // Parameter (unary expressions) - IntegerParameter = 0xE8, // lnum - PlayerParameter = 0xE9, // gnum - StringParameter = 0xEA, // lstr - ObjectParameter = 0xEB, // gstr - - // Placeholder expressions also exist between 0xEC ~ 0xEF, where 0xEC is at least checked existing. - StackColor = 0xEC, // stackcolor - - // Embedded SeString - SeString = 0xff, // Followed by length (including length) and data + /// Uses the millisecond value in the contextual time storage. + [ExpressionData( ExpressionArity.Nullary, "t_msec" )] + Millisecond = 0xD8, + + /// Uses the second value in the contextual time storage. + [ExpressionData( ExpressionArity.Nullary, "t_sec" )] + Second = 0xD9, + + /// Uses the minute value in the contextual time storage. + [ExpressionData( ExpressionArity.Nullary, "t_min" )] + Minute = 0xDA, + + /// Uses the hour value in the contextual time storage, ranging from 0 to 23. + [ExpressionData( ExpressionArity.Nullary, "t_hour" )] + Hour = 0xDB, + + /// Uses the day of month value in the contextual time storage. + [ExpressionData( ExpressionArity.Nullary, "t_day" )] + Day = 0xDC, + + /// Uses the weekday value in the contextual time storage, ranging from 1(Sunday) to 7(Saturday). + [ExpressionData( ExpressionArity.Nullary, "t_wday" )] + Weekday = 0xDD, + + /// Uses the month value in the contextual time storage, ranging from 0(January) to 11(December). + [ExpressionData( ExpressionArity.Nullary, "t_mon" )] + Month = 0xDE, + + /// Uses the year value in the contextual time storage, where 0 means the year 1900. + [ExpressionData( ExpressionArity.Nullary, "t_year" )] + Year = 0xDF, + + /// Tests if the evaluated result from first sub-expression is greater than or equal to the evaluated result from second sub-expression. + [ExpressionData( ExpressionArity.Binary, "[gteq]" )] + GreaterThanOrEqualTo = 0xE0, + + /// Tests if the evaluated result from first sub-expression is greater than the evaluated result from second sub-expression. + [ExpressionData( ExpressionArity.Binary, "[gt]" )] + GreaterThan = 0xE1, + + /// Tests if the evaluated result from first sub-expression is less than or equal to the evaluated result from second sub-expression. + [ExpressionData( ExpressionArity.Binary, "[lteq]" )] + LessThanOrEqualTo = 0xE2, + + /// Tests if the evaluated result from first sub-expression is less than the evaluated result from second sub-expression. + [ExpressionData( ExpressionArity.Binary, "[lt]" )] + LessThan = 0xE3, + + /// Tests if the evaluated result from first sub-expression is equal to the evaluated result from second sub-expression. + [ExpressionData( ExpressionArity.Binary, "[eq]" )] + Equal = 0xE4, + + /// Tests if the evaluated result from first sub-expression is not equal to the evaluated result from second sub-expression. + [ExpressionData( ExpressionArity.Binary, "[neq]" )] + NotEqual = 0xE5, + + /// Uses a numeric value at the specified index in the local parameter storage. + [ExpressionData( ExpressionArity.Unary, "lnum" )] + LocalNumber = 0xE8, + + /// Uses a numeric value at the specified index in the global parameter storage. + [ExpressionData( ExpressionArity.Unary, "gnum" )] + GlobalNumber = 0xE9, + + /// Uses a SeString value at the specified index in the local parameter storage. + [ExpressionData( ExpressionArity.Unary, "lstr" )] + LocalString = 0xEA, + + /// Uses a SeString value at the specified index in the global parameter storage. + [ExpressionData( ExpressionArity.Unary, "gstr" )] + GlobalString = 0xEB, + + /// Uses the last color value pushed. + [ExpressionData( ExpressionArity.Nullary, "stackcolor" )] + StackColor = 0xEC, + + /// A SeString preceded by its length is followed. + SeString = 0xff, + + /// Old name for . + [Obsolete( $"Use {nameof( LocalNumber )}." )] + IntegerParameter = LocalNumber, + + /// Old name for . + [Obsolete( $"Use {nameof( GlobalNumber )}." )] + PlayerParameter = GlobalNumber, + + /// Old name for . + [Obsolete( $"Use {nameof( LocalString )}." )] + StringParameter = LocalString, + + /// Old name for . + [Obsolete( $"Use {nameof( GlobalString )}." )] + ObjectParameter = GlobalString, } \ No newline at end of file diff --git a/src/Lumina/Text/Expressions/ExpressionTypeExtensions.cs b/src/Lumina/Text/Expressions/ExpressionTypeExtensions.cs new file mode 100644 index 00000000..fb6b012f --- /dev/null +++ b/src/Lumina/Text/Expressions/ExpressionTypeExtensions.cs @@ -0,0 +1,35 @@ +using System.Reflection; + +namespace Lumina.Text.Expressions; + +/// Extension methods for . +public static class ExpressionTypeExtensions +{ + private static readonly string?[] NativeNames; + private static readonly ExpressionArity[] Arities; + + static ExpressionTypeExtensions() + { + NativeNames = new string?[256]; + Arities = new ExpressionArity[256]; + foreach( var v in typeof( ExpressionType ).GetFields( BindingFlags.Static | BindingFlags.Public ) ) + { + if( v.GetRawConstantValue() is not byte b ) + continue; + if( v.GetCustomAttribute< ExpressionDataAttribute >() is not { } eda ) + continue; + NativeNames[ b ] = eda.NativeName; + Arities[ b ] = eda.Arity; + } + } + + /// Gets the native name for an expression type, if available. + /// The expression type. + /// The native name of the expression type, or null if not available. + public static string? GetNativeName( this ExpressionType v ) => NativeNames[ (int) v ]; + + /// Gets the arity for an expression type, if applicable. + /// The expression type. + /// The arity of the expression type, or if not applicable. + public static ExpressionArity GetArity( this ExpressionType v ) => Arities[ (int) v ]; +} \ No newline at end of file diff --git a/src/Lumina/Text/Payloads/BasePayload.cs b/src/Lumina/Text/Payloads/BasePayload.cs index 165dad6f..a0b219b5 100644 --- a/src/Lumina/Text/Payloads/BasePayload.cs +++ b/src/Lumina/Text/Payloads/BasePayload.cs @@ -247,27 +247,14 @@ public override string ToString() if( PayloadType == PayloadType.Text ) return RawString.Replace( "<", "\\<" ); - var code = (MacroCodes)PayloadType; - switch( code ) - { - case MacroCodes.NewLine: - return "
"; - case MacroCodes.NonBreakingSpace: - return ""; - case MacroCodes.SoftHyphen: - return "<->"; - case MacroCodes.Hyphen: - return "<-->"; - default: - { - if( Expressions.Count == 0 ) - return $"<{code.ToString().ToLower()}>"; - - return $"<{code.ToString().ToLower()}({string.Join( ',', Expressions.Select( - ex => ex is StringExpression se ? se.Value.ToMacroString() : ex.ToString() - ) )})>"; - } - } + var code = (MacroCode)PayloadType; + var encodeName = code.GetEncodeName(); + if( Expressions.Count == 0 ) + return $"<{encodeName}>"; + + return $"<{encodeName}({string.Join( ',', Expressions.Select( + ex => ex is StringExpression se ? se.Value.ToMacroString() : ex.ToString() + ) )})>"; } } } \ No newline at end of file diff --git a/src/Lumina/Text/Payloads/MacroCode.cs b/src/Lumina/Text/Payloads/MacroCode.cs new file mode 100644 index 00000000..7a6bd4c6 --- /dev/null +++ b/src/Lumina/Text/Payloads/MacroCode.cs @@ -0,0 +1,146 @@ +namespace Lumina.Text.Payloads; + +/// Valid macro payload types. +public enum MacroCode : byte +{ + /// Sets the reset time to the contextual time storage. + /// Parameters: weekday, hour, terminator. + [MacroCodeData( null, "n N x" )] SetResetTime = 0x06, + + /// Sets the specified time to the contextual time storage. + /// Parameters: unix timestamp in seconds, terminator. + [MacroCodeData( null, "n x" )] SetTime = 0x07, + + /// Tests an expression and uses a corresponding subexpression. + /// Parameters: condition, expression to use if condition is true, expression to use if condition is false. + [MacroCodeData( null, ". . * x" )] If = 0x08, + + /// Tests an expression and uses a corresponding subexpression. + /// Parameters: condition, expression to use if condition is 1, expression to use if condition is 2, and so on. + [MacroCodeData( null, ". . ." )] Switch = 0x09, + + [MacroCodeData( null, "n x" )] PcName = 0x0A, + [MacroCodeData( null, "n . . x" )] IfPcGender = 0x0B, + [MacroCodeData( null, "n . . . x" )] IfPcName = 0x0C, + + /// Determines the type of josa required from the last character of the first expression. + /// Parameters: test string, eun/i/eul suffix, neun/ga/reul suffix, terminator. + [MacroCodeData( null, "s s s x" )] Josa = 0x0D, + + /// Determines the type of josa, ro in particular, required from the last character of the first expression. + /// Parameters: test string, ro suffix, euro suffix, terminator. + [MacroCodeData( null, "s s s x" )] Josaro = 0x0E, + + [MacroCodeData( null, "n . . x" )] IfSelf = 0x0F, + + /// Adds a line break. + [MacroCodeData( "br", "" )] NewLine = 0x10, + + /// Waits for a specified duration. + /// Parameters: delay in seconds, terminator. + [MacroCodeData( null, "n x" )] Wait = 0x11, + + /// Adds an icon from gfdata.gfd. + /// Parameters: icon ID, terminator. + [MacroCodeData( null, "n x" )] Icon = 0x12, + + /// Pushes the text foreground color. + /// Parameters: something that resolves to B8G8R8A8 or stackcolor(, ?), terminator + [MacroCodeData( null, "n N x" )] Color = 0x13, + + /// Pushes the text border color. + /// Parameters: something that resolves to B8G8R8A8 or stackcolor(, ?), terminator + [MacroCodeData( null, "n N x" )] EdgeColor = 0x14, + + /// Pushes the text shadow color. + /// Parameters: something that resolves to B8G8R8A8 or stackcolor(, ?), terminator + [MacroCodeData( null, "n N x" )] ShadowColor = 0x15, + + /// Adds a soft hyphen. + [MacroCodeData( "-", "" )] SoftHyphen = 0x16, + + [MacroCodeData( null, "" )] Key = 0x17, + [MacroCodeData( null, "n" )] Scale = 0x18, + + /// Sets whether to use bold text effect. + /// Parameters: bool enabled. + [MacroCodeData( null, "n" )] Bold = 0x19, + + /// Sets whether to use italic text effect. + /// Parameters: bool enabled. + [MacroCodeData( null, "n" )] Italic = 0x1A, + + [MacroCodeData( null, "n n" )] Edge = 0x1B, + [MacroCodeData( null, "n n" )] Shadow = 0x1C, + + /// Adds a non-breaking space. + [MacroCodeData( "nbsp", "" )] NonBreakingSpace = 0x1D, + + [MacroCodeData( null, "n N x" )] Icon2 = 0x1E, + + /// Adds a hyphen. + [MacroCodeData( "--", "" )] Hyphen = 0x1F, + + /// Adds a decimal representation of an integer expression. + /// Parameters: integer expression, terminator. + [MacroCodeData( null, "n x" )] Num = 0x20, + + /// Adds a hexadecimal representation of an integer expression. + /// Parameters: integer expression, terminator. + [MacroCodeData( null, "n x" )] Hex = 0x21, + + /// Adds a decimal representation of an integer expression, separating by thousands. + /// Parameters: integer expression, separator (usually a comma or a dot), terminator. + [MacroCodeData( null, ". s x" )] Kilo = 0x22, + + [MacroCodeData( null, "n x" )] Byte = 0x23, + + /// Adds a zero-padded-to-two-digits decimal representation of an integer expression. + /// Parameters: integer expression, terminator. + [MacroCodeData( null, "n x" )] Sec = 0x24, + + [MacroCodeData( null, "n x" )] Time = 0x25, + [MacroCodeData( null, "n n s x" )] Float = 0x26, + [MacroCodeData( null, "n n n n s" )] Link = 0x27, + + /// Adds a column from a sheet. + /// Parameters: sheet name, row ID, column ID(, ?). + [MacroCodeData( null, "s . . ." )] Sheet = 0x28, + + /// Adds a string expression as-is. + /// Parameters: string expression, terminator. + [MacroCodeData( null, "s x" )] String = 0x29, + + /// Adds a string, fully upper cased. + /// Parameters: string expression, terminator. + [MacroCodeData( null, "s x" )] Caps = 0x2A, + + [MacroCodeData( null, "s x" )] Head = 0x2B, + [MacroCodeData( null, "s s n x" )] Split = 0x2C, + [MacroCodeData( null, "s x" )] HeadAll = 0x2D, + [MacroCodeData( null, "n n . . ." )] Fixed = 0x2E, + + /// Adds a string, fully lower cased. + /// Parameters: string expression, terminator. + [MacroCodeData( null, "s x" )] Lower = 0x2F, + + [MacroCodeData( null, "s . ." )] JaNoun = 0x30, + [MacroCodeData( null, "s . ." )] EnNoun = 0x31, + [MacroCodeData( null, "s . ." )] DeNoun = 0x32, + [MacroCodeData( null, "s . ." )] FrNoun = 0x33, + [MacroCodeData( null, "s . ." )] ChNoun = 0x34, + [MacroCodeData( null, "s x" )] LowerHead = 0x40, + + /// Pushes the text foreground color, referring to a color defined in UIColor sheet. + /// Parameters: row ID in UIColor sheet or 0 to pop(or reset?) the pushed color, terminator. + [MacroCodeData( null, "n x" )] ColorType = 0x48, + + /// Pushes the text border color, referring to a color defined in UIColor sheet. + /// Parameters: row ID in UIColor sheet or 0 to pop(or reset?) the pushed color, terminator. + [MacroCodeData( null, "n x" )] EdgeColorType = 0x49, + + [MacroCodeData( null, "n n x" )] Digit = 0x50, + [MacroCodeData( null, "n x" )] Ordinal = 0x51, + [MacroCodeData( null, "n n" )] Sound = 0x60, + [MacroCodeData( null, "x" )] LevelPos = 0x61, +} \ No newline at end of file diff --git a/src/Lumina/Text/Payloads/MacroCodeDataAttribute.cs b/src/Lumina/Text/Payloads/MacroCodeDataAttribute.cs new file mode 100644 index 00000000..887e6835 --- /dev/null +++ b/src/Lumina/Text/Payloads/MacroCodeDataAttribute.cs @@ -0,0 +1,20 @@ +using System; + +namespace Lumina.Text.Payloads; + +/// Attaches extra data for . +[AttributeUsage( AttributeTargets.Field )] +internal sealed class MacroCodeDataAttribute : Attribute +{ + public MacroCodeDataAttribute( string? encodedName, string parameterDefinition ) + { + EncodedName = encodedName; + ParameterDefinition = parameterDefinition; + } + + /// Gets the name to use instead when encoding to the text representation of SeString. + public string? EncodedName { get; } + + /// Gets the parameter definition. + public string ParameterDefinition { get; } +} \ No newline at end of file diff --git a/src/Lumina/Text/Payloads/MacroCodeExtensions.cs b/src/Lumina/Text/Payloads/MacroCodeExtensions.cs new file mode 100644 index 00000000..0940d0ef --- /dev/null +++ b/src/Lumina/Text/Payloads/MacroCodeExtensions.cs @@ -0,0 +1,27 @@ +using System.Reflection; + +namespace Lumina.Text.Payloads; + +/// Extension methods for . +public static class MacroCodeExtensions +{ + private static readonly string?[] EncodedNames; + + static MacroCodeExtensions() + { + EncodedNames = new string?[256]; + foreach( var v in typeof( MacroCode ).GetFields( BindingFlags.Static | BindingFlags.Public ) ) + { + if( v.GetRawConstantValue() is not byte b ) + continue; + if( v.GetCustomAttribute< MacroCodeDataAttribute >() is not { } mcda ) + continue; + EncodedNames[ b ] = mcda.EncodedName ?? v.Name.ToLowerInvariant(); + } + } + + /// Gets the encoded name for an macro code, if available. + /// The macro code. + /// The native name of the macro code, or null if not available. + public static string? GetEncodeName( this MacroCode v ) => EncodedNames[ (int) v ]; +} \ No newline at end of file diff --git a/src/Lumina/Text/Payloads/MacroCodes.cs b/src/Lumina/Text/Payloads/MacroCodes.cs deleted file mode 100644 index f4107b37..00000000 --- a/src/Lumina/Text/Payloads/MacroCodes.cs +++ /dev/null @@ -1,59 +0,0 @@ -namespace Lumina.Text.Payloads; - -internal enum MacroCodes : byte -{ - SetResetTime = 0x06, // n N x - SetTime = 0x07, // n x - If = 0x08, // . . * x - Switch = 0x09, // . . . - PcName = 0x0A, // n x - IfPcGender = 0x0B, // n . . x - IfPcName = 0x0C, // n . . . x - Josa = 0x0D, // s s s x - Josaro = 0x0E, // s s s x - IfSelf = 0x0F, // n . . x - NewLine = 0x10, //
- Wait = 0x11, // n x - Icon = 0x12, // n x - Color = 0x13, // n N x - EdgeColor = 0x14, // n N x - ShadowColor = 0x15, // n N x - SoftHyphen = 0x16, // - - Key = 0x17, // - Scale = 0x18, // n - Bold = 0x19, // n - Italic = 0x1A, // n - Edge = 0x1B, // n n - Shadow = 0x1C, // n n - NonBreakingSpace = 0x1D, // - Icon2 = 0x1E, // n N x - Hyphen = 0x1F, // -- - Num = 0x20, // n x - Hex = 0x21, // n x - Kilo = 0x22, // . s x - Byte = 0x23, // n x - Sec = 0x24, // n x - Time = 0x25, // n x - Float = 0x26, // n n s x - Link = 0x27, // n n n n s - Sheet = 0x28, // s . . . - String = 0x29, // s x - Caps = 0x2A, // s x - Head = 0x2B, // s x - Split = 0x2C, // s s n x - HeadAll = 0x2D, // s x - Fixed = 0x2E, // n n . . . - Lower = 0x2F, // s x - JaNoun = 0x30, // s . . - EnNoun = 0x31, // s . . - DeNoun = 0x32, // s . . - FrNoun = 0x33, // s . . - ChNoun = 0x34, // s . . - LowerHead = 0x40, // s x - ColorType = 0x48, // n x - EdgeColorType = 0x49, // n x - Digit = 0x50, // n n x - Ordinal = 0x51, // n x - Sound = 0x60, // n n - LevelPos = 0x61 // n x -} diff --git a/src/Lumina/Text/ReadOnly/ReadOnlySeExpression.cs b/src/Lumina/Text/ReadOnly/ReadOnlySeExpression.cs new file mode 100644 index 00000000..afb40eb4 --- /dev/null +++ b/src/Lumina/Text/ReadOnly/ReadOnlySeExpression.cs @@ -0,0 +1,126 @@ +using System; +using System.Runtime.CompilerServices; + +namespace Lumina.Text.ReadOnly; + +/// Represents an expression in a . +public readonly struct ReadOnlySeExpression : IEquatable< ReadOnlySeExpression > +{ + /// Read-only byte data for the SeString expression. + public readonly ReadOnlyMemory< byte > Body; + + /// Initializes a new instance of the struct. + /// Raw SeString expression data. + public ReadOnlySeExpression( ReadOnlyMemory< byte > body ) => Body = body; + + /// Gets the read-only byte memory view of this instance of . + /// The expression to get a byte memory view of. + /// The read-only byte memory view of . + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public static implicit operator ReadOnlyMemory< byte >( ReadOnlySeExpression s ) => s.Body; + + /// Gets the read-only byte span view of this instance of . + /// The expression to get a byte span view of. + /// The read-only byte span view of . + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public static implicit operator ReadOnlySpan< byte >( ReadOnlySeExpression s ) => s.Body.Span; + + /// Creates a new instance of struct from the given byte memory view. + /// The source byte memory view. + /// A new instance of wrapping . + /// The backing data of is not expected to be mutated once the cast is done. + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public static implicit operator ReadOnlySeExpression( ReadOnlyMemory< byte > s ) => new( s ); + + /// Creates a new instance of struct from the given byte memory view. + /// The source byte memory view. + /// A new instance of wrapping . + /// The backing data of is not expected to be mutated once the cast is done. + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public static implicit operator ReadOnlySeExpression( Memory< byte > s ) => new( s ); + + /// Creates a new instance of struct from the given byte array. + /// The source byte array. + /// A new instance of wrapping . + /// is not expected to be mutated once the cast is done. + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public static implicit operator ReadOnlySeExpression( byte[] s ) => new( s ); + + /// Returns a value that indicates whether the specified expressions are equal. + /// The first expression to compare. + /// The second expression to compare. + /// if and are equal; otherwise, . + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public static bool operator ==( ReadOnlySeExpression left, ReadOnlySeExpression right ) => left.Equals( right ); + + /// Returns a value that indicates whether the specified expressions are not equal. + /// The first expression to compare. + /// The second expression to compare. + /// if and are not equal; otherwise, . + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public static bool operator !=( ReadOnlySeExpression left, ReadOnlySeExpression right ) => !left.Equals( right ); + + /// Gets a from this . + /// A new instance of . + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public ReadOnlySeExpressionSpan AsSpan() => new( Body.Span ); + + /// Attempts to get an integer value from this expression. + /// The parsed integer value. + /// true if successful. + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public bool TryGetUInt( out uint value ) => SeExpressionUtilities.TryDecodeUInt( Body.Span, out value, out _ ); + + /// + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public bool TryGetInt( out int value ) => SeExpressionUtilities.TryDecodeInt( Body.Span, out value, out _ ); + + /// Attempts to get a SeString value from this expression. + /// The parsed value. + /// true if successful. + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public bool TryGetString( out ReadOnlySeString value ) => SeExpressionUtilities.TryDecodeString( Body, out value, out _ ); + + /// Attempts to get a placeholder type from this expression. + /// The parsed expression type. + /// true if successful. + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public bool TryGetPlaceholderExpression( out byte expressionType ) => SeExpressionUtilities.TryDecodeNullary( Body, out expressionType, out _ ); + + /// Attempts to get a paramter expression from this expression. + /// The parsed expression type. + /// The operand. + /// true if successful. + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public bool TryGetParameterExpression( out byte expressionType, out ReadOnlySeExpression operand ) => + SeExpressionUtilities.TryDecodeUnary( Body, out expressionType, out operand, out _ ); + + /// Attempts to get a binary expression from this expression. + /// The parsed expression type. + /// The operand 1. + /// The operand 2. + /// true if successful. + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public bool TryGetBinaryExpression( out byte expressionType, out ReadOnlySeExpression operand1, out ReadOnlySeExpression operand2 ) => + SeExpressionUtilities.TryDecodeBinary( Body, out expressionType, out operand1, out operand2, out _ ); + + /// + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public bool Validate() => AsSpan().Validate(); + + /// + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public bool Equals( ReadOnlySeExpression other ) => AsSpan().Equals( other.AsSpan() ); + + /// + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public override bool Equals( object? obj ) => obj is ReadOnlySeExpression other && Equals( other ); + + /// + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public override int GetHashCode() => AsSpan().GetHashCode(); + + /// + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public override string ToString() => AsSpan().ToString(); +} \ No newline at end of file diff --git a/src/Lumina/Text/ReadOnly/ReadOnlySeExpressionSpan.cs b/src/Lumina/Text/ReadOnly/ReadOnlySeExpressionSpan.cs new file mode 100644 index 00000000..088dc64c --- /dev/null +++ b/src/Lumina/Text/ReadOnly/ReadOnlySeExpressionSpan.cs @@ -0,0 +1,221 @@ +using System; +using System.Runtime.CompilerServices; +using System.Text; +using Lumina.Text.Expressions; + +namespace Lumina.Text.ReadOnly; + +/// Represents an expression in a . +public readonly ref struct ReadOnlySeExpressionSpan +{ + /// Read-only byte data for the SeString expression. + public readonly ReadOnlySpan< byte > Body; + + /// Initializes a new instance of the struct. + /// Raw SeString expression data. + public ReadOnlySeExpressionSpan( ReadOnlySpan< byte > body ) => Body = body; + + /// Gets the read-only byte memory view of this instance of . + /// The expression to get a byte memory view of. + /// The read-only byte memory view of . + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public static implicit operator ReadOnlySpan< byte >( ReadOnlySeExpressionSpan s ) => s.Body; + + /// Creates a new instance of struct from the given byte span view. + /// The source byte memory view. + /// A new instance of wrapping . + /// The backing data of is not expected to be mutated once the cast is done. + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public static implicit operator ReadOnlySeExpressionSpan( ReadOnlySpan< byte > s ) => new( s ); + + /// Creates a new instance of struct from the given byte span view. + /// The source byte memory view. + /// A new instance of wrapping . + /// The backing data of is not expected to be mutated once the cast is done. + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public static implicit operator ReadOnlySeExpressionSpan( Span< byte > s ) => new( s ); + + /// Gets the read-only byte span view of this instance of . + /// The expression to get a byte span view of. + /// The read-only byte span view of . + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public static implicit operator ReadOnlySeExpressionSpan( ReadOnlySeExpression s ) => s.AsSpan(); + + /// Creates a new instance of struct from the given byte memory view. + /// The source byte memory view. + /// A new instance of wrapping . + /// The backing data of is not expected to be mutated once the cast is done. + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public static implicit operator ReadOnlySeExpressionSpan( ReadOnlyMemory< byte > s ) => new( s.Span ); + + /// Creates a new instance of struct from the given byte memory view. + /// The source byte memory view. + /// A new instance of wrapping . + /// The backing data of is not expected to be mutated once the cast is done. + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public static implicit operator ReadOnlySeExpressionSpan( Memory< byte > s ) => new( s.Span ); + + /// Creates a new instance of struct from the given byte array. + /// The source byte array. + /// A new instance of wrapping . + /// is not expected to be mutated once the cast is done. + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public static implicit operator ReadOnlySeExpressionSpan( byte[] s ) => new( s ); + + /// Returns a value that indicates whether the specified expressions are equal. + /// The first expression to compare. + /// The second expression to compare. + /// if and are equal; otherwise, . + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public static bool operator ==( ReadOnlySeExpressionSpan left, ReadOnlySeExpressionSpan right ) => left.Equals( right ); + + /// Returns a value that indicates whether the specified expressions are equal. + /// The first expression to compare. + /// The second expression to compare. + /// if and are equal; otherwise, . + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public static bool operator ==( ReadOnlySeExpressionSpan left, ReadOnlySeExpression right ) => left.Equals( right.AsSpan() ); + + /// Returns a value that indicates whether the specified expressions are equal. + /// The first expression to compare. + /// The second expression to compare. + /// if and are equal; otherwise, . + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public static bool operator ==( ReadOnlySeExpression left, ReadOnlySeExpressionSpan right ) => left.AsSpan().Equals( right ); + + /// Returns a value that indicates whether the specified expressions are not equal. + /// The first expression to compare. + /// The second expression to compare. + /// if and are not equal; otherwise, . + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public static bool operator !=( ReadOnlySeExpressionSpan left, ReadOnlySeExpressionSpan right ) => !left.Equals( right ); + + /// Returns a value that indicates whether the specified expressions are not equal. + /// The first expression to compare. + /// The second expression to compare. + /// if and are not equal; otherwise, . + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public static bool operator !=( ReadOnlySeExpressionSpan left, ReadOnlySeExpression right ) => !left.Equals( right.AsSpan() ); + + /// Returns a value that indicates whether the specified expressions are not equal. + /// The first expression to compare. + /// The second expression to compare. + /// if and are not equal; otherwise, . + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public static bool operator !=( ReadOnlySeExpression left, ReadOnlySeExpressionSpan right ) => !left.AsSpan().Equals( right ); + + /// Attempts to get an integer value from this expression. + /// The parsed integer value. + /// true if successful. + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public bool TryGetUInt( out uint value ) => SeExpressionUtilities.TryDecodeUInt( Body, out value, out _ ); + + /// + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public bool TryGetInt( out int value ) => SeExpressionUtilities.TryDecodeInt( Body, out value, out _ ); + + /// Attempts to get a SeString value from this expression. + /// The parsed value. + /// true if successful. + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public bool TryGetString( out ReadOnlySeStringSpan value ) => SeExpressionUtilities.TryDecodeString( Body, out value, out _ ); + + /// Attempts to get a placeholder type from this expression. + /// The parsed expression type. + /// true if successful. + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public bool TryGetPlaceholderExpression( out byte expressionType ) => SeExpressionUtilities.TryDecodeNullary( Body, out expressionType, out _ ); + + /// Attempts to get a paramter expression from this expression. + /// The parsed expression type. + /// The operand. + /// true if successful. + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public bool TryGetParameterExpression( out byte expressionType, out ReadOnlySeExpressionSpan operand ) => + SeExpressionUtilities.TryDecodeUnary( Body, out expressionType, out operand, out _ ); + + /// Attempts to get a binary expression from this expression. + /// The parsed expression type. + /// The operand 1. + /// The operand 2. + /// true if successful. + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public bool TryGetBinaryExpression( out byte expressionType, out ReadOnlySeExpressionSpan operand1, out ReadOnlySeExpressionSpan operand2 ) => + SeExpressionUtilities.TryDecodeBinary( Body, out expressionType, out operand1, out operand2, out _ ); + + /// Validates whether this instance of is well-formed. + /// true if this instance of is structurally valid. + public bool Validate() + { + if( TryGetInt( out _ ) ) + return true; + if( TryGetString( out var s ) ) + return s.Validate(); + if( TryGetPlaceholderExpression( out _ ) ) + return true; + if( TryGetParameterExpression( out _, out var e1 ) ) + return e1.Validate(); + if( TryGetBinaryExpression( out _, out e1, out var e2 ) ) + return e1.Validate() && e2.Validate(); + return false; + } + + /// + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public bool Equals( ReadOnlySeExpressionSpan other ) => Body.SequenceEqual( other.Body ); + + /// + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public override bool Equals( object? obj ) => false; + + /// + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public override int GetHashCode() + { + var hc = default( HashCode ); + hc.AddBytes( Body ); + return hc.ToHashCode(); + } + + /// + public override string ToString() + { + if( Body.IsEmpty ) + return "(?)"; + + if( TryGetUInt( out var u32 ) ) + return u32.ToString(); + + if( TryGetString( out var s ) ) + return $"\"{s.ToString().Replace( "\\", "\\\\" ).Replace( "\"", "\\\"" )}\""; + + if( TryGetPlaceholderExpression( out var exprType ) ) + { + if( ( (ExpressionType) exprType ).GetNativeName() is { } nativeName ) + return nativeName; + return $"?x{exprType:X02}"; + } + + if( TryGetParameterExpression( out exprType, out var e1 ) ) + { + if( ( (ExpressionType) exprType ).GetNativeName() is { } nativeName ) + return $"{nativeName}({e1.ToString()})"; + throw new InvalidOperationException( "All native names must be defined for unary expressions." ); + } + + if( TryGetBinaryExpression( out exprType, out e1, out var e2 ) ) + { + if( ( (ExpressionType) exprType ).GetNativeName() is { } nativeName ) + return $"{e1.ToString()} {nativeName} {e2.ToString()}"; + throw new InvalidOperationException( "All native names must be defined for binary expressions." ); + } + + var sb = new StringBuilder(); + sb.EnsureCapacity( 1 + 3 * Body.Length ); + sb.Append( $"({Body[ 0 ]:X02}" ); + for( var i = 1; i < Body.Length; i++ ) + sb.Append( $" {Body[ i ]:X02}" ); + sb.Append( ')' ); + return sb.ToString(); + } +} \ No newline at end of file diff --git a/src/Lumina/Text/ReadOnly/ReadOnlySePayload.cs b/src/Lumina/Text/ReadOnly/ReadOnlySePayload.cs new file mode 100644 index 00000000..722c95d9 --- /dev/null +++ b/src/Lumina/Text/ReadOnly/ReadOnlySePayload.cs @@ -0,0 +1,534 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Text; +using Lumina.Text.Payloads; + +namespace Lumina.Text.ReadOnly; + +/// Represents a payload in a . +[SuppressMessage( "ReSharper", "ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator", Justification = "Avoid heap allocation" )] +public readonly struct ReadOnlySePayload : IEquatable< ReadOnlySePayload >, IReadOnlyCollection< ReadOnlySeExpression >, ICollection< ReadOnlySeExpression >, + ICollection +{ + /// Type of this payload. + public readonly ReadOnlySePayloadType Type; + + /// Type of the macro contained within, if is . + public readonly MacroCode MacroCode; + + /// Payload body, excluding envelope head and tail if . + public readonly ReadOnlyMemory< byte > Body; + + /// Initializes a new instance of the struct. + /// Type of this payload. + /// Macro code for this payload. + /// Raw SeString payload data. + /// No further mutations to the underlying data behind is expected. + public ReadOnlySePayload( ReadOnlySePayloadType type, MacroCode macroCode, ReadOnlyMemory< byte > body ) + { + switch( type ) + { + case ReadOnlySePayloadType.Invalid: + if( macroCode != default ) + throw new ArgumentOutOfRangeException( nameof( macroCode ), macroCode, "MacroCode may not be defined if the payload is of invalid type." ); + break; + case ReadOnlySePayloadType.Text: + if( macroCode != default ) + throw new ArgumentOutOfRangeException( nameof( macroCode ), macroCode, "MacroCode may not be defined if the payload is of text type." ); + break; + case ReadOnlySePayloadType.Macro: + if( !Enum.IsDefined( macroCode ) || macroCode == 0 ) + throw new ArgumentOutOfRangeException( nameof( macroCode ), macroCode, null ); + break; + default: + throw new ArgumentOutOfRangeException( nameof( type ), type, null ); + } + + Type = type; + MacroCode = macroCode; + Body = body; + } + + /// Gets the number of bytes, including the envelope head and tail. + public int EnvelopeByteLength { + [MethodImpl( MethodImplOptions.AggressiveInlining )] + get { + return Type switch + { + ReadOnlySePayloadType.Invalid => Body.Length, + ReadOnlySePayloadType.Text => Body.Length, + ReadOnlySePayloadType.Macro => 2 + SeExpressionUtilities.CalculateLengthInt( Body.Length ) + Body.Length + 1, + _ => Body.Length, + }; + } + } + + /// Gets the number of direct child expressions contained within this payload. + public int ExpressionCount { + [MethodImpl( MethodImplOptions.AggressiveInlining )] + get { + if( Type != ReadOnlySePayloadType.Macro ) + return 0; + var res = 0; + foreach( var _ in this ) + res++; + return res; + } + } + + /// + int IReadOnlyCollection< ReadOnlySeExpression >.Count => ExpressionCount; + + /// + int ICollection.Count => ExpressionCount; + + /// + int ICollection< ReadOnlySeExpression >.Count => ExpressionCount; + + /// + bool ICollection.IsSynchronized => true; + + /// + object ICollection.SyncRoot => new(); + + /// + bool ICollection< ReadOnlySeExpression >.IsReadOnly => true; + + /// Returns a value that indicates whether the specified payloads are equal. + /// The first payload to compare. + /// The second payload to compare. + /// if and are equal; otherwise, . + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public static bool operator ==( ReadOnlySePayload left, ReadOnlySePayload right ) => left.Equals( right ); + + /// Returns a value that indicates whether the specified payloads are not equal. + /// The first payload to compare. + /// The second payload to compare. + /// if and are not equal; otherwise, . + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public static bool operator !=( ReadOnlySePayload left, ReadOnlySePayload right ) => !left.Equals( right ); + + /// Returns a concatenated instance of . + /// The payload that comes first. + /// The payload that comes second. + /// The concatenated string. + public static ReadOnlySeString operator +( ReadOnlySePayload left, ReadOnlySePayload right ) + { + var res = new byte[left.EnvelopeByteLength + right.EnvelopeByteLength]; + left.WriteEnvelopeTo( res ); + right.WriteEnvelopeTo( res.AsSpan( left.EnvelopeByteLength ) ); + return new( res ); + } + + /// Returns a concatenated instance of . + /// The string that comes first. + /// The payload that comes second. + /// The concatenated string. + public static ReadOnlySeString operator +( ReadOnlySeString left, ReadOnlySePayload right ) + { + var res = new byte[left.ByteLength + right.EnvelopeByteLength]; + left.Data.CopyTo( res ); + right.WriteEnvelopeTo( res.AsSpan( left.ByteLength ) ); + return new( res ); + } + + /// Returns a concatenated instance of . + /// The payload that comes first. + /// The string that comes second. + /// The concatenated string. + public static ReadOnlySeString operator +( ReadOnlySePayload left, ReadOnlySeString right ) + { + var res = new byte[left.EnvelopeByteLength + right.ByteLength]; + left.WriteEnvelopeTo( res ); + right.Data.CopyTo( res.AsMemory( left.EnvelopeByteLength ) ); + return new( res ); + } + + /// Returns a concatenated instance of . + /// The string that comes first. + /// The payload that comes second. + /// The concatenated string. + public static ReadOnlySeString operator +( ReadOnlySpan< char > left, ReadOnlySePayload right ) + { + var lnb = Encoding.UTF8.GetByteCount( left ); + var res = new byte[lnb + right.EnvelopeByteLength]; + Encoding.UTF8.GetBytes( left, res ); + right.WriteEnvelopeTo( res.AsSpan( lnb ) ); + return new( res ); + } + + /// Returns a concatenated instance of . + /// The payload that comes first. + /// The string that comes second. + /// The concatenated string. + public static ReadOnlySeString operator +( ReadOnlySePayload left, ReadOnlySpan< char > right ) + { + var rnb = Encoding.UTF8.GetByteCount( right ); + var res = new byte[left.EnvelopeByteLength + rnb]; + left.WriteEnvelopeTo( res ); + Encoding.UTF8.GetBytes( right, res.AsSpan( left.EnvelopeByteLength ) ); + return new( res ); + } + + /// Returns a concatenated instance of . + /// The string that comes first. + /// The payload that comes second. + /// The concatenated string. + public static ReadOnlySeString operator +( ReadOnlySeStringSpan left, ReadOnlySePayload right ) + { + var res = new byte[left.ByteLength + right.EnvelopeByteLength]; + left.Data.CopyTo( res ); + right.WriteEnvelopeTo( res.AsSpan( left.ByteLength ) ); + return new( res ); + } + + /// Returns a concatenated instance of . + /// The payload that comes first. + /// The string that comes second. + /// The concatenated string. + public static ReadOnlySeString operator +( ReadOnlySePayload left, ReadOnlySeStringSpan right ) + { + var res = new byte[left.EnvelopeByteLength + right.ByteLength]; + left.WriteEnvelopeTo( res ); + right.Data.CopyTo( res.AsSpan( left.EnvelopeByteLength ) ); + return new( res ); + } + + /// Creates a new instance of from a given UTF-8 string. + /// The UTF-8 string. + /// A new instance of . + public static ReadOnlySePayload FromText( ReadOnlySpan< byte > utf8String ) + { + if( utf8String.IndexOfAny( SeString.StartByte, (byte) 0 ) != -1 ) + throw new ArgumentException( "A SeString cannot contain STX or NUL.", nameof( utf8String ) ); + return new( ReadOnlySePayloadType.Text, default, utf8String.ToArray() ); + } + + /// Creates a new instance of from a given UTF-8 string. + /// The UTF-8 string. + /// A new instance of . + public static ReadOnlySePayload FromText( ReadOnlyMemory< byte > utf8String ) + { + if( utf8String.Span.IndexOfAny( SeString.StartByte, (byte) 0 ) != -1 ) + throw new ArgumentException( "A SeString cannot contain STX or NUL.", nameof( utf8String ) ); + return new( ReadOnlySePayloadType.Text, default, utf8String ); + } + + /// Creates a new instance of from a given UTF-16 string. + /// The UTF-16 string. + /// A new instance of . + public static ReadOnlySePayloadSpan FromText( ReadOnlySpan< char > utf16String ) + { + if( utf16String.IndexOfAny( (char) SeString.StartByte, (char) 0 ) != -1 ) + throw new ArgumentException( "A SeString cannot contain STX or NUL.", nameof( utf16String ) ); + var buf = new byte[Encoding.UTF8.GetByteCount( utf16String )]; + Encoding.UTF8.GetBytes( utf16String, buf ); + return new( ReadOnlySePayloadType.Text, default, buf ); + } + + /// Gets a from this . + /// A new instance of . + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public ReadOnlySePayloadSpan AsSpan() => new( Type, MacroCode, Body.Span ); + + /// Gets the expression forming this payload. + /// The resolved expression. + /// true if the expression is resolved. + public bool TryGetExpression( out ReadOnlySeExpression expr1 ) + { + expr1 = default; + + using var enu = GetEnumerator(); + if( !enu.MoveNext() ) + return false; + expr1 = enu.Current; + return true; + } + + /// Gets the expressions forming this payload. + /// The resolved expression 1. + /// The resolved expression 2. + /// true if all expressions are resolved. + public bool TryGetExpression( out ReadOnlySeExpression expr1, out ReadOnlySeExpression expr2 ) + { + expr1 = expr2 = default; + + using var enu = GetEnumerator(); + if( !enu.MoveNext() ) + return false; + expr1 = enu.Current; + if( !enu.MoveNext() ) + return false; + expr2 = enu.Current; + return true; + } + + /// Gets the expressions forming this payload. + /// The resolved expression 1. + /// The resolved expression 2. + /// The resolved expression 3. + /// true if all expressions are resolved. + public bool TryGetExpression( + out ReadOnlySeExpression expr1, + out ReadOnlySeExpression expr2, + out ReadOnlySeExpression expr3 ) + { + expr1 = expr2 = expr3 = default; + + using var enu = GetEnumerator(); + if( !enu.MoveNext() ) + return false; + expr1 = enu.Current; + if( !enu.MoveNext() ) + return false; + expr2 = enu.Current; + if( !enu.MoveNext() ) + return false; + expr3 = enu.Current; + return true; + } + + /// Gets the expressions forming this payload. + /// The resolved expression 1. + /// The resolved expression 2. + /// The resolved expression 3. + /// The resolved expression 4. + /// true if all expressions are resolved. + public bool TryGetExpression( + out ReadOnlySeExpression expr1, + out ReadOnlySeExpression expr2, + out ReadOnlySeExpression expr3, + out ReadOnlySeExpression expr4 ) + { + expr1 = expr2 = expr3 = expr4 = default; + + using var enu = GetEnumerator(); + if( !enu.MoveNext() ) + return false; + expr1 = enu.Current; + if( !enu.MoveNext() ) + return false; + expr2 = enu.Current; + if( !enu.MoveNext() ) + return false; + expr3 = enu.Current; + if( !enu.MoveNext() ) + return false; + expr4 = enu.Current; + return true; + } + + /// Gets the expressions forming this payload. + /// The resolved expression 1. + /// The resolved expression 2. + /// The resolved expression 3. + /// The resolved expression 4. + /// The resolved expression 5. + /// true if all expressions are resolved. + public bool TryGetExpression( + out ReadOnlySeExpression expr1, + out ReadOnlySeExpression expr2, + out ReadOnlySeExpression expr3, + out ReadOnlySeExpression expr4, + out ReadOnlySeExpression expr5 ) + { + expr1 = expr2 = expr3 = expr4 = expr5 = default; + + using var enu = GetEnumerator(); + if( !enu.MoveNext() ) + return false; + expr1 = enu.Current; + if( !enu.MoveNext() ) + return false; + expr2 = enu.Current; + if( !enu.MoveNext() ) + return false; + expr3 = enu.Current; + if( !enu.MoveNext() ) + return false; + expr4 = enu.Current; + if( !enu.MoveNext() ) + return false; + expr5 = enu.Current; + return true; + } + + /// + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public bool Validate() => AsSpan().Validate(); + + /// + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public int WriteEnvelopeTo( Span< byte > target ) => AsSpan().WriteEnvelopeTo( target ); + + /// + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public bool Equals( ReadOnlySePayload other ) => AsSpan().Equals( other.AsSpan() ); + + /// + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public override bool Equals( object? obj ) => obj is ReadOnlySePayload other && Equals( other ); + + /// + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public override int GetHashCode() => AsSpan().GetHashCode(); + + /// + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public override string ToString() => AsSpan().ToString(); + + /// + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public Enumerator GetEnumerator() => new( new( this ) ); + + /// + IEnumerator< ReadOnlySeExpression > IEnumerable< ReadOnlySeExpression >.GetEnumerator() => GetEnumerator(); + + /// + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + /// Gets an enumerator that enumerates expressions with their offsets. + /// An enumerator that enumerates expressions with their offsets. + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public OffsetEnumerator GetOffsetEnumerator() => new( this ); + + /// + public bool Contains( ReadOnlySeExpression item ) + { + foreach( var e in this ) + { + if( e == item ) + return true; + } + + return false; + } + + /// + public void CopyTo( ReadOnlySeExpression[] array, int arrayIndex ) + { + if( array is null ) + throw new ArgumentNullException( nameof( array ) ); + if( arrayIndex < 0 ) + throw new ArgumentOutOfRangeException( nameof( arrayIndex ), arrayIndex, null ); + if( arrayIndex + ExpressionCount > array.Length ) + throw new ArgumentException( "The number of expressions is greater than the available space.", nameof( array ) ); + foreach( var p in this ) + array[ arrayIndex++ ] = p; + } + + /// + void ICollection< ReadOnlySeExpression >.Add( ReadOnlySeExpression item ) => throw new NotSupportedException(); + + /// + void ICollection< ReadOnlySeExpression >.Clear() => throw new NotSupportedException(); + + /// + bool ICollection< ReadOnlySeExpression >.Remove( ReadOnlySeExpression item ) => throw new NotSupportedException(); + + /// + void ICollection.CopyTo( Array? array, int index ) => CopyTo( ( array as ReadOnlySeExpression[] ) switch + { + { } a => a, + null when array is not null => throw new ArrayTypeMismatchException(), + null => throw new ArgumentNullException( nameof( array ) ), + }, index ); + + /// Enumerator for enumerating a by expressions with offsets. + public struct OffsetEnumerator : IEnumerator< (int Offset, ReadOnlySeExpression Expression) >, IEnumerable< (int Offset, ReadOnlySeExpression Expression) > + { + private readonly ReadOnlySePayload _obj; + private int _nextIndex; + + /// Initializes a new instance of the struct. + /// An instance of to enumerate. + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public OffsetEnumerator( ReadOnlySePayload obj ) => _obj = obj; + + /// + public (int Offset, ReadOnlySeExpression Expression) Current { get; private set; } + + /// + object IEnumerator.Current => Current; + + /// + public bool MoveNext() + { + if( _nextIndex >= _obj.Body.Length ) + return false; + + var submemory = _obj.Body[ _nextIndex.. ]; + if( !SeExpressionUtilities.TryDecodeLength( submemory, out var length ) ) + length = 1; + + Current = ( _nextIndex, new( submemory[ ..length ] ) ); + _nextIndex += length; + return true; + } + + /// + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public void Reset() => _nextIndex = 0; + + /// + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public void Dispose() + { } + + /// + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public OffsetEnumerator GetEnumerator() => new( _obj ); + + /// + IEnumerator< (int Offset, ReadOnlySeExpression Expression) > IEnumerable< (int Offset, ReadOnlySeExpression Expression) >.GetEnumerator() => + GetEnumerator(); + + /// + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } + + /// Enumerator for enumerating a by payloads. + public struct Enumerator : IEnumerator< ReadOnlySeExpression >, IEnumerable< ReadOnlySeExpression > + { + private OffsetEnumerator _offsetEnumerator; + + /// Initializes a new instance of the struct. + /// An instance of . + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public Enumerator( OffsetEnumerator offsetEnumerator ) => _offsetEnumerator = offsetEnumerator; + + /// + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public bool MoveNext() => _offsetEnumerator.MoveNext(); + + /// + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public void Reset() => _offsetEnumerator.Reset(); + + /// + public ReadOnlySeExpression Current { + [MethodImpl( MethodImplOptions.AggressiveInlining )] + get => _offsetEnumerator.Current.Expression; + } + + /// + object IEnumerator.Current => _offsetEnumerator.Current.Expression; + + /// + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public void Dispose() => _offsetEnumerator.Dispose(); + + /// + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public Enumerator GetEnumerator() => new( _offsetEnumerator ); + + /// + IEnumerator< ReadOnlySeExpression > IEnumerable< ReadOnlySeExpression >.GetEnumerator() => GetEnumerator(); + + /// + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +} \ No newline at end of file diff --git a/src/Lumina/Text/ReadOnly/ReadOnlySePayloadSpan.cs b/src/Lumina/Text/ReadOnly/ReadOnlySePayloadSpan.cs new file mode 100644 index 00000000..4a016952 --- /dev/null +++ b/src/Lumina/Text/ReadOnly/ReadOnlySePayloadSpan.cs @@ -0,0 +1,561 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Text; +using Lumina.Text.Payloads; + +namespace Lumina.Text.ReadOnly; + +/// Represents a payload in a . +[SuppressMessage( "ReSharper", "ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator", Justification = "Avoid heap allocation" )] +public readonly ref struct ReadOnlySePayloadSpan +{ + /// Type of this payload. + public readonly ReadOnlySePayloadType Type; + + /// Type of the macro contained within, if is . + public readonly MacroCode MacroCode; + + /// Payload body, excluding envelope head and tail if . + public readonly ReadOnlySpan< byte > Body; + + /// Initializes a new instance of the struct. + /// Type of this payload. + /// Macro code for this payload. + /// Raw SeString payload data. + /// No further mutations to the underlying data behind is expected. + public ReadOnlySePayloadSpan( ReadOnlySePayloadType type, MacroCode macroCode, ReadOnlySpan< byte > body ) + { + switch( type ) + { + case ReadOnlySePayloadType.Invalid: + if( macroCode != default ) + throw new ArgumentOutOfRangeException( nameof( macroCode ), macroCode, "MacroCode may not be defined if the payload is of invalid type." ); + break; + case ReadOnlySePayloadType.Text: + if( macroCode != default ) + throw new ArgumentOutOfRangeException( nameof( macroCode ), macroCode, "MacroCode may not be defined if the payload is of text type." ); + break; + case ReadOnlySePayloadType.Macro: + if( !Enum.IsDefined( macroCode ) || macroCode == 0 ) + throw new ArgumentOutOfRangeException( nameof( macroCode ), macroCode, null ); + break; + default: + throw new ArgumentOutOfRangeException( nameof( type ), type, null ); + } + + Type = type; + MacroCode = macroCode; + Body = body; + } + + /// Gets the number of bytes, including the envelope head and tail. + public int EnvelopeByteLength { + [MethodImpl( MethodImplOptions.AggressiveInlining )] + get { + return Type switch + { + ReadOnlySePayloadType.Invalid => Body.Length, + ReadOnlySePayloadType.Text => Body.Length, + ReadOnlySePayloadType.Macro => 2 + SeExpressionUtilities.CalculateLengthInt( Body.Length ) + Body.Length + 1, + _ => Body.Length, + }; + } + } + + /// Gets the number of direct child expressions contained within this payload. + public int ExpressionCount { + [MethodImpl( MethodImplOptions.AggressiveInlining )] + get { + if( Type != ReadOnlySePayloadType.Macro ) + return 0; + var res = 0; + foreach( var _ in this ) + res++; + return res; + } + } + + /// Returns a value that indicates whether the specified payloads are equal. + /// The first payload to compare. + /// The second payload to compare. + /// if and are equal; otherwise, . + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public static bool operator ==( ReadOnlySePayloadSpan left, ReadOnlySePayloadSpan right ) => left.Equals( right ); + + /// Returns a value that indicates whether the specified payloads are equal. + /// The first payload to compare. + /// The second payload to compare. + /// if and are equal; otherwise, . + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public static bool operator ==( ReadOnlySePayloadSpan left, ReadOnlySePayload right ) => left.Equals( right.AsSpan() ); + + /// Returns a value that indicates whether the specified payloads are equal. + /// The first payload to compare. + /// The second payload to compare. + /// if and are equal; otherwise, . + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public static bool operator ==( ReadOnlySePayload left, ReadOnlySePayloadSpan right ) => left.AsSpan().Equals( right ); + + /// Returns a value that indicates whether the specified payloads are not equal. + /// The first payload to compare. + /// The second payload to compare. + /// if and are not equal; otherwise, . + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public static bool operator !=( ReadOnlySePayloadSpan left, ReadOnlySePayloadSpan right ) => !left.Equals( right ); + + /// Returns a value that indicates whether the specified payloads are not equal. + /// The first payload to compare. + /// The second payload to compare. + /// if and are not equal; otherwise, . + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public static bool operator !=( ReadOnlySePayloadSpan left, ReadOnlySePayload right ) => !left.Equals( right.AsSpan() ); + + /// Returns a value that indicates whether the specified payloads are not equal. + /// The first payload to compare. + /// The second payload to compare. + /// if and are not equal; otherwise, . + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public static bool operator !=( ReadOnlySePayload left, ReadOnlySePayloadSpan right ) => !left.AsSpan().Equals( right ); + + /// Returns a concatenated instance of . + /// The payload that comes first. + /// The payload that comes second. + /// The concatenated string. + public static ReadOnlySeString operator +( ReadOnlySePayloadSpan left, ReadOnlySePayloadSpan right ) + { + var res = new byte[left.EnvelopeByteLength + right.EnvelopeByteLength]; + left.WriteEnvelopeTo( res ); + right.WriteEnvelopeTo( res.AsSpan( left.EnvelopeByteLength ) ); + return new( res ); + } + + /// Returns a concatenated instance of . + /// The string that comes first. + /// The payload that comes second. + /// The concatenated string. + public static ReadOnlySeString operator +( ReadOnlySeString left, ReadOnlySePayloadSpan right ) + { + var res = new byte[left.ByteLength + right.EnvelopeByteLength]; + left.Data.CopyTo( res ); + right.WriteEnvelopeTo( res.AsSpan( left.ByteLength ) ); + return new( res ); + } + + /// Returns a concatenated instance of . + /// The payload that comes first. + /// The string that comes second. + /// The concatenated string. + public static ReadOnlySeString operator +( ReadOnlySePayloadSpan left, ReadOnlySeString right ) + { + var res = new byte[left.EnvelopeByteLength + right.ByteLength]; + left.WriteEnvelopeTo( res ); + right.Data.CopyTo( res.AsMemory( left.EnvelopeByteLength ) ); + return new( res ); + } + + /// Returns a concatenated instance of . + /// The string that comes first. + /// The payload that comes second. + /// The concatenated string. + public static ReadOnlySeString operator +( ReadOnlySpan< char > left, ReadOnlySePayloadSpan right ) + { + var lnb = Encoding.UTF8.GetByteCount( left ); + var res = new byte[lnb + right.EnvelopeByteLength]; + Encoding.UTF8.GetBytes( left, res ); + right.WriteEnvelopeTo( res.AsSpan( lnb ) ); + return new( res ); + } + + /// Returns a concatenated instance of . + /// The payload that comes first. + /// The string that comes second. + /// The concatenated string. + public static ReadOnlySeString operator +( ReadOnlySePayloadSpan left, ReadOnlySpan< char > right ) + { + var rnb = Encoding.UTF8.GetByteCount( right ); + var res = new byte[left.EnvelopeByteLength + rnb]; + left.WriteEnvelopeTo( res ); + Encoding.UTF8.GetBytes( right, res.AsSpan( left.EnvelopeByteLength ) ); + return new( res ); + } + + /// Returns a concatenated instance of . + /// The string that comes first. + /// The payload that comes second. + /// The concatenated string. + public static ReadOnlySeString operator +( ReadOnlySeStringSpan left, ReadOnlySePayloadSpan right ) + { + var res = new byte[left.ByteLength + right.EnvelopeByteLength]; + left.Data.CopyTo( res ); + right.WriteEnvelopeTo( res.AsSpan( left.ByteLength ) ); + return new( res ); + } + + /// Returns a concatenated instance of . + /// The payload that comes first. + /// The string that comes second. + /// The concatenated string. + public static ReadOnlySeString operator +( ReadOnlySePayloadSpan left, ReadOnlySeStringSpan right ) + { + var res = new byte[left.EnvelopeByteLength + right.ByteLength]; + left.WriteEnvelopeTo( res ); + right.Data.CopyTo( res.AsSpan( left.EnvelopeByteLength ) ); + return new( res ); + } + + /// Creates a new instance of from a given UTF-8 string. + /// The UTF-8 string. + /// A new instance of . + public static ReadOnlySePayloadSpan FromText( ReadOnlySpan< byte > utf8String ) + { + if( utf8String.IndexOfAny( SeString.StartByte, (byte) 0 ) != -1 ) + throw new ArgumentException( "A SeString cannot contain STX or NUL.", nameof( utf8String ) ); + return new( ReadOnlySePayloadType.Text, default, utf8String ); + } + + /// Creates a new instance of from a given UTF-16 string. + /// The UTF-16 string. + /// A new instance of . + public static ReadOnlySePayloadSpan FromText( ReadOnlySpan< char > utf16String ) + { + if( utf16String.IndexOfAny( (char) SeString.StartByte, (char) 0 ) != -1 ) + throw new ArgumentException( "A SeString cannot contain STX or NUL.", nameof( utf16String ) ); + var buf = new byte[Encoding.UTF8.GetByteCount( utf16String )]; + Encoding.UTF8.GetBytes( utf16String, buf ); + return new( ReadOnlySePayloadType.Text, default, buf ); + } + + /// Gets the expression forming this payload. + /// The resolved expression. + /// true if the expression is resolved. + public bool TryGetExpression( out ReadOnlySeExpressionSpan expr1 ) + { + expr1 = default; + + var enu = GetEnumerator(); + if( !enu.MoveNext() ) + return false; + expr1 = enu.Current; + return true; + } + + /// Gets the expressions forming this payload. + /// The resolved expression 1. + /// The resolved expression 2. + /// true if all expressions are resolved. + public bool TryGetExpression( out ReadOnlySeExpressionSpan expr1, out ReadOnlySeExpressionSpan expr2 ) + { + expr1 = expr2 = default; + + var enu = GetEnumerator(); + if( !enu.MoveNext() ) + return false; + expr1 = enu.Current; + if( !enu.MoveNext() ) + return false; + expr2 = enu.Current; + return true; + } + + /// Gets the expressions forming this payload. + /// The resolved expression 1. + /// The resolved expression 2. + /// The resolved expression 3. + /// true if all expressions are resolved. + public bool TryGetExpression( + out ReadOnlySeExpressionSpan expr1, + out ReadOnlySeExpressionSpan expr2, + out ReadOnlySeExpressionSpan expr3 ) + { + expr1 = expr2 = expr3 = default; + + var enu = GetEnumerator(); + if( !enu.MoveNext() ) + return false; + expr1 = enu.Current; + if( !enu.MoveNext() ) + return false; + expr2 = enu.Current; + if( !enu.MoveNext() ) + return false; + expr3 = enu.Current; + return true; + } + + /// Gets the expressions forming this payload. + /// The resolved expression 1. + /// The resolved expression 2. + /// The resolved expression 3. + /// The resolved expression 4. + /// true if all expressions are resolved. + public bool TryGetExpression( + out ReadOnlySeExpressionSpan expr1, + out ReadOnlySeExpressionSpan expr2, + out ReadOnlySeExpressionSpan expr3, + out ReadOnlySeExpressionSpan expr4 ) + { + expr1 = expr2 = expr3 = expr4 = default; + + var enu = GetEnumerator(); + if( !enu.MoveNext() ) + return false; + expr1 = enu.Current; + if( !enu.MoveNext() ) + return false; + expr2 = enu.Current; + if( !enu.MoveNext() ) + return false; + expr3 = enu.Current; + if( !enu.MoveNext() ) + return false; + expr4 = enu.Current; + return true; + } + + /// Gets the expressions forming this payload. + /// The resolved expression 1. + /// The resolved expression 2. + /// The resolved expression 3. + /// The resolved expression 4. + /// The resolved expression 5. + /// true if all expressions are resolved. + public bool TryGetExpression( + out ReadOnlySeExpressionSpan expr1, + out ReadOnlySeExpressionSpan expr2, + out ReadOnlySeExpressionSpan expr3, + out ReadOnlySeExpressionSpan expr4, + out ReadOnlySeExpressionSpan expr5 ) + { + expr1 = expr2 = expr3 = expr4 = expr5 = default; + + var enu = GetEnumerator(); + if( !enu.MoveNext() ) + return false; + expr1 = enu.Current; + if( !enu.MoveNext() ) + return false; + expr2 = enu.Current; + if( !enu.MoveNext() ) + return false; + expr3 = enu.Current; + if( !enu.MoveNext() ) + return false; + expr4 = enu.Current; + if( !enu.MoveNext() ) + return false; + expr5 = enu.Current; + return true; + } + + /// Validates whether this instance of is well-formed. + /// true if this instance of is structurally valid. + public bool Validate() + { + switch( Type ) + { + case ReadOnlySePayloadType.Invalid: + default: + return false; + + case ReadOnlySePayloadType.Text: + // A text may not contain NUL or STX. + return Body.IndexOfAny( (byte) 0, (byte) 2 ) == -1; + + case ReadOnlySePayloadType.Macro: + foreach( var v in this ) + { + if( !v.Validate() ) + return false; + } + + return true; + } + } + + /// Writes the data in envelope to the given byte span. + /// The optional target that should be at least of size . + /// Number of bytes (that has to be) written. + public int WriteEnvelopeTo( Span< byte > target ) + { + switch( Type ) + { + case ReadOnlySePayloadType.Invalid: + case ReadOnlySePayloadType.Text: + default: + if( !target.IsEmpty ) + Body.CopyTo( target ); + return Body.Length; + case ReadOnlySePayloadType.Macro: + var len = 0; + len += SeExpressionUtilities.WriteRaw( target.IsEmpty ? target : target[ len.. ], SeString.StartByte ); + len += SeExpressionUtilities.WriteRaw( target.IsEmpty ? target : target[ len.. ], (byte) MacroCode ); + len += SeExpressionUtilities.EncodeInt( target.IsEmpty ? target : target[ len.. ], Body.Length ); + len += SeExpressionUtilities.WriteRaw( target.IsEmpty ? target : target[ len.. ], Body ); + len += SeExpressionUtilities.WriteRaw( target.IsEmpty ? target : target[ len.. ], SeString.EndByte ); + return len; + } + } + + /// + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public bool Equals( ReadOnlySePayloadSpan other ) => Type == other.Type && MacroCode == other.MacroCode && Body.SequenceEqual( other.Body ); + + /// + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public override bool Equals( object? obj ) => false; + + /// + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public override int GetHashCode() + { + var hc = default( HashCode ); + hc.Add( Type ); + hc.Add( MacroCode ); + hc.AddBytes( Body ); + return hc.ToHashCode(); + } + + /// + public override string ToString() + { + switch( Type ) + { + case ReadOnlySePayloadType.Text: + return Encoding.UTF8.GetString( Body ).Replace( "<", "\\<" ); + + case ReadOnlySePayloadType.Macro: + { + var sb = new StringBuilder( "<" ); + + // Not using ternary operator here, as it's better to let StringBuilder handle the string interpolation. + if( MacroCode.GetEncodeName() is { } encodeName ) + sb.Append( encodeName ); + else + sb.Append( $"x{(uint) MacroCode:X02}" ); + + var expre = GetEnumerator(); + if( expre.MoveNext() ) + { + sb.Append( '(' ); + sb.Append( expre.Current.ToString() ); + while( expre.MoveNext() ) + sb.Append( ", " ).Append( expre.Current.ToString() ); + sb.Append( ')' ); + } + + return sb.Append( '>' ).ToString(); + } + + case var _ when Body.Length == 0: + return ""; + + default: + { + var sb = new StringBuilder( 3 * Body.Length + 3 ); + sb.Append( "' ).ToString(); + } + } + } + + /// + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public Enumerator GetEnumerator() => new( new( this ) ); + + /// Gets an enumerator that enumerates expressions with their offsets. + /// An enumerator that enumerates expressions with their offsets. + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public OffsetEnumerator GetOffsetEnumerator() => new( this ); + + /// A pair of offset in and the corresponding instance of . + public readonly ref struct OffsetAndExpression + { + /// The offset in that is at. + public readonly int Offset; + + /// The expression. + public readonly ReadOnlySeExpressionSpan Expression; + + /// Initializes a new instance of the struct. + /// The offset. + /// The expression. + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public OffsetAndExpression( int offset, ReadOnlySeExpressionSpan expression ) + { + Offset = offset; + Expression = expression; + } + } + + /// Enumerator for enumerating a by expressions with offsets. + public ref struct OffsetEnumerator + { + private readonly ReadOnlySePayloadSpan _obj; + private int _nextIndex; + + /// Initializes a new instance of the struct. + /// An instance of to enumerate. + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public OffsetEnumerator( ReadOnlySePayloadSpan obj ) => _obj = obj; + + /// + public OffsetAndExpression Current { get; private set; } + + /// + public bool MoveNext() + { + if( _nextIndex >= _obj.Body.Length ) + return false; + + var submemory = _obj.Body[ _nextIndex.. ]; + if( !SeExpressionUtilities.TryDecodeLength( submemory, out var length ) ) + length = 1; + + Current = new( _nextIndex, new( submemory[ ..length ] ) ); + _nextIndex += length; + return true; + } + + /// + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public void Reset() => _nextIndex = 0; + + /// + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public OffsetEnumerator GetEnumerator() => new( _obj ); + } + + /// Enumerator for enumerating a by payloads. + public ref struct Enumerator + { + private OffsetEnumerator _offsetEnumerator; + + /// Initializes a new instance of the struct. + /// An instance of . + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public Enumerator( OffsetEnumerator offsetEnumerator ) => _offsetEnumerator = offsetEnumerator; + + /// + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public bool MoveNext() => _offsetEnumerator.MoveNext(); + + /// + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public void Reset() => _offsetEnumerator.Reset(); + + /// + public ReadOnlySeExpressionSpan Current { + [MethodImpl( MethodImplOptions.AggressiveInlining )] + get => _offsetEnumerator.Current.Expression; + } + + /// + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public Enumerator GetEnumerator() => new( _offsetEnumerator ); + } +} \ No newline at end of file diff --git a/src/Lumina/Text/ReadOnly/ReadOnlySePayloadType.cs b/src/Lumina/Text/ReadOnly/ReadOnlySePayloadType.cs new file mode 100644 index 00000000..fa415c2d --- /dev/null +++ b/src/Lumina/Text/ReadOnly/ReadOnlySePayloadType.cs @@ -0,0 +1,16 @@ +namespace Lumina.Text.ReadOnly; + +/// Possible types for a payload. +public enum ReadOnlySePayloadType +{ + /// Indicates that this payload is either invalid or empty. No field is valid. + Invalid = default, + + /// Indicates that this payload contains text. + /// is valid. + Text = 1, + + /// Indicates that this payload contains a macro and its arguments. + /// and is valid. + Macro = 2, +} \ No newline at end of file diff --git a/src/Lumina/Text/ReadOnly/ReadOnlySeString.cs b/src/Lumina/Text/ReadOnly/ReadOnlySeString.cs new file mode 100644 index 00000000..8a2e6770 --- /dev/null +++ b/src/Lumina/Text/ReadOnly/ReadOnlySeString.cs @@ -0,0 +1,389 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Text; +using Lumina.Text.Payloads; + +namespace Lumina.Text.ReadOnly; + +/// A -like immutable implementation of SeString. +[SuppressMessage( "ReSharper", "ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator", Justification = "Avoid heap allocation" )] +public readonly struct ReadOnlySeString : IEquatable< ReadOnlySeString >, IReadOnlyCollection< ReadOnlySePayload >, ICollection< ReadOnlySePayload >, + ICollection +{ + /// STX, which is used as a sentinel value in a SeString to signify that a macro payload will follow. + public const byte Stx = 2; + + /// ETX, which is used as a sentinel value in a SeString to signify that a macro payload will end. + public const byte Etx = 3; + + /// Read-only byte data for the SeString. + public readonly ReadOnlyMemory< byte > Data; + + /// Initializes a new instance of the struct. + /// Raw SeString data. + /// No further mutations to the underlying data behind is expected. + public ReadOnlySeString( ReadOnlyMemory< byte > data ) => Data = data; + + /// Gets a value indicating whether this is empty. + public bool IsEmpty { + [MethodImpl( MethodImplOptions.AggressiveInlining )] + get => Data.IsEmpty; + } + + /// Gets the number of bytes in this . + public int ByteLength { + [MethodImpl( MethodImplOptions.AggressiveInlining )] + get => Data.Length; + } + + /// Gets the number of direct child payloads in this . + public int PayloadCount { + [MethodImpl( MethodImplOptions.AggressiveInlining )] + get { + var res = 0; + foreach( var _ in this ) + res++; + return res; + } + } + + /// + int IReadOnlyCollection< ReadOnlySePayload >.Count => PayloadCount; + + /// + int ICollection.Count => PayloadCount; + + /// + int ICollection< ReadOnlySePayload >.Count => PayloadCount; + + /// + bool ICollection.IsSynchronized => true; + + /// + object ICollection.SyncRoot => new(); + + /// + bool ICollection< ReadOnlySePayload >.IsReadOnly => true; + + /// Gets the read-only byte memory view of this instance of . + /// The string to get a byte memory view of. + /// The read-only byte memory view of . + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public static implicit operator ReadOnlyMemory< byte >( ReadOnlySeString s ) => s.Data; + + /// Gets the read-only byte span view of this instance of . + /// The string to get a byte span view of. + /// The read-only byte span view of . + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public static implicit operator ReadOnlySpan< byte >( ReadOnlySeString s ) => s.Data.Span; + + /// Creates a new instance of struct from the given byte memory view. + /// The source byte memory view. + /// A new instance of wrapping . + /// The backing data of is not expected to be mutated once the cast is done. + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public static implicit operator ReadOnlySeString( ReadOnlyMemory< byte > s ) => new( s ); + + /// Creates a new instance of struct from the given byte memory view. + /// The source byte memory view. + /// A new instance of wrapping . + /// The backing data of is not expected to be mutated once the cast is done. + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public static implicit operator ReadOnlySeString( Memory< byte > s ) => new( s ); + + /// Creates a new instance of struct from the given byte array. + /// The source byte array. + /// A new instance of wrapping . + /// is not expected to be mutated once the cast is done. + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public static implicit operator ReadOnlySeString( byte[] s ) => new( s ); + + /// Returns a value that indicates whether the specified strings are equal. + /// The first string to compare. + /// The second string to compare. + /// if and are equal; otherwise, . + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public static bool operator ==( ReadOnlySeString left, ReadOnlySeString right ) => left.Equals( right ); + + /// Returns a value that indicates whether the specified strings are not equal. + /// The first string to compare. + /// The second string to compare. + /// if and are not equal; otherwise, . + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public static bool operator !=( ReadOnlySeString left, ReadOnlySeString right ) => !left.Equals( right ); + + /// Returns a concatenated instance of . + /// The string that comes first. + /// The string that comes second. + /// The concatenated string. + public static ReadOnlySeString operator +( ReadOnlySeString left, ReadOnlySeString right ) + { + var res = new byte[left.ByteLength + right.ByteLength]; + left.Data.CopyTo( res ); + right.Data.CopyTo( res.AsMemory( left.ByteLength ) ); + return new( res ); + } + + /// Returns a concatenated instance of . + /// The string that comes first. + /// The string that comes second. + /// The concatenated string. + public static ReadOnlySeString operator +( ReadOnlySeString left, ReadOnlySpan< char > right ) + { + var rnb = Encoding.UTF8.GetByteCount( right ); + var res = new byte[left.ByteLength + rnb]; + left.Data.CopyTo( res ); + Encoding.UTF8.GetBytes( right, res.AsSpan( left.Data.Length ) ); + return new( res ); + } + + /// Returns a concatenated instance of . + /// The string that comes first. + /// The string that comes second. + /// The concatenated string. + public static ReadOnlySeString operator +( ReadOnlySpan< char > left, ReadOnlySeString right ) + { + var lnb = Encoding.UTF8.GetByteCount( left ); + var res = new byte[lnb + right.ByteLength]; + Encoding.UTF8.GetBytes( left, res ); + right.Data.CopyTo( res.AsMemory( lnb ) ); + return new( res ); + } + + /// Gets a from this . + /// A new instance of . + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public ReadOnlySeStringSpan AsSpan() => new( Data.Span ); + + /// + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public bool Validate() => AsSpan().Validate(); + + /// + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public string ExtractText() => AsSpan().ExtractText(); + + /// + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public bool Equals( ReadOnlySeString other ) => AsSpan().Equals( other.AsSpan() ); + + /// + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public override bool Equals( object? obj ) => obj is ReadOnlySeString other && Equals( other ); + + /// + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public override int GetHashCode() => AsSpan().GetHashCode(); + + /// Gets the encodeable macro representation of this instance of . + /// The encodeable macro representation. + public override string ToString() => AsSpan().ToString(); + + /// + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public Enumerator GetEnumerator() => new( new( this ) ); + + /// + IEnumerator< ReadOnlySePayload > IEnumerable< ReadOnlySePayload >.GetEnumerator() => GetEnumerator(); + + /// + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + /// Gets an enumerator that enumerates payloads with their offsets. + /// An enumerator that enumerates payloads with their offsets. + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public OffsetEnumerator GetOffsetEnumerator() => new( this ); + + /// + public bool Contains( ReadOnlySePayload item ) + { + foreach( var e in this ) + { + if( e == item ) + return true; + } + + return false; + } + + /// + public void CopyTo( ReadOnlySePayload[] array, int arrayIndex ) + { + if( array is null ) + throw new ArgumentNullException( nameof( array ) ); + if( arrayIndex < 0 ) + throw new ArgumentOutOfRangeException( nameof( arrayIndex ), arrayIndex, null ); + if( arrayIndex + PayloadCount > array.Length ) + throw new ArgumentException( "The number of payloads is greater than the available space.", nameof( array ) ); + foreach( var p in this ) + array[ arrayIndex++ ] = p; + } + + /// + void ICollection< ReadOnlySePayload >.Add( ReadOnlySePayload item ) => throw new NotSupportedException(); + + /// + void ICollection< ReadOnlySePayload >.Clear() => throw new NotSupportedException(); + + /// + bool ICollection< ReadOnlySePayload >.Remove( ReadOnlySePayload item ) => throw new NotSupportedException(); + + /// + void ICollection.CopyTo( Array? array, int index ) => CopyTo( ( array as ReadOnlySePayload[] ) switch + { + { } a => a, + null when array is not null => throw new ArrayTypeMismatchException(), + null => throw new ArgumentNullException( nameof( array ) ), + }, index ); + + /// Enumerator for enumerating a by payloads with offsets. + public struct OffsetEnumerator : IEnumerator< (int Offset, ReadOnlySePayload Payload) >, IEnumerable< (int Offset, ReadOnlySePayload Payload) > + { + private readonly ReadOnlySeString _obj; + private int _nextIndex; + + /// Initializes a new instance of the struct. + /// An instance of to enumerate. + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public OffsetEnumerator( ReadOnlySeString obj ) + { + _obj = obj; + _nextIndex = 0; + } + + /// Gets the byte offset of the current payload. + public (int Offset, ReadOnlySePayload Payload) Current { get; private set; } + + /// + object IEnumerator.Current => Current; + + /// + public bool MoveNext() + { + if( _nextIndex >= _obj.Data.Length ) + return false; + + var subspan = _obj.Data.Span[ _nextIndex.. ]; + switch( subspan[ 0 ] ) + { + // A valid SeString never "contains a null byte; it is always the terminator. + // Take one byte, and consider it as an invalid payload. + case 0: + Current = ( + _nextIndex, + new( ReadOnlySePayloadType.Invalid, default, _obj.Data.Slice( _nextIndex, 1 ) ) ); + _nextIndex += 1; + break; + + // Start byte. + case Stx: + { + // A macro payload is always at least 4 bytes. (STX, macro type, body length, ETX) + // If we cannot produce a well-formed macro payload, consider the current byte as a single byte invalid payload. + if( subspan.Length < 4 ) + goto case 0; + + var macroCode = (MacroCode) subspan[ 1 ]; + if( !SeExpressionUtilities.TryDecodeInt( subspan[ 2.. ], out var bodyLength, out var bodyLengthLength ) ) + goto case 0; + + // See above comments on if( Length < 4 ). + if( 2 + bodyLengthLength + bodyLength + 1 > subspan.Length ) + goto case 0; + + // The payload is not terminating with an ETX. Consider it invalid. + if( subspan[ 2 + bodyLengthLength + bodyLength ] != Etx ) + goto case 0; + + Current = ( + _nextIndex, + new( ReadOnlySePayloadType.Macro, macroCode, _obj.Data.Slice( _nextIndex + 2 + bodyLengthLength, bodyLength ) ) ); + _nextIndex += 2 + bodyLengthLength + bodyLength + 1; + break; + } + + // Raw text that will be followed by a payload or a null character. + case var _ when subspan.IndexOfAny( (byte) 2, (byte) 0 ) is var i and >= 0: + Current = ( + _nextIndex, + new( ReadOnlySePayloadType.Text, default, _obj.Data.Slice( _nextIndex, i ) ) ); + _nextIndex += i; + break; + + // Final raw text. + default: + Current = ( + _nextIndex, + new( ReadOnlySePayloadType.Text, default, _obj.Data[ _nextIndex.. ] ) ); + _nextIndex = _obj.Data.Length; + break; + } + + return true; + } + + /// + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public void Reset() => _nextIndex = 0; + + /// + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public void Dispose() + { } + + /// + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public OffsetEnumerator GetEnumerator() => new( _obj ); + + /// + IEnumerator< (int Offset, ReadOnlySePayload Payload) > IEnumerable< (int Offset, ReadOnlySePayload Payload) >.GetEnumerator() => GetEnumerator(); + + /// + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } + + /// Enumerator for enumerating a by payloads. + public struct Enumerator : IEnumerator< ReadOnlySePayload >, IEnumerable< ReadOnlySePayload > + { + private OffsetEnumerator _offsetEnumerator; + + /// Initializes a new instance of the struct. + /// An instance of . + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public Enumerator( OffsetEnumerator offsetEnumerator ) => _offsetEnumerator = offsetEnumerator; + + /// + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public bool MoveNext() => _offsetEnumerator.MoveNext(); + + /// + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public void Reset() => _offsetEnumerator.Reset(); + + /// + public ReadOnlySePayload Current { + [MethodImpl( MethodImplOptions.AggressiveInlining )] + get => _offsetEnumerator.Current.Payload; + } + + /// + object IEnumerator.Current => _offsetEnumerator.Current.Payload; + + /// + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public void Dispose() => _offsetEnumerator.Dispose(); + + /// + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public Enumerator GetEnumerator() => new( _offsetEnumerator ); + + /// + IEnumerator< ReadOnlySePayload > IEnumerable< ReadOnlySePayload >.GetEnumerator() => GetEnumerator(); + + /// + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +} \ No newline at end of file diff --git a/src/Lumina/Text/ReadOnly/ReadOnlySeStringSpan.cs b/src/Lumina/Text/ReadOnly/ReadOnlySeStringSpan.cs new file mode 100644 index 00000000..461d24ac --- /dev/null +++ b/src/Lumina/Text/ReadOnly/ReadOnlySeStringSpan.cs @@ -0,0 +1,461 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; +using Lumina.Text.Payloads; + +namespace Lumina.Text.ReadOnly; + +/// A -like immutable implementation of SeString. +[SuppressMessage( "ReSharper", "ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator", Justification = "Avoid heap allocation" )] +public readonly ref struct ReadOnlySeStringSpan +{ + /// Read-only byte data for the SeString. + public readonly ReadOnlySpan< byte > Data; + + /// Initializes a new instance of the struct. + /// Raw SeString data. + /// No further mutations to the underlying data behind is expected. + public ReadOnlySeStringSpan( ReadOnlySpan< byte > data ) => Data = data; + + /// Initializes a new instance of the struct. + /// Pointer to the raw SeString data. + /// A SeString is a null-terminated byte sequence, so passing length is not necessary. + public unsafe ReadOnlySeStringSpan( byte* pointer ) : this( MemoryMarshal.CreateReadOnlySpanFromNullTerminated( pointer ) ) + { } + + /// Gets a value indicating whether this is empty. + public bool IsEmpty { + [MethodImpl( MethodImplOptions.AggressiveInlining )] + get => Data.IsEmpty; + } + + /// Gets the number of bytes in this . + public int ByteLength { + [MethodImpl( MethodImplOptions.AggressiveInlining )] + get => Data.Length; + } + + /// Gets the number of direct child payloads in this . + public int PayloadCount { + [MethodImpl( MethodImplOptions.AggressiveInlining )] + get { + var res = 0; + foreach( var _ in this ) + res++; + return res; + } + } + + /// Gets the read-only byte memory view of this instance of . + /// The string to get a byte span view of. + /// The read-only byte span view of . + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public static implicit operator ReadOnlySpan< byte >( ReadOnlySeStringSpan s ) => s.Data; + + /// Creates a new instance of struct from the given byte span view. + /// The source byte span view. + /// A new instance of wrapping . + /// The backing data of is not expected to be mutated once the cast is done. + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public static implicit operator ReadOnlySeStringSpan( ReadOnlySpan< byte > s ) => new( s ); + + /// Creates a new instance of struct from the given byte span view. + /// The source byte span view. + /// A new instance of wrapping . + /// The backing data of is not expected to be mutated once the cast is done. + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public static implicit operator ReadOnlySeStringSpan( Span< byte > s ) => new( s ); + + /// Creates a new instance of struct from the given byte memory view. + /// The source byte memory view. + /// A new instance of wrapping . + /// The backing data of is not expected to be mutated once the cast is done. + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public static implicit operator ReadOnlySeStringSpan( ReadOnlyMemory< byte > s ) => new( s.Span ); + + /// Creates a new instance of struct from the given byte memory view. + /// The source byte memory view. + /// A new instance of wrapping . + /// The backing data of is not expected to be mutated once the cast is done. + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public static implicit operator ReadOnlySeStringSpan( Memory< byte > s ) => new( s.Span ); + + /// Creates a new instance of struct from the given byte array. + /// The source byte array. + /// A new instance of wrapping . + /// is not expected to be mutated once the cast is done. + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public static implicit operator ReadOnlySeStringSpan( byte[] s ) => new( s ); + + /// Returns a value that indicates whether the specified strings are equal. + /// The first string to compare. + /// The second string to compare. + /// if and are equal; otherwise, . + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public static bool operator ==( ReadOnlySeStringSpan left, ReadOnlySeStringSpan right ) => left.Equals( right ); + + /// Returns a value that indicates whether the specified strings are equal. + /// The first string to compare. + /// The second string to compare. + /// if and are equal; otherwise, . + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public static bool operator ==( ReadOnlySeStringSpan left, ReadOnlySeString right ) => left.Equals( right.AsSpan() ); + + /// Returns a value that indicates whether the specified strings are equal. + /// The first string to compare. + /// The second string to compare. + /// if and are equal; otherwise, . + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public static bool operator ==( ReadOnlySeString left, ReadOnlySeStringSpan right ) => left.AsSpan().Equals( right ); + + /// Returns a value that indicates whether the specified strings are not equal. + /// The first string to compare. + /// The second string to compare. + /// if and are not equal; otherwise, . + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public static bool operator !=( ReadOnlySeStringSpan left, ReadOnlySeStringSpan right ) => !left.Equals( right ); + + /// Returns a value that indicates whether the specified strings are not equal. + /// The first string to compare. + /// The second string to compare. + /// if and are not equal; otherwise, . + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public static bool operator !=( ReadOnlySeStringSpan left, ReadOnlySeString right ) => !left.Equals( right.AsSpan() ); + + /// Returns a value that indicates whether the specified strings are not equal. + /// The first string to compare. + /// The second string to compare. + /// if and are not equal; otherwise, . + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public static bool operator !=( ReadOnlySeString left, ReadOnlySeStringSpan right ) => !left.AsSpan().Equals( right ); + + /// Returns a concatenated instance of . + /// The string that comes first. + /// The string that comes second. + /// The concatenated string. + public static ReadOnlySeString operator +( ReadOnlySeString left, ReadOnlySeStringSpan right ) + { + var res = new byte[left.ByteLength + right.ByteLength]; + left.Data.CopyTo( res ); + right.Data.CopyTo( res.AsSpan( left.ByteLength ) ); + return new( res ); + } + + /// Returns a concatenated instance of . + /// The string that comes first. + /// The string that comes second. + /// The concatenated string. + public static ReadOnlySeString operator +( ReadOnlySeStringSpan left, ReadOnlySeString right ) + { + var res = new byte[left.ByteLength + right.ByteLength]; + left.Data.CopyTo( res ); + right.Data.CopyTo( res.AsMemory( left.ByteLength ) ); + return new( res ); + } + + /// Returns a concatenated instance of . + /// The string that comes first. + /// The string that comes second. + /// The concatenated string. + public static ReadOnlySeString operator +( ReadOnlySeStringSpan left, ReadOnlySeStringSpan right ) + { + var res = new byte[left.ByteLength + right.ByteLength]; + left.Data.CopyTo( res ); + right.Data.CopyTo( res.AsSpan( left.ByteLength ) ); + return new( res ); + } + + /// Returns a concatenated instance of . + /// The string that comes first. + /// The string that comes second. + /// The concatenated string. + public static ReadOnlySeString operator +( ReadOnlySeStringSpan left, ReadOnlySpan< char > right ) + { + var rnb = Encoding.UTF8.GetByteCount( right ); + var res = new byte[left.ByteLength + rnb]; + left.Data.CopyTo( res ); + Encoding.UTF8.GetBytes( right, res.AsSpan( left.Data.Length ) ); + return new( res ); + } + + /// Returns a concatenated instance of . + /// The string that comes first. + /// The string that comes second. + /// The concatenated string. + public static ReadOnlySeString operator +( ReadOnlySpan< char > left, ReadOnlySeStringSpan right ) + { + var lnb = Encoding.UTF8.GetByteCount( left ); + var res = new byte[lnb + right.ByteLength]; + Encoding.UTF8.GetBytes( left, res ); + right.Data.CopyTo( res.AsSpan( lnb ) ); + return new( res ); + } + + /// Validates whether this instance of SeString is well-formed. + /// true if this instance of is structurally valid. + public bool Validate() + { + foreach( var p in this ) + { + if( !p.Validate() ) + return false; + } + + return false; + } + + /// Extracts the text contained in this instance of , ignoring any payload that does not have a direct equivalent string + /// representation. + /// The extracted text. + public string ExtractText() + { + var len = 0; + foreach( var v in this ) + { + switch( v.Type ) + { + case ReadOnlySePayloadType.Text: + len += Encoding.UTF8.GetCharCount( v.Body ); + break; + case ReadOnlySePayloadType.Macro: + { + switch( v.MacroCode ) + { + case MacroCode.NewLine: + len += Environment.NewLine.Length; + break; + case MacroCode.NonBreakingSpace: + case MacroCode.Hyphen: + case MacroCode.SoftHyphen: + len += 1; + break; + } + + break; + } + } + } + + var buf = new char[len]; + var bufspan = buf.AsSpan(); + foreach( var v in this ) + { + switch( v.Type ) + { + case ReadOnlySePayloadType.Text: + bufspan = bufspan[ Encoding.UTF8.GetChars( v.Body, bufspan ) .. ]; + break; + case ReadOnlySePayloadType.Macro: + { + switch( v.MacroCode ) + { + case MacroCode.NewLine: + Environment.NewLine.CopyTo( bufspan ); + bufspan = bufspan[ Environment.NewLine.Length .. ]; + break; + + case MacroCode.NonBreakingSpace: + bufspan[ 0 ] = '\u00A0'; + bufspan = bufspan[ 1.. ]; + break; + + case MacroCode.Hyphen: + bufspan[ 0 ] = '-'; + bufspan = bufspan[ 1.. ]; + break; + + case MacroCode.SoftHyphen: + bufspan[ 0 ] = '\u00AD'; + bufspan = bufspan[ 1.. ]; + break; + } + + break; + } + } + } + + return new( buf ); + } + + /// + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public bool Equals( ReadOnlySeStringSpan other ) => Data.SequenceEqual( other.Data ); + + /// + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public override bool Equals( object? obj ) => false; + + /// + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public override int GetHashCode() + { + var hc = default( HashCode ); + hc.AddBytes( Data ); + return hc.ToHashCode(); + } + + /// Gets the encodeable macro representation of this instance of . + /// The encodeable macro representation. + public override string ToString() + { + var sb = new StringBuilder(); + foreach( var v in this ) + sb.Append( v.ToString() ); + return sb.ToString(); + } + + /// + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public Enumerator GetEnumerator() => new( new( this ) ); + + /// Gets an enumerator that enumerates payloads with their offsets. + /// An enumerator that enumerates payloads with their offsets. + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public OffsetEnumerator GetOffsetEnumerator() => new( this ); + + /// A pair of offset in and the corresponding instance of . + public readonly ref struct OffsetAndPayload + { + /// The offset in that is at. + public readonly int Offset; + + /// The payload. + public readonly ReadOnlySePayloadSpan Payload; + + /// Initializes a new instance of the struct. + /// The offset. + /// The expression. + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public OffsetAndPayload( int offset, ReadOnlySePayloadSpan payload ) + { + Offset = offset; + Payload = payload; + } + } + + /// Enumerator for enumerating a by payloads with offsets. + public ref struct OffsetEnumerator + { + private readonly ReadOnlySeStringSpan _obj; + private int _nextIndex; + + /// Initializes a new instance of the struct. + /// An instance of to enumerate. + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public OffsetEnumerator( ReadOnlySeStringSpan obj ) + { + _obj = obj; + _nextIndex = 0; + } + + /// Gets the byte offset of the current payload. + public OffsetAndPayload Current { get; private set; } + + /// + public bool MoveNext() + { + if( _nextIndex >= _obj.Data.Length ) + return false; + + var subspan = _obj.Data[ _nextIndex.. ]; + switch( subspan[ 0 ] ) + { + // A valid SeString never "contains a null byte; it is always the terminator. + // Take one byte, and consider it as an invalid payload. + case 0: + Current = new( + _nextIndex, + new( ReadOnlySePayloadType.Invalid, default, _obj.Data.Slice( _nextIndex, 1 ) ) ); + _nextIndex += 1; + break; + + // Start byte. + case SeString.StartByte: + { + // A macro payload is always at least 4 bytes. (STX, macro type, body length, ETX) + // If we cannot produce a well-formed macro payload, consider the current byte as a single byte invalid payload. + if( subspan.Length < 4 ) + goto case 0; + + var macroCode = (MacroCode) subspan[ 1 ]; + if( !SeExpressionUtilities.TryDecodeInt( subspan[ 2.. ], out var bodyLength, out var bodyLengthLength ) ) + goto case 0; + + // See above comments on if( Length < 4 ). + if( 2 + bodyLengthLength + bodyLength + 1 > subspan.Length ) + goto case 0; + + // The payload is not terminating with an ETX. Consider it invalid. + if( subspan[ 2 + bodyLengthLength + bodyLength ] != SeString.EndByte ) + goto case 0; + + Current = new( + _nextIndex, + new( ReadOnlySePayloadType.Macro, macroCode, _obj.Data.Slice( _nextIndex + 2 + bodyLengthLength, bodyLength ) ) ); + _nextIndex += 2 + bodyLengthLength + bodyLength + 1; + break; + } + + // Raw text that will be followed by a payload or a null character. + case var _ when subspan.IndexOfAny( (byte) 2, (byte) 0 ) is var i and >= 0: + Current = new( + _nextIndex, + new( ReadOnlySePayloadType.Text, default, _obj.Data.Slice( _nextIndex, i ) ) ); + _nextIndex += i; + break; + + // Final raw text. + default: + Current = new( + _nextIndex, + new( ReadOnlySePayloadType.Text, default, _obj.Data[ _nextIndex.. ] ) ); + _nextIndex = _obj.Data.Length; + break; + } + + return true; + } + + /// + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public void Reset() => _nextIndex = 0; + + /// + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public OffsetEnumerator GetEnumerator() => new( _obj ); + } + + /// Enumerator for enumerating a by payloads. + public ref struct Enumerator + { + private OffsetEnumerator _offsetEnumerator; + + /// Initializes a new instance of the struct. + /// An instance of . + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public Enumerator( OffsetEnumerator offsetEnumerator ) => _offsetEnumerator = offsetEnumerator; + + /// + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public bool MoveNext() => _offsetEnumerator.MoveNext(); + + /// + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public void Reset() => _offsetEnumerator.Reset(); + + /// + public ReadOnlySePayloadSpan Current { + [MethodImpl( MethodImplOptions.AggressiveInlining )] + get => _offsetEnumerator.Current.Payload; + } + + /// + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public Enumerator GetEnumerator() => new( _offsetEnumerator ); + } +} \ No newline at end of file diff --git a/src/Lumina/Text/SeExpressionUtilities.cs b/src/Lumina/Text/SeExpressionUtilities.cs new file mode 100644 index 00000000..e6d1c117 --- /dev/null +++ b/src/Lumina/Text/SeExpressionUtilities.cs @@ -0,0 +1,545 @@ +using System; +using System.Runtime.CompilerServices; +using System.Text; +using Lumina.Text.ReadOnly; + +namespace Lumina.Text; + +/// Utilities for implementing SeString expressions. +public static class SeExpressionUtilities +{ + /// Parses the length of an expression. + /// The byte memory to parse from. + /// The consumed length. + /// true on success. + public static bool TryDecodeLength( ReadOnlyMemory< byte > memory, out int length ) + { + if( memory.IsEmpty ) + { + length = 0; + return false; + } + + return TryDecodeUInt( memory, out _, out length ) + || TryDecodeString( memory, out _, out length ) + || TryDecodeNullary( memory, out _, out length ) + || TryDecodeUnary( memory, out _, out _, out length ) + || TryDecodeBinary( memory, out _, out _, out _, out length ); + } + + /// Parses the length of an expression. + /// The byte span to parse from. + /// The consumed length. + /// true on success. + public static bool TryDecodeLength( ReadOnlySpan< byte > span, out int length ) + { + if( span.IsEmpty ) + { + length = 0; + return false; + } + + return TryDecodeUInt( span, out _, out length ) + || TryDecodeString( span, out _, out length ) + || TryDecodeNullary( span, out _, out length ) + || TryDecodeUnary( span, out _, out _, out length ) + || TryDecodeBinary( span, out _, out _, out _, out length ); + } + + /// + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public static bool TryDecodeUInt( ReadOnlyMemory< byte > span, out uint value, out int length ) => + TryDecodeUInt( span.Span, out value, out length ); + + /// Parses an integer expression. + /// The byte span to parse from. + /// The parsed value. + /// The consumed length. + /// true on success. + public static bool TryDecodeUInt( ReadOnlySpan< byte > span, out uint value, out int length ) + { + value = 0; + switch( span.IsEmpty ? (byte) 0 : span[ 0 ] ) + { + case > 0 and < 0xD0: + value = (uint) span[ 0 ] - 1; + length = 1; + return true; + + case >= 0xF0 and <= 0xFE when span.Length >= 2: + { + var typeByte = ( span[ 0 ] + 1 ) & 0xF; + value = 0; + length = 1; + + if( ( typeByte & 8 ) != 0 ) + { + if( span.Length <= length ) + return false; + var u = (uint) span[ length++ ]; + if( u == 0 ) + return false; + value |= u << 24; + } + + if( ( typeByte & 4 ) != 0 ) + { + if( span.Length <= length ) + return false; + var u = (uint) span[ length++ ]; + if( u == 0 ) + return false; + value |= u << 16; + } + + if( ( typeByte & 2 ) != 0 ) + { + if( span.Length <= length ) + return false; + var u = (uint) span[ length++ ]; + if( u == 0 ) + return false; + value |= u << 8; + } + + if( ( typeByte & 1 ) != 0 ) + { + if( span.Length <= length ) + return false; + var u = (uint) span[ length++ ]; + if( u == 0 ) + return false; + value |= u; + } + + return true; + } + + default: + value = 0; + length = 0; + return false; + } + } + + /// + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public static bool TryDecodeInt( ReadOnlyMemory< byte > span, out int value, out int length ) => + TryDecodeInt( span.Span, out value, out length ); + + /// + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public static bool TryDecodeInt( ReadOnlySpan< byte > span, out int value, out int length ) + { + if( !TryDecodeUInt( span, out var u32, out length ) ) + { + value = 0; + return false; + } + + value = unchecked( (int) u32 ); + return true; + } + + /// Parses a SeString expression. + /// The byte memory to parse from. + /// The parsed value. + /// The consumed length. + /// true on success. + public static bool TryDecodeString( ReadOnlyMemory< byte > memory, out ReadOnlySeString value, out int length ) + { + value = default; + length = 0; + + var span = memory.Span; + if( span.Length < 2 || span[ 0 ] != 0xFF ) + return false; + if( !TryDecodeInt( span[ 1.. ], out var strLen, out var exprLen ) + || strLen < 0 ) + return false; + + length = 1 + exprLen + strLen; + if( length > span.Length ) + return false; + + value = new( memory.Slice( 1 + exprLen, strLen ) ); + return true; + } + + /// Parses a SeString expression. + /// The byte span to parse from. + /// The parsed value. + /// The consumed length. + /// true on success. + public static bool TryDecodeString( ReadOnlySpan< byte > span, out ReadOnlySeStringSpan value, out int length ) + { + value = default; + length = 0; + + if( span.Length < 2 || span[ 0 ] != 0xFF ) + return false; + if( !TryDecodeInt( span[ 1.. ], out var strLen, out var exprLen ) + || strLen < 0 ) + return false; + + length = 1 + exprLen + strLen; + if( length > span.Length ) + return false; + + value = new( span.Slice( 1 + exprLen, strLen ) ); + return true; + } + + /// Parses a SeString expression. + /// The byte span to parse from. + /// The parsed value. + /// The consumed length. + /// true on success. + public static bool TryDecodeSpan( ReadOnlySpan< byte > span, out ReadOnlySeStringSpan value, out int length ) + { + value = default; + length = 0; + + if( span.Length < 2 || span[ 0 ] != 0xFF ) + return false; + if( !TryDecodeInt( span[ 1.. ], out var strLen, out var exprLen ) + || strLen < 0 ) + return false; + + length = 1 + exprLen + strLen; + if( length > span.Length ) + return false; + + value = new( span.Slice( 1 + exprLen, strLen ) ); + return true; + } + + /// Parses a placeholder expression. + /// The byte memory to parse from. + /// The parsed expression type. + /// The consumed length. + /// true on success. + public static bool TryDecodeNullary( ReadOnlyMemory< byte > memory, out byte expressionType, out int length ) => + TryDecodeNullary( memory.Span, out expressionType, out length ); + + /// Parses a placeholder expression. + /// The byte span to parse from. + /// The parsed expression type. + /// The consumed length. + /// true on success. + public static bool TryDecodeNullary( ReadOnlySpan< byte > span, out byte expressionType, out int length ) + { + expressionType = span.IsEmpty ? (byte) 0 : span[ 0 ]; + length = expressionType is >= 0xD0 and <= 0xDF or 0xEC ? 1 : 0; + return length != 0; + } + + /// Parses a parameter expression. + /// The byte memory to parse from. + /// The parsed expression type. + /// The parsed operand. + /// The consumed length. + /// true on success. + public static bool TryDecodeUnary( + ReadOnlyMemory< byte > memory, + out byte expressionType, + out ReadOnlySeExpression operand, + out int length ) + { + expressionType = memory.IsEmpty ? (byte) 0 : memory.Span[ 0 ]; + if( expressionType is >= 0xE8 and <= 0xEB ) + { + memory = memory[ 1.. ]; + if( !TryDecodeLength( memory, out length ) || memory.Length < length) + { + operand = default; + return false; + } + + operand = memory[ ..length ]; + length++; + return true; + } + + operand = default; + length = 0; + return false; + } + + /// Parses a parameter expression. + /// The byte span to parse from. + /// The parsed expression type. + /// The parsed operand. + /// The consumed length. + /// true on success. + public static bool TryDecodeUnary( + ReadOnlySpan< byte > span, + out byte expressionType, + out ReadOnlySeExpressionSpan operand, + out int length ) + { + expressionType = span.IsEmpty ? (byte) 0 : span[ 0 ]; + if( expressionType is >= 0xE8 and <= 0xEB ) + { + span = span[ 1.. ]; + if( !TryDecodeLength( span, out length ) || span.Length < length) + { + operand = default; + return false; + } + + operand = span[ ..length ]; + length++; + return true; + } + + operand = default; + length = 0; + return false; + } + + /// Parses a binary expression. + /// The byte memory to parse from. + /// The parsed expression type. + /// The parsed operand 1. + /// The parsed operand 2. + /// The consumed length. + /// true on success. + public static bool TryDecodeBinary( + ReadOnlyMemory< byte > memory, + out byte expressionType, + out ReadOnlySeExpression operand1, + out ReadOnlySeExpression operand2, + out int length ) + { + expressionType = memory.IsEmpty ? (byte) 0 : memory.Span[ 0 ]; + if( expressionType is >= 0xE0 and <= 0xE5 ) + { + memory = memory[ 1.. ]; + if( !TryDecodeLength( memory, out length ) || memory.Length < length ) + { + operand1 = operand2 = default; + return false; + } + + operand1 = memory[ ..length ]; + memory = memory[ length.. ]; + + if( !TryDecodeLength( memory, out var length2 ) || memory.Length < length2 ) + { + operand2 = default; + return false; + } + + operand2 = memory[ ..length2 ]; + length = 1 + length + length2; + return true; + } + + operand1 = operand2 = default; + length = 0; + return false; + } + + /// Parses a binary expression. + /// The byte span to parse from. + /// The parsed expression type. + /// The parsed operand 1. + /// The parsed operand 2. + /// The consumed length. + /// true on success. + public static bool TryDecodeBinary( + ReadOnlySpan< byte > span, + out byte expressionType, + out ReadOnlySeExpressionSpan operand1, + out ReadOnlySeExpressionSpan operand2, + out int length ) + { + expressionType = span.IsEmpty ? (byte) 0 : span[ 0 ]; + if( expressionType is >= 0xE0 and <= 0xE5 ) + { + span = span[ 1.. ]; + if( !TryDecodeLength( span, out length ) || span.Length < length ) + { + operand1 = operand2 = default; + return false; + } + + operand1 = span[ ..length ]; + span = span[ length.. ]; + + if( !TryDecodeLength( span, out var length2 ) || span.Length < length2 ) + { + operand2 = default; + return false; + } + + operand2 = span[ ..length2 ]; + length = 1 + length + length2; + return true; + } + + operand1 = operand2 = default; + length = 0; + return false; + } + + /// Calculates the number of bytes required to encode the given value as a SeString expression. + /// The value. + /// The required number of bytes. + public static int CalculateLengthUInt( uint value ) => Expressions.IntegerExpression.CalculateSize( value ); + + /// Calculates the number of bytes required to encode the given value as a SeString expression. + /// The value. + /// The required number of bytes. + public static int CalculateLengthInt( int value ) => Expressions.IntegerExpression.CalculateSize( unchecked( (uint) value ) ); + + /// Calculates the number of bytes required to encode the given value as a SeString expression. + /// The value. + /// The required number of bytes. + public static int CalculateLengthString( ReadOnlySeString value ) => + 1 + CalculateLengthInt( value.ByteLength ) + value.ByteLength; + + /// Calculates the number of bytes required to encode the given value as a SeString expression. + /// The value. + /// The required number of bytes. + public static int CalculateLengthString( ReadOnlySeStringSpan value ) => + 1 + CalculateLengthInt( value.ByteLength ) + value.ByteLength; + + /// Calculates the number of bytes required to encode the given value as a SeString expression. + /// The value. + /// The required number of bytes. + public static int CalculateLengthString( ReadOnlySpan< char > value ) + { + var len8 = Encoding.UTF8.GetByteCount( value ); + return 1 + CalculateLengthInt( len8 ) + len8; + } + + /// Writes the given byte to the beginning of the span. + /// The span to write to. + /// The byte. + /// Number of bytes (to be) written. + public static int WriteRaw( Span< byte > span, byte b ) + { + if( !span.IsEmpty ) + span[ 0 ] = b; + return 1; + } + + /// Writes the given bytes to the beginning of the span. + /// The span to write to. + /// The bytes. + /// Number of bytes (to be) written. + public static int WriteRaw( Span< byte > span, ReadOnlySpan< byte > b ) + { + if( !span.IsEmpty ) + b.CopyTo( span ); + return b.Length; + } + + /// Encodes the given value to the beginning of the span. + /// The span to write to. + /// The value to encode. + /// Number of bytes (to be) written. + public static int EncodeUInt( Span< byte > span, uint value ) + { + if( value < 0xCF ) + { + if( !span.IsEmpty ) + span[ 0 ] = (byte) ( 1 + value ); + return 1; + } + + var ptr = 1; + if( !span.IsEmpty ) + span[ 0 ] = 0xF0; + + var t = (byte) ( value >> 24 ); + if( t != 0 ) + { + if( !span.IsEmpty ) + { + span[ ptr ] = t; + span[ 0 ] |= 8; + } + + ptr++; + } + + t = (byte) ( value >> 16 ); + if( t != 0 ) + { + if( !span.IsEmpty ) + { + span[ ptr ] = t; + span[ 0 ] |= 4; + } + + ptr++; + } + + t = (byte) ( value >> 8 ); + if( t != 0 ) + { + if( !span.IsEmpty ) + { + span[ ptr ] = t; + span[ 0 ] |= 2; + } + + ptr++; + } + + t = (byte) ( value >> 0 ); + if( t != 0 ) + { + if( !span.IsEmpty ) + { + span[ ptr ] = t; + span[ 0 ] |= 1; + } + + ptr++; + } + + if( !span.IsEmpty ) + span[ 0 ]--; + return ptr; + } + + /// Encodes the given value to the beginning of the span. + /// The span to write to. + /// The value to encode. + /// Number of bytes (to be) written. + public static int EncodeInt( Span< byte > span, int value ) => EncodeUInt( span, (uint) value ); + + /// Encodes the given value to the beginning of the span. + /// The span to write to. + /// The value to encode. + /// Number of bytes (to be) written. + public static int EncodeString( Span< byte > span, ReadOnlySeString value ) + { + var len = 0; + len += WriteRaw( span[ len.. ], 0xFF ); + len += EncodeInt( span[ len.. ], value.ByteLength ); + len += WriteRaw( span[ len.. ], value.Data.Span ); + return len; + } + + /// Encodes the given value to the beginning of the span. + /// The span to write to. + /// The value to encode. + /// Number of bytes (to be) written. + public static int EncodeString( Span< byte > span, ReadOnlySpan< char > value ) + { + var bc = Encoding.UTF8.GetByteCount( value ); + if( span.IsEmpty ) + return 1 + CalculateLengthInt( bc ) + bc; + + var len = 0; + len += WriteRaw( span[ len.. ], 0xFF ); + len += EncodeInt( span[ len.. ], bc ); + len += Encoding.UTF8.GetBytes( value, span.Slice( len, bc ) ); + return len; + } +} \ No newline at end of file diff --git a/src/Lumina/Text/SeString.cs b/src/Lumina/Text/SeString.cs index a9433f31..76d947c7 100644 --- a/src/Lumina/Text/SeString.cs +++ b/src/Lumina/Text/SeString.cs @@ -7,6 +7,7 @@ using Lumina.Data; using Lumina.Extensions; using Lumina.Text.Payloads; +using Lumina.Text.ReadOnly; namespace Lumina.Text { @@ -135,6 +136,8 @@ private ImmutableList< BasePayload > BuildPayloads() public static implicit operator string( SeString str ) => str.RawString; + public static implicit operator ReadOnlySeString( SeString str ) => str.AsReadOnly(); + // public static SeString operator +( SeString lhs, SeString rhs ) // { // return null; @@ -168,6 +171,10 @@ private ImmutableList< BasePayload > BuildPayloads() // throw new NotImplementedException(); // } + /// Gets a view of this . + /// A new instance of . + public ReadOnlySeString AsReadOnly() => new( _rawData.Value ); + public override string ToString() { return RawString; diff --git a/src/Lumina/Text/SeStringBuilder.Append.cs b/src/Lumina/Text/SeStringBuilder.Append.cs new file mode 100644 index 00000000..2acfec45 --- /dev/null +++ b/src/Lumina/Text/SeStringBuilder.Append.cs @@ -0,0 +1,277 @@ +using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using Lumina.Text.ReadOnly; + +namespace Lumina.Text; + +/// A builder for . +public sealed partial class SeStringBuilder +{ + private static readonly ConditionalWeakTable< Type, Delegate > TypedAppendDelegates = new(); + + private delegate void TypedAppendDelegate< T >( SeStringBuilder ssb, scoped in T value ) where T : struct; + + /// Adds the given UTF-16 char sequence. + /// Text to add. + /// A reference of this instance after the append operation is completed. + public SeStringBuilder Append( ReadOnlySpan< char > value ) + { + if( value.IndexOfAny( (char) ReadOnlySeString.Stx, '\0' ) != -1 ) + throw new ArgumentException( "A SeString may not contain STX or NUL for text.", nameof( value ) ); + Encoding.UTF8.GetBytes( value, AllocateStringSpan( Encoding.UTF8.GetByteCount( value ) ) ); + return this; + } + + /// Adds the given UTF-16 char sequence. + /// Text to add. + /// A reference of this instance after the append operation is completed. + public SeStringBuilder Append( Span< char > value ) => Append( (ReadOnlySpan< char >) value ); + + /// Adds the given UTF-16 char sequence. + /// Text to add. + /// A reference of this instance after the append operation is completed. + public SeStringBuilder Append( ReadOnlyMemory< char > value ) => Append( value.Span ); + + /// Adds the given UTF-16 char sequence. + /// Text to add. + /// A reference of this instance after the append operation is completed. + public SeStringBuilder Append( Memory< char > value ) => Append( value.Span ); + + /// Adds the given UTF-16 char sequence. + /// Text to add. + /// A reference of this instance after the append operation is completed. + public SeStringBuilder Append( char[] value ) => Append( value.AsSpan() ); + + /// Adds the given UTF-16 char sequence. + /// Text to add. + /// The starting position of the substring within . + /// The number of characters in to append. + /// A reference of this instance after the append operation is completed. + public SeStringBuilder Append( char[] value, int startIndex, int count ) => Append( value.AsSpan( startIndex, count ) ); + + /// Adds the given UTF-16 char sequence. + /// Text to add. + /// A reference of this instance after the append operation is completed. + public SeStringBuilder Append( string? value ) => Append( value.AsSpan() ); + + /// Adds the given UTF-16 char sequence. + /// Text to add. + /// The starting position of the substring within . + /// The number of characters in to append. + /// A reference of this instance after the append operation is completed. + public SeStringBuilder Append( string? value, int startIndex, int count ) => Append( value.AsSpan( startIndex, count ) ); + + /// Adds the given UTF-8 byte sequence. + /// Text to add. + /// A reference of this instance after the append operation is completed. + public SeStringBuilder Append( ReadOnlySpan< byte > value ) + { + var stream = GetStringStream(); + if( value.IndexOfAny( ReadOnlySeString.Stx, (byte) 0 ) != -1 ) + throw new ArgumentException( "A SeString may not contain STX or NUL for text.", nameof( value ) ); + stream.Write( value ); + return this; + } + + /// Adds the given UTF-8 byte sequence. + /// Text to add. + /// A reference of this instance after the append operation is completed. + public SeStringBuilder Append( Span< byte > value ) => Append( (ReadOnlySpan< byte >) value ); + + /// Adds the given UTF-8 byte sequence. + /// Text to add. + /// A reference of this instance after the append operation is completed. + public SeStringBuilder Append( ReadOnlyMemory< byte > value ) => Append( value.Span ); + + /// Adds the given UTF-8 byte sequence. + /// Text to add. + /// A reference of this instance after the append operation is completed. + public SeStringBuilder Append( Memory< byte > value ) => Append( value.Span ); + + /// Adds the given UTF-8 byte sequence. + /// Text to add. + /// A reference of this instance after the append operation is completed. + public SeStringBuilder Append( byte[] value ) => Append( value.AsSpan() ); + + /// Adds the given UTF-8 byte sequence. + /// Text to add. + /// The starting position of the substring within . + /// The number of characters in to append. + /// A reference of this instance after the append operation is completed. + public SeStringBuilder Append( byte[] value, int startIndex, int count ) => Append( value.AsSpan( startIndex, count ) ); + + /// Adds the given SeString from the given StringBuilder. + /// The string builder that contains the substring to append. + /// A reference of this instance after the append operation is completed. + public SeStringBuilder Append( StringBuilder value ) + { + var len = 0; + foreach( var chunk in value.GetChunks() ) + { + if (chunk.Span.IndexOfAny( (char) ReadOnlySeString.Stx, '\0' ) != -1 ) + throw new ArgumentException( "A SeString may not contain STX or NUL for text.", nameof( value ) ); + len += Encoding.UTF8.GetByteCount( chunk.Span ); + } + + var span = AllocateStringSpan( len ); + foreach( var chunk in value.GetChunks() ) + span = span[ Encoding.UTF8.GetBytes( chunk.Span, span ).. ]; + return this; + } + + /// Adds the given SeString from the given StringBuilder. + /// The string builder that contains the substring to append. + /// The starting position of the substring within . + /// The number of characters in to append. + /// A reference of this instance after the append operation is completed. + public SeStringBuilder Append( StringBuilder value, int startIndex, int count ) + { + if( startIndex + count > value.Length ) + throw new ArgumentException( "Tried to append from out of StringBuilder data range.", nameof( count ) ); + + var len = 0; + { + var index2 = startIndex; + var count2 = count; + foreach( var chunk in value.GetChunks() ) + { + if( chunk.Length <= index2 ) + { + index2 -= chunk.Length; + continue; + } + + var avail = Math.Min( count2, chunk.Length - index2 ); + if (chunk.Span.Slice( index2, avail ).IndexOfAny( (char) ReadOnlySeString.Stx, '\0' ) != -1 ) + throw new ArgumentException( "A SeString may not contain STX or NUL for text.", nameof( value ) ); + len += Encoding.UTF8.GetByteCount( chunk.Span.Slice( index2, avail ) ); + index2 += avail; + count2 -= avail; + + if( count2 == 0 ) + break; + Debug.Assert( count2 > 0, "Logic problems" ); + } + } + + var span = AllocateStringSpan( len ); + { + var index2 = startIndex; + var count2 = count; + foreach( var chunk in value.GetChunks() ) + { + if( chunk.Length <= index2 ) + { + index2 -= chunk.Length; + continue; + } + + var avail = Math.Min( count2, chunk.Length - index2 ); + span = span[ Encoding.UTF8.GetBytes( chunk.Span.Slice( index2, avail ), span ).. ]; + index2 += avail; + count2 -= avail; + + if( count2 == 0 ) + break; + Debug.Assert( count2 > 0, "Logic problems" ); + } + } + + return this; + } + + /// Adds the given SeString. + /// Text to add. + /// A reference of this instance after the append operation is completed. + public SeStringBuilder Append( SeString value ) + { + GetStringStream().Write( value.RawData ); + return this; + } + + /// Adds the given SeString. + /// Text to add. + /// A reference of this instance after the append operation is completed. + public SeStringBuilder Append( ReadOnlySeString value ) + { + GetStringStream().Write( value.Data.Span ); + return this; + } + + /// Adds the given SeString payload, wrapping in envelope as needed. + /// Payload to add. + /// A reference of this instance after the append operation is completed. + public SeStringBuilder Append( ReadOnlySePayload value ) + { + value.WriteEnvelopeTo( AllocateStringSpan( value.EnvelopeByteLength ) ); + return this; + } + + /// Adds the given SeString expression, which is an invalid operation. + /// Expression to add. + [Obsolete( "You cannot append a SeExpression to a SeString.", true )] + [SuppressMessage( "ReSharper", "UnusedParameter.Global", Justification = "Trap for invalid append call from implicit casts with overloads." )] + public void Append( ReadOnlySeExpression value ) => throw new InvalidOperationException(); + + /// Adds the given SeString. + /// Text to add. + /// A reference of this instance after the append operation is completed. + public SeStringBuilder Append( ReadOnlySeStringSpan value ) + { + GetStringStream().Write( value.Data ); + return this; + } + + /// Adds the given SeString payload, wrapping in envelope as needed. + /// Payload to add. + /// A reference of this instance after the append operation is completed. + public SeStringBuilder Append( ReadOnlySePayloadSpan value ) + { + value.WriteEnvelopeTo( AllocateStringSpan( value.EnvelopeByteLength ) ); + return this; + } + + /// Adds the given SeString expression, which is an invalid operation. + /// Expression to add. + [Obsolete( "You cannot append a SeExpression to a SeString.", true )] + [SuppressMessage( "ReSharper", "UnusedParameter.Global", Justification = "Trap for invalid append call from implicit casts with overloads." )] + public void Append( ReadOnlySeExpressionSpan value ) => throw new InvalidOperationException(); + + /// Adds the given value. + /// Value to add. + /// A reference of this instance after the append operation is completed. + public SeStringBuilder Append( object? value ) => Append( value?.ToString() ); + + /// Adds the given value. + /// Value to add. + /// A reference of this instance after the append operation is completed. + public SeStringBuilder Append< T >( scoped in T value ) where T : struct + { + ( (TypedAppendDelegate< T >) TypedAppendDelegates.GetValue( typeof( T ), static ty => { +#if NET8_0 + if( ty.IsAssignableTo( typeof( IUtf8SpanFormattable ) ) ) + { + return typeof( SeStringBuilder ) + .GetMethod( nameof( TypedAppendUtf8SpanFormattable ), BindingFlags.Static | BindingFlags.NonPublic )! + .MakeGenericMethod( typeof( T ) ) + .CreateDelegate< TypedAppendDelegate< T > >(); + } +#endif + + if( ty.IsAssignableTo( typeof( ISpanFormattable ) ) ) + { + return typeof( SeStringBuilder ) + .GetMethod( nameof( TypedAppendSpanFormattable ), BindingFlags.Static | BindingFlags.NonPublic )! + .MakeGenericMethod( typeof( T ) ) + .CreateDelegate< TypedAppendDelegate< T > >(); + } + + return new TypedAppendDelegate< T >( ( SeStringBuilder ssb, scoped in T value ) => ssb.Append( (object?) value ) ); + } ) ).Invoke( this, in value ); + return this; + } +} \ No newline at end of file diff --git a/src/Lumina/Text/SeStringBuilder.Expressions.cs b/src/Lumina/Text/SeStringBuilder.Expressions.cs new file mode 100644 index 00000000..12274edb --- /dev/null +++ b/src/Lumina/Text/SeStringBuilder.Expressions.cs @@ -0,0 +1,169 @@ +using System; +using Lumina.Text.Expressions; + +namespace Lumina.Text; + +/// A builder for . +public sealed partial class SeStringBuilder +{ + /// Appends an integer calculated from RGBA values as an expression. + /// A normalized RGBA value to append. + /// A reference of this instance after the append operation is completed. + public SeStringBuilder AppendRgbaIntExpression( System.Numerics.Vector4 rgba ) => + AppendRgbaIntExpression( + (byte) Math.Clamp( 256 * rgba.X, 0, 255 ), + (byte) Math.Clamp( 256 * rgba.Y, 0, 255 ), + (byte) Math.Clamp( 256 * rgba.Z, 0, 255 ), + (byte) Math.Clamp( 256 * rgba.W, 0, 255 ) ); + + /// Appends an integer as an expression. + /// An integer value to append. + /// A reference of this instance after the append operation is completed. + public SeStringBuilder AppendIntExpression( int value ) => AppendUIntExpression( (uint) value ); + + /// Appends an unsigned integer as an expression. + /// An unsigned integer value to append. + /// A reference of this instance after the append operation is completed. + public SeStringBuilder AppendUIntExpression( uint value ) + { + SeExpressionUtilities.EncodeUInt( AllocateExpressionSpan( SeExpressionUtilities.CalculateLengthUInt( value ) ), value ); + OneExpressionWritten(); + return this; + } + + /// Appends an integer calculated from RGBA values as an expression. + /// A red byte value to append. + /// A red byte value to append. + /// A red byte value to append. + /// A red byte value to append. + /// A reference of this instance after the append operation is completed. + public SeStringBuilder AppendRgbaIntExpression( byte r, byte g, byte b, byte a ) => + AppendUIntExpression( ( (uint) a << 24 ) | ( (uint) b << 16 ) | ( (uint) g << 8 ) | r ); + + /// Appends a nullary expression. + /// A nullary expression type. + /// A reference of this instance after the append operation is completed. + public SeStringBuilder AppendNullaryExpression( ExpressionType expressionType ) + { + if( _mss[ ^1 ].Type is not StackType.Payload and not StackType.Expression ) + throw new InvalidOperationException("Expression cannot be appended in current state."); + if( _mss[ ^1 ].Type == StackType.Expression && _mss[ ^1 ].Ident <= 0 ) + throw new InvalidOperationException( $"No more expressions may be written. Call {nameof( EndExpression )}." ); + if( expressionType.GetArity() != ExpressionArity.Nullary ) + throw new ArgumentOutOfRangeException( nameof( expressionType ), expressionType, "Only nullary expression types are allowed." ); + AllocateExpressionSpan( 1 )[ 0 ] = (byte) expressionType; + OneExpressionWritten(); + return this; + } + + /// Starts writing an unary expression. + /// An unary expression type. + /// A reference of this instance after the operation is completed. + public SeStringBuilder BeginUnaryExpression( ExpressionType expressionType ) + { + if( _mss[ ^1 ].Type is not StackType.Payload and not StackType.Expression ) + throw new InvalidOperationException("Expression cannot be appended in current state."); + if( _mss[ ^1 ].Type == StackType.Expression && _mss[ ^1 ].Ident <= 0 ) + throw new InvalidOperationException( $"No more expressions may be written. Call {nameof( EndExpression )}." ); + if( expressionType.GetArity() != ExpressionArity.Unary ) + throw new ArgumentOutOfRangeException( nameof( expressionType ), expressionType, "Only unary expression types are allowed." ); + if( _mssFree.Count == 0 ) + { + _mss.Add( ( StackType.Expression, 1, new() ) ); + } + else + { + _mss.Add( ( StackType.Expression, 1, _mssFree[ ^1 ] ) ); + _mssFree.RemoveAt( _mssFree.Count - 1 ); + } + + AllocateExpressionSpan( 1 )[ 0 ] = (byte) expressionType; + return this; + } + + /// Starts writing a binary expression. + /// A binary expression type. + /// A reference of this instance after the operation is completed. + public SeStringBuilder BeginBinaryExpression( ExpressionType expressionType ) + { + if( _mss[ ^1 ].Type is not StackType.Payload and not StackType.Expression ) + throw new InvalidOperationException("Expression cannot be appended in current state."); + if( _mss[ ^1 ].Type == StackType.Expression && _mss[ ^1 ].Ident <= 0 ) + throw new InvalidOperationException( $"No more expressions may be written. Call {nameof( EndExpression )}." ); + if( expressionType.GetArity() != ExpressionArity.Binary ) + throw new ArgumentOutOfRangeException( nameof( expressionType ), expressionType, "Only unary expression types are allowed." ); + if( _mssFree.Count == 0 ) + { + _mss.Add( ( StackType.Expression, 2, new() ) ); + } + else + { + _mss.Add( ( StackType.Expression, 2, _mssFree[ ^1 ] ) ); + _mssFree.RemoveAt( _mssFree.Count - 1 ); + } + + AllocateExpressionSpan( 1 )[ 0 ] = (byte) expressionType; + return this; + } + + /// Starts writing a string expression. + /// A reference of this instance after the operation is completed. + public SeStringBuilder BeginStringExpression() + { + if( _mss[ ^1 ].Type is not StackType.Expression and not StackType.Payload ) + throw new InvalidOperationException( "A string expression can be added only in the context of expression or payload." ); + if( _mss[ ^1 ].Type == StackType.Expression && _mss[ ^1 ].Ident <= 0 ) + throw new InvalidOperationException( $"No more expressions may be written. Call {nameof( EndExpression )}." ); + if( _mssFree.Count == 0 ) + { + _mss.Add( ( StackType.String, 0, new() ) ); + } + else + { + _mss.Add( ( StackType.String, 0, _mssFree[ ^1 ] ) ); + _mssFree.RemoveAt( _mssFree.Count - 1 ); + } + + return this; + } + + /// Ends writing an unary, binary, or string expression. + /// + /// Use + public SeStringBuilder EndExpression() + { + var stream = _mss[ ^1 ].Stream; + var span = stream.GetBuffer().AsSpan( 0, (int) stream.Length ); + + switch( _mss[ ^1 ].Type ) + { + case StackType.String: + if( _mss.Count == 1 || _mss[ ^1 ].Type != StackType.String ) + throw new InvalidOperationException( "String expression is not currently being built." ); + + _mss.RemoveAt( _mss.Count - 1 ); + var buf = AllocateExpressionSpan( 1 + SeExpressionUtilities.CalculateLengthInt( span.Length ) + span.Length ); + buf = buf[ SeExpressionUtilities.WriteRaw( buf, 0xFF ).. ]; + buf = buf[ SeExpressionUtilities.EncodeInt( buf, span.Length ).. ]; + span.CopyTo( buf ); + break; + + case StackType.Expression: + if( _mss[ ^1 ].Ident != 0 ) + throw new InvalidOperationException( $"{_mss[ ^1 ].Ident} more expression(s) must be written." ); + + _mss.RemoveAt( _mss.Count - 1 ); + _mss[ ^1 ].Stream.Write( span ); + break; + + default: + throw new InvalidOperationException( "No expression is being written." ); + } + + stream.SetLength( stream.Position = 0 ); + _mssFree.Add( stream ); + + OneExpressionWritten(); + return this; + } +} \ No newline at end of file diff --git a/src/Lumina/Text/SeStringBuilder.Presets.cs b/src/Lumina/Text/SeStringBuilder.Presets.cs new file mode 100644 index 00000000..6633e4b5 --- /dev/null +++ b/src/Lumina/Text/SeStringBuilder.Presets.cs @@ -0,0 +1,141 @@ +using System; +using Lumina.Text.Expressions; +using Lumina.Text.Payloads; +using Lumina.Text.ReadOnly; + +namespace Lumina.Text; + +/// A builder for . +public sealed partial class SeStringBuilder +{ + /// Appends a local number unary expression. + /// The value ID. + /// A reference of this instance after the append operation is completed. + public SeStringBuilder AppendLocalNumberExpression( int id ) => + BeginUnaryExpression( ExpressionType.LocalNumber ).AppendIntExpression( id ).EndExpression(); + + /// Appends a local string unary expression. + /// The value ID. + /// A reference of this instance after the append operation is completed. + public SeStringBuilder AppendLocalStringExpression( int id ) => + BeginUnaryExpression( ExpressionType.LocalString ).AppendIntExpression( id ).EndExpression(); + + /// Appends a global number unary expression. + /// The value ID. + /// A reference of this instance after the append operation is completed. + public SeStringBuilder AppendGlobalNumberExpression( int id ) => + BeginUnaryExpression( ExpressionType.GlobalNumber ).AppendIntExpression( id ).EndExpression(); + + /// Appends a global string unary expression. + /// The value ID. + /// A reference of this instance after the append operation is completed. + public SeStringBuilder AppendGlobalStringExpression( int id ) => + BeginUnaryExpression( ExpressionType.GlobalString ).AppendIntExpression( id ).EndExpression(); + + /// Appends a string expression. + /// The value. + /// A reference of this instance after the append operation is completed. + public SeStringBuilder AppendStringExpression( ReadOnlySeStringSpan rosss ) => + BeginStringExpression().Append( rosss ).EndExpression(); + + /// Appends a string expression. + /// The value. + /// A reference of this instance after the append operation is completed. + public SeStringBuilder AppendStringExpression( ReadOnlySpan str ) => + BeginStringExpression().Append( str ).EndExpression(); + + /// Appends an icon. + /// The icon ID. + /// A reference of this instance after the append operation is completed. + public SeStringBuilder AppendIcon( uint icon ) => + BeginMacro( MacroCode.Icon ).AppendUIntExpression( icon ).EndMacro(); + + /// Appends an italicized text. + /// The string to append italicized. + /// + public SeStringBuilder AppendItalicized( ReadOnlySeStringSpan rosss ) => + AppendSetItalic( true ).Append( rosss ).AppendSetItalic( false ); + + /// Appends an italicized text. + /// The string to append italicized. + /// + public SeStringBuilder AppendItalicized( ReadOnlySpan< char > str ) => + AppendSetItalic( true ).Append( str ).AppendSetItalic( false ); + + /// Appends a payload to either turn italic on or off. + /// Whether to enable italics. + /// A reference of this instance after the append operation is completed. + public SeStringBuilder AppendSetItalic( bool enable ) => + BeginMacro( MacroCode.Italic ).AppendUIntExpression( enable ? 1u : 0u ).EndMacro(); + + /// Appends a bold text. + /// The string to append italicized. + /// + public SeStringBuilder AppendBold( ReadOnlySeStringSpan rosss ) => + AppendSetBold( true ).Append( rosss ).AppendSetBold( false ); + + /// Appends a bold text. + /// The string to append italicized. + /// + public SeStringBuilder AppendBold( ReadOnlySpan< char > str ) => + AppendSetBold( true ).Append( str ).AppendSetBold( false ); + + /// Appends a payload to either bold italic on or off. + /// Whether to enable bold. + /// A reference of this instance after the append operation is completed. + public SeStringBuilder AppendSetBold( bool enable ) => + BeginMacro( MacroCode.Bold ).AppendUIntExpression( enable ? 1u : 0u ).EndMacro(); + + /// Pushes a text foreground color. + /// The RGBA color value. + /// A reference of this instance after the append operation is completed. + public SeStringBuilder PushColor( uint rgba ) => + BeginMacro( MacroCode.Color ).AppendUIntExpression( rgba ).EndMacro(); + + /// Pops a text foreground color. + /// A reference of this instance after the append operation is completed. + public SeStringBuilder PopColor() => + BeginMacro( MacroCode.Color ).AppendNullaryExpression( ExpressionType.StackColor ).EndMacro(); + + /// Pushes a text border color. + /// The RGBA color value. + /// A reference of this instance after the append operation is completed. + public SeStringBuilder PushEdgeColor( uint rgba ) => + BeginMacro( MacroCode.EdgeColor ).AppendUIntExpression( rgba ).EndMacro(); + + /// Pops a text border color. + /// A reference of this instance after the append operation is completed. + public SeStringBuilder PopEdgeColor() => + BeginMacro( MacroCode.EdgeColor ).AppendNullaryExpression( ExpressionType.StackColor ).EndMacro(); + + /// Pushes a text shadow color. + /// The RGBA color value. + /// A reference of this instance after the append operation is completed. + public SeStringBuilder PushShadowColor( uint rgba ) => + BeginMacro( MacroCode.ShadowColor ).AppendUIntExpression( rgba ).EndMacro(); + + /// Pops a text shadow color. + /// A reference of this instance after the append operation is completed. + public SeStringBuilder PopShadowColor() => + BeginMacro( MacroCode.ShadowColor ).AppendNullaryExpression( ExpressionType.StackColor ).EndMacro(); + + /// Pushes a text foreground color from UIColor sheet. + /// The row ID in the UIColor sheet. + /// A reference of this instance after the append operation is completed. + public SeStringBuilder PushColorType( uint uiColorRowId ) => + BeginMacro( MacroCode.ColorType ).AppendUIntExpression( uiColorRowId ).EndMacro(); + + /// Pops a text foreground color pushed with . + /// A reference of this instance after the append operation is completed. + public SeStringBuilder PopColorType() => PushColorType( 0u ); + + /// Pushes a text border color from UIColor sheet. + /// The row ID in the UIColor sheet. + /// A reference of this instance after the append operation is completed. + public SeStringBuilder PushEdgeColorType( uint uiColorRowId ) => + BeginMacro( MacroCode.EdgeColorType ).AppendUIntExpression( uiColorRowId ).EndMacro(); + + /// Pops a text border color pushed with . + /// A reference of this instance after the append operation is completed. + public SeStringBuilder PopEdgeColorType() => PushEdgeColorType( 0u ); +} \ No newline at end of file diff --git a/src/Lumina/Text/SeStringBuilder.cs b/src/Lumina/Text/SeStringBuilder.cs new file mode 100644 index 00000000..a25561be --- /dev/null +++ b/src/Lumina/Text/SeStringBuilder.cs @@ -0,0 +1,202 @@ +using System; +using System.Buffers; +using System.Collections.Generic; +using System.IO; +using Lumina.Text.Payloads; +using Lumina.Text.ReadOnly; +using Microsoft.Extensions.ObjectPool; + +namespace Lumina.Text; + +/// A builder for . +public sealed partial class SeStringBuilder : IResettable +{ + private readonly List< (StackType Type, int Ident, MemoryStream Stream) > _mss = [( StackType.String, 0, new() )]; + private readonly List< MemoryStream > _mssFree = []; + + private enum StackType + { + String, + Payload, + Expression, + } + + /// Begins a macro. + /// The macro code. + /// A reference of this instance after the operation is completed. + public SeStringBuilder BeginMacro( MacroCode macroCode ) + { + if( !Enum.IsDefined( macroCode ) ) + throw new ArgumentOutOfRangeException( nameof( macroCode ), macroCode, "Invalid macro code specified." ); + if( _mss[ ^1 ].Type != StackType.String ) + throw new InvalidOperationException( "A payload can be added only in the context of SeString." ); + if( _mssFree.Count == 0 ) + { + _mss.Add( ( StackType.Payload, (byte) macroCode, new() ) ); + } + else + { + _mss.Add( ( StackType.Payload, (byte) macroCode, _mssFree[ ^1 ] ) ); + _mssFree.RemoveAt( _mssFree.Count - 1 ); + } + + return this; + } + + /// Ends a macro. + /// A reference of this instance after the operation is completed. + public SeStringBuilder EndMacro() + { + if( _mss[ ^1 ].Type != StackType.Payload ) + throw new InvalidOperationException( "No payload is currently being built." ); + + var stream = _mss[ ^1 ].Stream; + var payload = new ReadOnlySePayloadSpan( + ReadOnlySePayloadType.Macro, + (MacroCode) _mss[ ^1 ].Ident, + stream.GetBuffer().AsSpan( 0, (int) stream.Length ) ); + + _mss.RemoveAt( _mss.Count - 1 ); + Append( payload ); + + stream.SetLength( stream.Position = 0 ); + _mssFree.Add( stream ); + + return this; + } + + /// Clears everything. + /// Whether to fill the underlying buffer with zeroes. + /// A reference of this instance after the clear operation is completed. + public SeStringBuilder Clear( bool zeroBuffer = false ) + { + foreach( var ms in _mss ) + _mssFree.Add( ms.Stream ); + _mss.Clear(); + + foreach( var m in _mssFree ) + { + m.SetLength( m.Position = 0 ); + if( zeroBuffer ) + m.GetBuffer().AsSpan().Clear(); + } + + _mss.Add( ( StackType.String, 0, _mssFree[ ^1 ] ) ); + _mssFree.RemoveAt( _mssFree.Count - 1 ); + return this; + } + + /// Gets the SeString as a new byte array. + /// A new byte array. + public byte[] ToArray() + { + if( _mss.Count != 1 ) + throw new InvalidOperationException( "The string is incomplete, due to non-empty stack." ); + return _mss[ 0 ].Stream.ToArray(); + } + + /// Gets the SeString as a new instance of . + /// A new instance of . + public SeString ToSeString() => new( ToArray() ); + + /// Gets the SeString as a new instance of . + /// A new instance of . + public ReadOnlySeString ToReadOnlySeString() => ToArray(); + + /// + public bool TryReset() + { + Clear(); + return true; + } + +#if NET8_0 + /// Helper method for . + /// The target instance of . + /// The value to append. + /// The span formattable type. + private static void TypedAppendUtf8SpanFormattable< T >( SeStringBuilder ssb, scoped in T value ) where T : IUtf8SpanFormattable + { + for (var len = 128; ; len *= 2) + { + var buf = ArrayPool< byte >.Shared.Rent( len ); + if( value.TryFormat( buf, out var written, default, null ) ) + { + ssb.Append( buf[ ..written ] ); + ArrayPool< byte >.Shared.Return( buf ); + return; + } + + ArrayPool< byte >.Shared.Return( buf ); + } + } +#endif + + /// Helper method for . + /// The target instance of . + /// The value to append. + /// The span formattable type. + private static void TypedAppendSpanFormattable< T >( SeStringBuilder ssb, scoped in T value ) where T : ISpanFormattable + { + for( var len = 128;; len *= 2 ) + { + var buf = ArrayPool< char >.Shared.Rent( len ); + if( value.TryFormat( buf, out var written, default, null ) ) + { + ssb.Append( buf[ ..written ] ); + ArrayPool< char >.Shared.Return( buf ); + return; + } + + ArrayPool< char >.Shared.Return( buf ); + } + } + + /// Allocates a byte span from the result of . + /// Number of bytes to allocate. + /// The allocated byte span. + private Span< byte > AllocateStringSpan( int length ) + { + var stream = GetStringStream(); + var offset = unchecked( (int) stream.Position ); + stream.SetLength( stream.Position = offset + length ); + return stream.GetBuffer().AsSpan( offset, length ); + } + + /// Allocates a byte span from the result of . + /// Number of bytes to allocate. + /// The allocated byte span. + private Span< byte > AllocateExpressionSpan( int length ) + { + var stream = GetExpressionStream(); + var offset = unchecked( (int) stream.Position ); + stream.SetLength( stream.Position = offset + length ); + return stream.GetBuffer().AsSpan( offset, length ); + } + + /// Ensures that the current stack top is a string. + /// The string stream. + private MemoryStream GetStringStream() + { + if( _mss[ ^1 ].Type != StackType.String ) + throw new InvalidOperationException( "Strings may not be appended in current state." ); + return _mss[ ^1 ].Stream; + } + + /// Ensures that the current stack top is an expression or payload. + /// The string stream. + private MemoryStream GetExpressionStream() + { + if( _mss[ ^1 ].Type is not StackType.Payload and not StackType.Expression ) + throw new InvalidOperationException( "Expressions may not be appended in current state." ); + if( _mss[ ^1 ].Type is StackType.Expression && _mss[ ^1 ].Ident <= 0 ) + throw new InvalidOperationException( $"No more expressions may be written. Call {nameof( EndExpression )}." ); + return _mss[ ^1 ].Stream; + } + + private void OneExpressionWritten() + { + if( _mss[ ^1 ].Type is StackType.Expression ) + _mss[ ^1 ] = _mss[ ^1 ] with { Ident = _mss[ ^1 ].Ident - 1 }; + } +} \ No newline at end of file From 19414eb6f5fd4f9bb1ca837e69c62a053133cd97 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Wed, 10 Apr 2024 12:43:06 +0900 Subject: [PATCH 2/7] Make things clearer --- src/Lumina/Text/Payloads/LinkPayloadType.cs | 52 ++++ src/Lumina/Text/Payloads/MacroCode.cs | 11 +- .../Text/SeStringBuilder.Expressions.cs | 19 -- src/Lumina/Text/SeStringBuilder.Presets.cs | 186 +++++++++++--- src/Lumina/Text/SeStringBuilder.UIntColors.cs | 227 ++++++++++++++++++ 5 files changed, 436 insertions(+), 59 deletions(-) create mode 100644 src/Lumina/Text/Payloads/LinkPayloadType.cs create mode 100644 src/Lumina/Text/SeStringBuilder.UIntColors.cs diff --git a/src/Lumina/Text/Payloads/LinkPayloadType.cs b/src/Lumina/Text/Payloads/LinkPayloadType.cs new file mode 100644 index 00000000..cbcd2d24 --- /dev/null +++ b/src/Lumina/Text/Payloads/LinkPayloadType.cs @@ -0,0 +1,52 @@ +namespace Lumina.Text.Payloads; + +/// Known values for the first parameter of . +public enum LinkMacroPayloadType +{ + /// A character is linked. + /// Parameters: this, unknown(0), ID in the World sheet, reserved(0), character name. + Character = 0x00, + + /// An item is linked. + /// Parameters: this, ID in the Item sheet, unknown(1), unknown(0), optional display string. + Item = 0x02, + + /// A map position is linked. + /// Parameters: this, packed IDs (TerritoryId << 16 | MapID), raw X, raw Y, probably an optional display string. + MapPosition = 0x03, + + /// A quest is linked. + /// Parameters: this, ID in the Quest sheet, unknown(0), unknown(0), probably an optional display string. + Quest = 0x04, + + /// An achievement is linked. + /// Parameters: this, ID in the Achievement sheet, unknown(0), unknown(0), probably an optional display string. + Achievement = 0x05, + + /// A How-To entry is linked. + /// Parameters: this, ID in the HowTo sheet, unknown(0), unknown(0), probably an optional display string. + HowTo = 0x06, + + /// The party finder dialog is linked. + /// Parameters: this, reserved(0), reserved(0), reserved(0), reserved(""). + PartyFinderNotification = 0x07, + + /// A status is linked. + /// Parameters: this, ID in the Status sheet, unknown(0), unknown(0), probably an optional display string. + Status = 0x08, + + /// A party finder listing is linked. + /// + /// Parameters: this, party finder listing ID, unknown(0), ID in the World sheet(*), probably an optional display string.
+ /// *: ID in the World sheet, or 0x10000 if the party finder listing is not cross-world. The high 16 bits might be a flag, where having LSB set means that + /// there is no World ID attached.
+ PartyFinder = 0x09, + + /// An AkatsukiNote entry is linked. + /// Parameters: this, ID in the AkatsukiNote sheet, unknown(0), unknown(0), probably an optional display string. + AkatsukiNote = 0x0A, + + /// A link is terminated. Akin to </a>. + /// Parameters: this, reserved(0), reserved(0), reserved(0), reserved(""). + Terminator = 0xCE, +} \ No newline at end of file diff --git a/src/Lumina/Text/Payloads/MacroCode.cs b/src/Lumina/Text/Payloads/MacroCode.cs index 7a6bd4c6..d568d327 100644 --- a/src/Lumina/Text/Payloads/MacroCode.cs +++ b/src/Lumina/Text/Payloads/MacroCode.cs @@ -1,6 +1,7 @@ namespace Lumina.Text.Payloads; /// Valid macro payload types. +/// A terminator argument does not mean that an argument is required. public enum MacroCode : byte { /// Sets the reset time to the contextual time storage. @@ -76,6 +77,7 @@ public enum MacroCode : byte /// Adds a non-breaking space. [MacroCodeData( "nbsp", "" )] NonBreakingSpace = 0x1D, + // TODO: how is this different from Icon? [MacroCodeData( null, "n N x" )] Icon2 = 0x1E, /// Adds a hyphen. @@ -101,6 +103,10 @@ public enum MacroCode : byte [MacroCodeData( null, "n x" )] Time = 0x25, [MacroCodeData( null, "n n s x" )] Float = 0x26, + + /// Begins or ends a region of link. + /// Parameters: , numeric argument 1, numeric argument 2, numeric argument 3, display string.
+ /// See comments in for the argument usages.
[MacroCodeData( null, "n n n n s" )] Link = 0x27, /// Adds a column from a sheet. @@ -142,5 +148,8 @@ public enum MacroCode : byte [MacroCodeData( null, "n n x" )] Digit = 0x50, [MacroCodeData( null, "n x" )] Ordinal = 0x51, [MacroCodeData( null, "n n" )] Sound = 0x60, - [MacroCodeData( null, "x" )] LevelPos = 0x61, + + /// Adds a formatted map name and corresponding coordinates, in the format of Map Name\n( X , Y ). + /// Parameters: row ID in Level sheet, terminator. + [MacroCodeData( null, "n x" )] LevelPos = 0x61, } \ No newline at end of file diff --git a/src/Lumina/Text/SeStringBuilder.Expressions.cs b/src/Lumina/Text/SeStringBuilder.Expressions.cs index 12274edb..22eb6b33 100644 --- a/src/Lumina/Text/SeStringBuilder.Expressions.cs +++ b/src/Lumina/Text/SeStringBuilder.Expressions.cs @@ -6,16 +6,6 @@ namespace Lumina.Text; /// A builder for . public sealed partial class SeStringBuilder { - /// Appends an integer calculated from RGBA values as an expression. - /// A normalized RGBA value to append. - /// A reference of this instance after the append operation is completed. - public SeStringBuilder AppendRgbaIntExpression( System.Numerics.Vector4 rgba ) => - AppendRgbaIntExpression( - (byte) Math.Clamp( 256 * rgba.X, 0, 255 ), - (byte) Math.Clamp( 256 * rgba.Y, 0, 255 ), - (byte) Math.Clamp( 256 * rgba.Z, 0, 255 ), - (byte) Math.Clamp( 256 * rgba.W, 0, 255 ) ); - /// Appends an integer as an expression. /// An integer value to append. /// A reference of this instance after the append operation is completed. @@ -31,15 +21,6 @@ public SeStringBuilder AppendUIntExpression( uint value ) return this; } - /// Appends an integer calculated from RGBA values as an expression. - /// A red byte value to append. - /// A red byte value to append. - /// A red byte value to append. - /// A red byte value to append. - /// A reference of this instance after the append operation is completed. - public SeStringBuilder AppendRgbaIntExpression( byte r, byte g, byte b, byte a ) => - AppendUIntExpression( ( (uint) a << 24 ) | ( (uint) b << 16 ) | ( (uint) g << 8 ) | r ); - /// Appends a nullary expression. /// A nullary expression type. /// A reference of this instance after the append operation is completed. diff --git a/src/Lumina/Text/SeStringBuilder.Presets.cs b/src/Lumina/Text/SeStringBuilder.Presets.cs index 6633e4b5..333e8497 100644 --- a/src/Lumina/Text/SeStringBuilder.Presets.cs +++ b/src/Lumina/Text/SeStringBuilder.Presets.cs @@ -41,7 +41,7 @@ public SeStringBuilder AppendStringExpression( ReadOnlySeStringSpan rosss ) => /// Appends a string expression. /// The value. /// A reference of this instance after the append operation is completed. - public SeStringBuilder AppendStringExpression( ReadOnlySpan str ) => + public SeStringBuilder AppendStringExpression( ReadOnlySpan< char > str ) => BeginStringExpression().Append( str ).EndExpression(); /// Appends an icon. @@ -86,56 +86,164 @@ public SeStringBuilder AppendBold( ReadOnlySpan< char > str ) => public SeStringBuilder AppendSetBold( bool enable ) => BeginMacro( MacroCode.Bold ).AppendUIntExpression( enable ? 1u : 0u ).EndMacro(); - /// Pushes a text foreground color. - /// The RGBA color value. - /// A reference of this instance after the append operation is completed. - public SeStringBuilder PushColor( uint rgba ) => - BeginMacro( MacroCode.Color ).AppendUIntExpression( rgba ).EndMacro(); - - /// Pops a text foreground color. - /// A reference of this instance after the append operation is completed. - public SeStringBuilder PopColor() => - BeginMacro( MacroCode.Color ).AppendNullaryExpression( ExpressionType.StackColor ).EndMacro(); - - /// Pushes a text border color. - /// The RGBA color value. - /// A reference of this instance after the append operation is completed. - public SeStringBuilder PushEdgeColor( uint rgba ) => - BeginMacro( MacroCode.EdgeColor ).AppendUIntExpression( rgba ).EndMacro(); - - /// Pops a text border color. - /// A reference of this instance after the append operation is completed. - public SeStringBuilder PopEdgeColor() => - BeginMacro( MacroCode.EdgeColor ).AppendNullaryExpression( ExpressionType.StackColor ).EndMacro(); - - /// Pushes a text shadow color. - /// The RGBA color value. - /// A reference of this instance after the append operation is completed. - public SeStringBuilder PushShadowColor( uint rgba ) => - BeginMacro( MacroCode.ShadowColor ).AppendUIntExpression( rgba ).EndMacro(); - - /// Pops a text shadow color. - /// A reference of this instance after the append operation is completed. - public SeStringBuilder PopShadowColor() => - BeginMacro( MacroCode.ShadowColor ).AppendNullaryExpression( ExpressionType.StackColor ).EndMacro(); + /// Pushes a link. + /// Type of the link. + /// The 1st argument. + /// The 2nd argument. + /// The 3rd argument. + /// A reference of this instance after the push operation is completed. + /// Nested link can be done, but only the outermost link will be handled by the game, as of patch 6.58. + public SeStringBuilder PushLink( LinkMacroPayloadType type, uint arg1, uint arg2, uint arg3 ) => + BeginMacro( MacroCode.Link ) + .AppendUIntExpression( (uint) type ) + .AppendUIntExpression( arg1 ) + .AppendUIntExpression( arg2 ) + .AppendUIntExpression( arg3 ); + + /// Pushes a link. + /// Type of the link. + /// The 1st argument. + /// The 2nd argument. + /// The 3rd argument. + /// The optional display name. + /// A reference of this instance after the push operation is completed. + /// Nested link can be done, but only the outermost link will be handled by the game, as of patch 6.58. + public SeStringBuilder PushLink( LinkMacroPayloadType type, uint arg1, uint arg2, uint arg3, ReadOnlySeStringSpan displayName ) => + BeginMacro( MacroCode.Link ) + .AppendUIntExpression( (uint) type ) + .AppendUIntExpression( arg1 ) + .AppendUIntExpression( arg2 ) + .AppendUIntExpression( arg3 ) + .AppendStringExpression( displayName ); + + /// Pushes a link. + /// Type of the link. + /// The 1st argument. + /// The 2nd argument. + /// The 3rd argument. + /// The optional display name. + /// A reference of this instance after the push operation is completed. + /// Nested link can be done, but only the outermost link will be handled by the game, as of patch 6.58. + public SeStringBuilder PushLink( LinkMacroPayloadType type, uint arg1, uint arg2, uint arg3, ReadOnlySpan< char > displayName ) => + BeginMacro( MacroCode.Link ) + .AppendUIntExpression( (uint) type ) + .AppendUIntExpression( arg1 ) + .AppendUIntExpression( arg2 ) + .AppendUIntExpression( arg3 ) + .AppendStringExpression( displayName ); + + /// Pushes a link to a character. + /// The name of the target player. + /// Optional world ID. + /// A reference of this instance after the push operation is completed. + public SeStringBuilder PushLinkCharacter( ReadOnlySpan< char > characterName, uint worldId = 0u ) => + PushLink( LinkMacroPayloadType.Character, 0u, worldId, 0u, characterName ); + + /// Pushes a link to an item. + /// The item ID. + /// A reference of this instance after the push operation is completed. + public SeStringBuilder PushLinkItem( uint itemId ) => + PushLink( LinkMacroPayloadType.Item, itemId, 1u, 0u ); + + /// Pushes a link to an item. + /// The item ID. + /// The display name. + /// A reference of this instance after the push operation is completed. + public SeStringBuilder PushLinkItem( uint itemId, ReadOnlySpan< char > displayName ) => + PushLink( LinkMacroPayloadType.Item, itemId, 1u, 0u, displayName ); + + /// Pushes a link to a map position. + /// The territory ID. + /// The map ID. + /// The raw X coordinate. + /// The raw Y coordinate. + /// A reference of this instance after the push operation is completed. + public SeStringBuilder PushLinkMapPosition( uint territoryId, uint mapId, int rawX, int rawY ) + { + if( territoryId > ushort.MaxValue ) + throw new ArgumentOutOfRangeException( nameof( territoryId ), territoryId, null ); + if( mapId > ushort.MaxValue ) + throw new ArgumentOutOfRangeException( nameof( mapId ), mapId, null ); + return PushLink( + LinkMacroPayloadType.MapPosition, + ( territoryId << 16 ) | mapId, + unchecked( (uint) rawX ), + unchecked( (uint) rawY ), + default( ReadOnlySeStringSpan ) ); + } + + /// Pushes a link to a quest. + /// The quest ID. + /// A reference of this instance after the push operation is completed. + public SeStringBuilder PushLinkQuest(uint questId) => PushLink( LinkMacroPayloadType.Quest, questId, 0u, 0u ); + + /// Pushes a link to an achievement. + /// The achievement ID. + /// A reference of this instance after the push operation is completed. + public SeStringBuilder PushLinkAchievement(uint achievementId) => PushLink( LinkMacroPayloadType.Achievement, achievementId, 0u, 0u ); + + /// Pushes a link to a How-To entry. + /// The How-To entry ID. + /// A reference of this instance after the push operation is completed. + public SeStringBuilder PushLinkHowTo(uint howToId) => PushLink( LinkMacroPayloadType.HowTo, howToId, 0u, 0u ); + + /// Pushes a link to the party finder dialog. + /// A reference of this instance after the push operation is completed. + public SeStringBuilder PushLinkPartyFinderNotification() => PushLink( LinkMacroPayloadType.PartyFinderNotification, 0u, 0u, 0u ); + + /// Pushes a link to a status. + /// The status ID. + /// A reference of this instance after the push operation is completed. + public SeStringBuilder PushLinkStatus(uint statusId) => PushLink( LinkMacroPayloadType.Status, statusId, 0u, 0u, " "u8 ); + + /// Pushes a link to a cross-world party finder listing entry. + /// The party finder listing ID. + /// The party finder owner's world ID. + /// A reference of this instance after the push operation is completed. + public SeStringBuilder PushLinkPartyFinderCrossWorld(uint listingId, uint worldId) + { + if( worldId > ushort.MaxValue ) + throw new ArgumentOutOfRangeException( nameof( worldId ), worldId, null ); + return PushLink( LinkMacroPayloadType.PartyFinder, listingId, 0u, worldId ); + } + + /// Pushes a link to a party finder listing entry. + /// The party finder listing ID. + /// A reference of this instance after the push operation is completed. + public SeStringBuilder PushLinkPartyFinder(uint listingId) => PushLink( LinkMacroPayloadType.PartyFinder, listingId, 0u, 0x10000u ); + + /// Pushes a link to an AkatsukiNote entry. + /// The AkatsukiNote entry ID. + /// A reference of this instance after the push operation is completed. + public SeStringBuilder PushLinkAkatsukiNote(uint akatsukiNoteId) => PushLink( LinkMacroPayloadType.AkatsukiNote, akatsukiNoteId, 0u, 0u ); + + /// Pops a link. + /// A reference of this instance after the pop operation is completed. + public SeStringBuilder PopLink() => PushLink( LinkMacroPayloadType.Terminator, 0, 0, 0, default( ReadOnlySeStringSpan ) ); /// Pushes a text foreground color from UIColor sheet. - /// The row ID in the UIColor sheet. - /// A reference of this instance after the append operation is completed. + /// The row ID in the UIColor sheet. Specifying 0 will pop the color. + /// A reference of this instance after the push operation is completed. + /// UIColor sheet contains color values as 0xRRGGBBAA (0xAA 0xBB 0xGG 0xRR), and should be treated as having ABGR pixel format. + /// See RGB/BGR color model + /// if the naming gets confusing. public SeStringBuilder PushColorType( uint uiColorRowId ) => BeginMacro( MacroCode.ColorType ).AppendUIntExpression( uiColorRowId ).EndMacro(); /// Pops a text foreground color pushed with . - /// A reference of this instance after the append operation is completed. + /// A reference of this instance after the pop operation is completed. public SeStringBuilder PopColorType() => PushColorType( 0u ); /// Pushes a text border color from UIColor sheet. - /// The row ID in the UIColor sheet. - /// A reference of this instance after the append operation is completed. + /// The row ID in the UIColor sheet. Specifying 0 will pop the color. + /// A reference of this instance after the push operation is completed. + /// UIColor sheet contains color values as 0xRRGGBBAA (0xAA 0xBB 0xGG 0xRR), and should be treated as having ABGR pixel format. + /// See RGB/BGR color model + /// if the naming gets confusing. public SeStringBuilder PushEdgeColorType( uint uiColorRowId ) => BeginMacro( MacroCode.EdgeColorType ).AppendUIntExpression( uiColorRowId ).EndMacro(); /// Pops a text border color pushed with . - /// A reference of this instance after the append operation is completed. + /// A reference of this instance after the pop operation is completed. public SeStringBuilder PopEdgeColorType() => PushEdgeColorType( 0u ); } \ No newline at end of file diff --git a/src/Lumina/Text/SeStringBuilder.UIntColors.cs b/src/Lumina/Text/SeStringBuilder.UIntColors.cs new file mode 100644 index 00000000..07a30f4b --- /dev/null +++ b/src/Lumina/Text/SeStringBuilder.UIntColors.cs @@ -0,0 +1,227 @@ +using System; +using System.Numerics; +using Lumina.Text.Expressions; +using Lumina.Text.Payloads; + +namespace Lumina.Text; + +/// A builder for . +public sealed partial class SeStringBuilder +{ + /// Appends a BGRA integer calculated from RGBA values as an expression. + /// A normalized RGBA value to append. + /// A reference of this instance after the append operation is completed. + /// See RGB/BGR color model + /// if the naming gets confusing. + public SeStringBuilder AppendBgraIntExpressionFromRgba( Vector4 rgba ) => AppendBgraIntExpression( rgba with { X = rgba.Z, Z = rgba.X } ); + + /// Appends a BGRA integer calculated from RGBA values as an expression. + /// A red byte value to append. + /// A green byte value to append. + /// A blue byte value to append. + /// A alpha byte value to append. + /// A reference of this instance after the append operation is completed. + /// See RGB/BGR color model + /// if the naming gets confusing. + public SeStringBuilder AppendBgraIntExpressionFromRgba( byte r, byte g, byte b, byte a ) => AppendBgraIntExpression( b, g, r, a ); + + /// Appends a BGRA integer calculated from BGRA values as an expression. + /// A normalized RGBA value to append. + /// A reference of this instance after the append operation is completed. + /// See RGB/BGR color model + /// if the naming gets confusing. + public SeStringBuilder AppendBgraIntExpression( Vector4 rgba ) => + AppendBgraIntExpression( + (byte) Math.Clamp( 256 * rgba.X, 0, 255 ), + (byte) Math.Clamp( 256 * rgba.Y, 0, 255 ), + (byte) Math.Clamp( 256 * rgba.Z, 0, 255 ), + (byte) Math.Clamp( 256 * rgba.W, 0, 255 ) ); + + /// Appends a BGRA integer calculated from BGRA values as an expression. + /// A blue byte value to append. + /// A green byte value to append. + /// A red byte value to append. + /// A alpha byte value to append. + /// A reference of this instance after the append operation is completed. + /// See RGB/BGR color model + /// if the naming gets confusing. + public SeStringBuilder AppendBgraIntExpression( byte b, byte g, byte r, byte a ) => + AppendUIntExpression( ( (uint) a << 24 ) | ( (uint) r << 16 ) | ( (uint) g << 8 ) | b ); + + /// Pushes a text foreground color. + /// A blue byte value to append. + /// A green byte value to append. + /// A red byte value to append. + /// A alpha byte value to append. + /// A reference of this instance after the push operation is completed. + /// See RGB/BGR color model + /// if the naming gets confusing. + public SeStringBuilder PushColorBgra( byte b, byte g, byte r, byte a ) => + BeginMacro( MacroCode.Color ).AppendBgraIntExpression( b, g, r, a ).EndMacro(); + + /// Pushes a text foreground color. + /// The BGRA color value, which is 0xAARRGGBB (0xBB 0xGG 0xRR 0xAA) on Windows version. + /// A reference of this instance after the push operation is completed. + /// See RGB/BGR color model + /// if the naming gets confusing. + public SeStringBuilder PushColorBgra( uint bgra ) => + BeginMacro( MacroCode.Color ).AppendUIntExpression( bgra ).EndMacro(); + + /// Pushes a text foreground color. + /// The BGRA color value, which is 0xAARRGGBB (0xBB 0xGG 0xRR 0xAA) on Windows version. + /// A reference of this instance after the push operation is completed. + /// See RGB/BGR color model + /// if the naming gets confusing. + public SeStringBuilder PushColorBgra( Vector4 bgra ) => + BeginMacro( MacroCode.Color ).AppendBgraIntExpression( bgra ).EndMacro(); + + /// Pushes a text foreground color. + /// A red byte value to append. + /// A green byte value to append. + /// A blue byte value to append. + /// A alpha byte value to append. + /// A reference of this instance after the push operation is completed. + /// See RGB/BGR color model + /// if the naming gets confusing. + public SeStringBuilder PushColorRgba( byte r, byte g, byte b, byte a ) => + BeginMacro( MacroCode.Color ).AppendBgraIntExpressionFromRgba( r, g, b, a ).EndMacro(); + + /// Pushes a text foreground color. + /// The RGBA color value, which is 0xAABBGGRR (0xRR 0xGG 0xBB 0xAA) on Windows version. + /// A reference of this instance after the push operation is completed. + /// See RGB/BGR color model + /// if the naming gets confusing. + public SeStringBuilder PushColorRgba( uint rgba ) => + BeginMacro( MacroCode.Color ).AppendUIntExpression( ( rgba & 0xFF00FF00 ) | ( rgba >> 16 ) | ( ( rgba & 0xFF ) << 16 ) ).EndMacro(); + + /// Pushes a text foreground color. + /// The BGRA color value, which is 0xAARRGGBB (0xBB 0xGG 0xRR 0xAA) on Windows version. + /// A reference of this instance after the push operation is completed. + /// See RGB/BGR color model + /// if the naming gets confusing. + public SeStringBuilder PushColorRgba( Vector4 bgra ) => + BeginMacro( MacroCode.Color ).AppendBgraIntExpressionFromRgba( bgra ).EndMacro(); + + /// Pops a text foreground color. + /// A reference of this instance after the pop operation is completed. + public SeStringBuilder PopColor() => + BeginMacro( MacroCode.Color ).AppendNullaryExpression( ExpressionType.StackColor ).EndMacro(); + + /// Pushes a text edge color. + /// A blue byte value to append. + /// A green byte value to append. + /// A red byte value to append. + /// A alpha byte value to append. + /// A reference of this instance after the push operation is completed. + /// See RGB/BGR color model + /// if the naming gets confusing. + public SeStringBuilder PushEdgeColorBgra( byte b, byte g, byte r, byte a ) => + BeginMacro( MacroCode.EdgeColor ).AppendBgraIntExpression( b, g, r, a ).EndMacro(); + + /// Pushes a text edge color. + /// The BGRA color value, which is 0xAARRGGBB (0xBB 0xGG 0xRR 0xAA) on Windows version. + /// A reference of this instance after the push operation is completed. + /// See RGB/BGR color model + /// if the naming gets confusing. + public SeStringBuilder PushEdgeColorBgra( uint bgra ) => + BeginMacro( MacroCode.EdgeColor ).AppendUIntExpression( bgra ).EndMacro(); + + /// Pushes a text edge color. + /// The BGRA color value, which is 0xAARRGGBB (0xBB 0xGG 0xRR 0xAA) on Windows version. + /// A reference of this instance after the push operation is completed. + /// See RGB/BGR color model + /// if the naming gets confusing. + public SeStringBuilder PushEdgeColorBgra( Vector4 bgra ) => + BeginMacro( MacroCode.EdgeColor ).AppendBgraIntExpression( bgra ).EndMacro(); + + /// Pushes a text edge color. + /// A red byte value to append. + /// A green byte value to append. + /// A blue byte value to append. + /// A alpha byte value to append. + /// A reference of this instance after the push operation is completed. + /// See RGB/BGR color model + /// if the naming gets confusing. + public SeStringBuilder PushEdgeColorRgba( byte r, byte g, byte b, byte a ) => + BeginMacro( MacroCode.EdgeColor ).AppendBgraIntExpressionFromRgba( r, g, b, a ).EndMacro(); + + /// Pushes a text edge color. + /// The RGBA color value, which is 0xAABBGGRR (0xRR 0xGG 0xBB 0xAA) on Windows version. + /// A reference of this instance after the push operation is completed. + /// See RGB/BGR color model + /// if the naming gets confusing. + public SeStringBuilder PushEdgeColorRgba( uint rgba ) => + BeginMacro( MacroCode.EdgeColor ).AppendUIntExpression( ( rgba & 0xFF00FF00 ) | ( rgba >> 16 ) | ( ( rgba & 0xFF ) << 16 ) ).EndMacro(); + + /// Pushes a text edge color. + /// The BGRA color value, which is 0xAARRGGBB (0xBB 0xGG 0xRR 0xAA) on Windows version. + /// A reference of this instance after the push operation is completed. + /// See RGB/BGR color model + /// if the naming gets confusing. + public SeStringBuilder PushEdgeColorRgba( Vector4 bgra ) => + BeginMacro( MacroCode.EdgeColor ).AppendBgraIntExpressionFromRgba( bgra ).EndMacro(); + + /// Pops a text border color. + /// A reference of this instance after the pop operation is completed. + public SeStringBuilder PopEdgeColor() => + BeginMacro( MacroCode.EdgeColor ).AppendNullaryExpression( ExpressionType.StackColor ).EndMacro(); + + /// Pushes a text shadow color. + /// A blue byte value to append. + /// A green byte value to append. + /// A red byte value to append. + /// A alpha byte value to append. + /// A reference of this instance after the push operation is completed. + /// See RGB/BGR color model + /// if the naming gets confusing. + public SeStringBuilder PushShadowColorBgra( byte b, byte g, byte r, byte a ) => + BeginMacro( MacroCode.ShadowColor ).AppendBgraIntExpression( b, g, r, a ).EndMacro(); + + /// Pushes a text shadow color. + /// The BGRA color value, which is 0xAARRGGBB (0xBB 0xGG 0xRR 0xAA) on Windows version. + /// A reference of this instance after the push operation is completed. + /// See RGB/BGR color model + /// if the naming gets confusing. + public SeStringBuilder PushShadowColorBgra( uint bgra ) => + BeginMacro( MacroCode.ShadowColor ).AppendUIntExpression( bgra ).EndMacro(); + + /// Pushes a text shadow color. + /// The BGRA color value, which is 0xAARRGGBB (0xBB 0xGG 0xRR 0xAA) on Windows version. + /// A reference of this instance after the push operation is completed. + /// See RGB/BGR color model + /// if the naming gets confusing. + public SeStringBuilder PushShadowColorBgra( Vector4 bgra ) => + BeginMacro( MacroCode.ShadowColor ).AppendBgraIntExpression( bgra ).EndMacro(); + + /// Pushes a text shadow color. + /// A red byte value to append. + /// A green byte value to append. + /// A blue byte value to append. + /// A alpha byte value to append. + /// A reference of this instance after the push operation is completed. + /// See RGB/BGR color model + /// if the naming gets confusing. + public SeStringBuilder PushShadowColorRgba( byte r, byte g, byte b, byte a ) => + BeginMacro( MacroCode.ShadowColor ).AppendBgraIntExpressionFromRgba( r, g, b, a ).EndMacro(); + + /// Pushes a text shadow color. + /// The RGBA color value, which is 0xAABBGGRR (0xRR 0xGG 0xBB 0xAA) on Windows version. + /// A reference of this instance after the push operation is completed. + /// See RGB/BGR color model + /// if the naming gets confusing. + public SeStringBuilder PushShadowColorRgba( uint rgba ) => + BeginMacro( MacroCode.ShadowColor ).AppendUIntExpression( ( rgba & 0xFF00FF00 ) | ( rgba >> 16 ) | ( ( rgba & 0xFF ) << 16 ) ).EndMacro(); + + /// Pushes a text shadow color. + /// The BGRA color value, which is 0xAARRGGBB (0xBB 0xGG 0xRR 0xAA) on Windows version. + /// A reference of this instance after the push operation is completed. + /// See RGB/BGR color model + /// if the naming gets confusing. + public SeStringBuilder PushShadowColorRgba( Vector4 bgra ) => + BeginMacro( MacroCode.ShadowColor ).AppendBgraIntExpressionFromRgba( bgra ).EndMacro(); + + /// Pops a text shadow color. + /// A reference of this instance after the pop operation is completed. + public SeStringBuilder PopShadowColor() => + BeginMacro( MacroCode.ShadowColor ).AppendNullaryExpression( ExpressionType.StackColor ).EndMacro(); +} \ No newline at end of file From 5c04ee914fbc75dfea81ca2dedd17a017cde6478 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Wed, 10 Apr 2024 12:44:41 +0900 Subject: [PATCH 3/7] fix --- src/Lumina/Text/SeStringBuilder.Presets.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Lumina/Text/SeStringBuilder.Presets.cs b/src/Lumina/Text/SeStringBuilder.Presets.cs index 333e8497..45156372 100644 --- a/src/Lumina/Text/SeStringBuilder.Presets.cs +++ b/src/Lumina/Text/SeStringBuilder.Presets.cs @@ -98,7 +98,8 @@ public SeStringBuilder PushLink( LinkMacroPayloadType type, uint arg1, uint arg2 .AppendUIntExpression( (uint) type ) .AppendUIntExpression( arg1 ) .AppendUIntExpression( arg2 ) - .AppendUIntExpression( arg3 ); + .AppendUIntExpression( arg3 ) + .EndMacro(); /// Pushes a link. /// Type of the link. @@ -114,7 +115,8 @@ public SeStringBuilder PushLink( LinkMacroPayloadType type, uint arg1, uint arg2 .AppendUIntExpression( arg1 ) .AppendUIntExpression( arg2 ) .AppendUIntExpression( arg3 ) - .AppendStringExpression( displayName ); + .AppendStringExpression( displayName ) + .EndMacro(); /// Pushes a link. /// Type of the link. @@ -130,7 +132,8 @@ public SeStringBuilder PushLink( LinkMacroPayloadType type, uint arg1, uint arg2 .AppendUIntExpression( arg1 ) .AppendUIntExpression( arg2 ) .AppendUIntExpression( arg3 ) - .AppendStringExpression( displayName ); + .AppendStringExpression( displayName ) + .EndMacro(); /// Pushes a link to a character. /// The name of the target player. From baa0e2f1cb9b356ed72883935bb10616c5321728 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Wed, 10 Apr 2024 12:57:03 +0900 Subject: [PATCH 4/7] Doc fix --- src/Lumina/Text/SeStringBuilder.UIntColors.cs | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/src/Lumina/Text/SeStringBuilder.UIntColors.cs b/src/Lumina/Text/SeStringBuilder.UIntColors.cs index 07a30f4b..7c822abb 100644 --- a/src/Lumina/Text/SeStringBuilder.UIntColors.cs +++ b/src/Lumina/Text/SeStringBuilder.UIntColors.cs @@ -25,6 +25,14 @@ public sealed partial class SeStringBuilder /// if the naming gets confusing. public SeStringBuilder AppendBgraIntExpressionFromRgba( byte r, byte g, byte b, byte a ) => AppendBgraIntExpression( b, g, r, a ); + /// Appends a BGRA integer calculated from RGBA values as an expression. + /// The RGBA color value, which is 0xAABBGGRR (0xRR 0xGG 0xBB 0xAA) on Windows version. + /// A reference of this instance after the append operation is completed. + /// See RGB/BGR color model + /// if the naming gets confusing. + public SeStringBuilder AppendBgraIntExpressionFromRgba( uint rgba ) => + AppendUIntExpression( ( rgba & 0xFF00FF00 ) | ( rgba >> 16 ) | ( ( rgba & 0xFF ) << 16 ) ); + /// Appends a BGRA integer calculated from BGRA values as an expression. /// A normalized RGBA value to append. /// A reference of this instance after the append operation is completed. @@ -92,15 +100,15 @@ public SeStringBuilder PushColorRgba( byte r, byte g, byte b, byte a ) => /// See RGB/BGR color model /// if the naming gets confusing. public SeStringBuilder PushColorRgba( uint rgba ) => - BeginMacro( MacroCode.Color ).AppendUIntExpression( ( rgba & 0xFF00FF00 ) | ( rgba >> 16 ) | ( ( rgba & 0xFF ) << 16 ) ).EndMacro(); + BeginMacro( MacroCode.Color ).AppendBgraIntExpressionFromRgba( rgba ).EndMacro(); /// Pushes a text foreground color. - /// The BGRA color value, which is 0xAARRGGBB (0xBB 0xGG 0xRR 0xAA) on Windows version. + /// The RGBA color value, which is 0xAABBGGRR (0xRR 0xGG 0xBB 0xAA) on Windows version. /// A reference of this instance after the push operation is completed. /// See RGB/BGR color model /// if the naming gets confusing. - public SeStringBuilder PushColorRgba( Vector4 bgra ) => - BeginMacro( MacroCode.Color ).AppendBgraIntExpressionFromRgba( bgra ).EndMacro(); + public SeStringBuilder PushColorRgba( Vector4 rgba ) => + BeginMacro( MacroCode.Color ).AppendBgraIntExpressionFromRgba( rgba ).EndMacro(); /// Pops a text foreground color. /// A reference of this instance after the pop operation is completed. @@ -151,15 +159,15 @@ public SeStringBuilder PushEdgeColorRgba( byte r, byte g, byte b, byte a ) => /// See RGB/BGR color model /// if the naming gets confusing. public SeStringBuilder PushEdgeColorRgba( uint rgba ) => - BeginMacro( MacroCode.EdgeColor ).AppendUIntExpression( ( rgba & 0xFF00FF00 ) | ( rgba >> 16 ) | ( ( rgba & 0xFF ) << 16 ) ).EndMacro(); + BeginMacro( MacroCode.EdgeColor ).AppendBgraIntExpressionFromRgba( rgba ).EndMacro(); /// Pushes a text edge color. - /// The BGRA color value, which is 0xAARRGGBB (0xBB 0xGG 0xRR 0xAA) on Windows version. + /// The RGBA color value, which is 0xAABBGGRR (0xRR 0xGG 0xBB 0xAA) on Windows version. /// A reference of this instance after the push operation is completed. /// See RGB/BGR color model /// if the naming gets confusing. - public SeStringBuilder PushEdgeColorRgba( Vector4 bgra ) => - BeginMacro( MacroCode.EdgeColor ).AppendBgraIntExpressionFromRgba( bgra ).EndMacro(); + public SeStringBuilder PushEdgeColorRgba( Vector4 rgba ) => + BeginMacro( MacroCode.EdgeColor ).AppendBgraIntExpressionFromRgba( rgba ).EndMacro(); /// Pops a text border color. /// A reference of this instance after the pop operation is completed. @@ -210,15 +218,15 @@ public SeStringBuilder PushShadowColorRgba( byte r, byte g, byte b, byte a ) => /// See RGB/BGR color model /// if the naming gets confusing. public SeStringBuilder PushShadowColorRgba( uint rgba ) => - BeginMacro( MacroCode.ShadowColor ).AppendUIntExpression( ( rgba & 0xFF00FF00 ) | ( rgba >> 16 ) | ( ( rgba & 0xFF ) << 16 ) ).EndMacro(); + BeginMacro( MacroCode.ShadowColor ).AppendBgraIntExpressionFromRgba( rgba ).EndMacro(); /// Pushes a text shadow color. - /// The BGRA color value, which is 0xAARRGGBB (0xBB 0xGG 0xRR 0xAA) on Windows version. + /// The RGBA color value, which is 0xAABBGGRR (0xRR 0xGG 0xBB 0xAA) on Windows version. /// A reference of this instance after the push operation is completed. /// See RGB/BGR color model /// if the naming gets confusing. - public SeStringBuilder PushShadowColorRgba( Vector4 bgra ) => - BeginMacro( MacroCode.ShadowColor ).AppendBgraIntExpressionFromRgba( bgra ).EndMacro(); + public SeStringBuilder PushShadowColorRgba( Vector4 rgba ) => + BeginMacro( MacroCode.ShadowColor ).AppendBgraIntExpressionFromRgba( rgba ).EndMacro(); /// Pops a text shadow color. /// A reference of this instance after the pop operation is completed. From 6feefbf40a143d1e8d0c72b8b43e62a79c86841b Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Wed, 10 Apr 2024 16:27:51 +0200 Subject: [PATCH 5/7] Add more MacroCode docs --- src/Lumina/Text/Payloads/MacroCode.cs | 48 ++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/src/Lumina/Text/Payloads/MacroCode.cs b/src/Lumina/Text/Payloads/MacroCode.cs index d568d327..aab15d7b 100644 --- a/src/Lumina/Text/Payloads/MacroCode.cs +++ b/src/Lumina/Text/Payloads/MacroCode.cs @@ -20,8 +20,16 @@ public enum MacroCode : byte /// Parameters: condition, expression to use if condition is 1, expression to use if condition is 2, and so on. [MacroCodeData( null, ". . ." )] Switch = 0x09, + /// Adds a characters name. + /// Parameters: ObjectId, terminator. [MacroCodeData( null, "n x" )] PcName = 0x0A, + + /// Tests a characters gender. + /// Parameters: ObjectId, expression to use if the character is male, expression to use if the character is female, terminator. [MacroCodeData( null, "n . . x" )] IfPcGender = 0x0B, + + /// Tests a characters name. + /// Parameters: ObjectId, the name to test against, expression to use if the name matches, expression to use if the name doesn't match, terminator. [MacroCodeData( null, "n . . . x" )] IfPcName = 0x0C, /// Determines the type of josa required from the last character of the first expression. @@ -32,6 +40,8 @@ public enum MacroCode : byte /// Parameters: test string, ro suffix, euro suffix, terminator. [MacroCodeData( null, "s s s x" )] Josaro = 0x0E, + /// Tests if the character is the local player. + /// Parameters: ObjectId, expression to use if the character is the local player, expression to use if the character is not the local player, terminator. [MacroCodeData( null, "n . . x" )] IfSelf = 0x0F, /// Adds a line break. @@ -95,6 +105,8 @@ public enum MacroCode : byte /// Parameters: integer expression, separator (usually a comma or a dot), terminator. [MacroCodeData( null, ". s x" )] Kilo = 0x22, + /// Adds a readable byte string (possible suffixes: omitted, K, M, G, T). + /// Parameters: integer expression, terminator. [MacroCodeData( null, "n x" )] Byte = 0x23, /// Adds a zero-padded-to-two-digits decimal representation of an integer expression. @@ -102,6 +114,9 @@ public enum MacroCode : byte [MacroCodeData( null, "n x" )] Sec = 0x24, [MacroCodeData( null, "n x" )] Time = 0x25, + + /// Adds a floating point number as text. + /// Parameters: integer expression, radix, separator, terminator. [MacroCodeData( null, "n n s x" )] Float = 0x26, /// Begins or ends a region of link. @@ -110,7 +125,7 @@ public enum MacroCode : byte [MacroCodeData( null, "n n n n s" )] Link = 0x27, /// Adds a column from a sheet. - /// Parameters: sheet name, row ID, column ID(, ?). + /// Parameters: sheet name, row ID, column index, expression passed as first local parameter to the columns text. [MacroCodeData( null, "s . . ." )] Sheet = 0x28, /// Adds a string expression as-is. @@ -121,20 +136,44 @@ public enum MacroCode : byte /// Parameters: string expression, terminator. [MacroCodeData( null, "s x" )] Caps = 0x2A, + /// Adds a string, first character upper cased. + /// Parameters: string expression, terminator. [MacroCodeData( null, "s x" )] Head = 0x2B, + [MacroCodeData( null, "s s n x" )] Split = 0x2C, + + /// Adds a string, every words first character upper cased. + /// Parameters: string expression, terminator. [MacroCodeData( null, "s x" )] HeadAll = 0x2D, + [MacroCodeData( null, "n n . . ." )] Fixed = 0x2E, /// Adds a string, fully lower cased. /// Parameters: string expression, terminator. [MacroCodeData( null, "s x" )] Lower = 0x2F, + /// Adds sheet text with proper declension in Japanese. + /// Parameters: sheet name, person, row id, amount, unused, unknown offset. [MacroCodeData( null, "s . ." )] JaNoun = 0x30, + + /// Adds sheet text with proper declension in English. + /// Parameters: sheet name, person, row id, amount, unused, unused. [MacroCodeData( null, "s . ." )] EnNoun = 0x31, + + /// Adds sheet text with proper declension in German. + /// Parameters: sheet name, person, row id, amount, case, unused. [MacroCodeData( null, "s . ." )] DeNoun = 0x32, + + /// Adds sheet text with proper declension in French. + /// Parameters: sheet name, person, row id, amount, unused, unknown offset. [MacroCodeData( null, "s . ." )] FrNoun = 0x33, + + /// Adds sheet text with proper declension in Chinese. + /// Parameters: sheet name, unused, row id, amount, unused, unknown offset. [MacroCodeData( null, "s . ." )] ChNoun = 0x34, + + /// Adds a string, first character lower cased. + /// Parameters: string expression, terminator. [MacroCodeData( null, "s x" )] LowerHead = 0x40, /// Pushes the text foreground color, referring to a color defined in UIColor sheet. @@ -145,8 +184,15 @@ public enum MacroCode : byte /// Parameters: row ID in UIColor sheet or 0 to pop(or reset?) the pushed color, terminator. [MacroCodeData( null, "n x" )] EdgeColorType = 0x49, + /// Adds a zero-prefixed number as text. + /// Parameters: integer expression, target length, terminator. [MacroCodeData( null, "n n x" )] Digit = 0x50, + + /// Adds an ordinal number as text (English only). [MacroCodeData( null, "n x" )] Ordinal = 0x51, + + /// Adds a non-visible sound payload. + /// Parameters: bool whether this sound is a Jingle (see sheet), the id. [MacroCodeData( null, "n n" )] Sound = 0x60, /// Adds a formatted map name and corresponding coordinates, in the format of Map Name\n( X , Y ). From c967105e0ac36767887f52738a61de31f124c733 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Wed, 10 Apr 2024 16:45:01 +0200 Subject: [PATCH 6/7] Rename param displayName to plainText --- src/Lumina/Text/SeStringBuilder.Presets.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Lumina/Text/SeStringBuilder.Presets.cs b/src/Lumina/Text/SeStringBuilder.Presets.cs index 45156372..d46bc8f7 100644 --- a/src/Lumina/Text/SeStringBuilder.Presets.cs +++ b/src/Lumina/Text/SeStringBuilder.Presets.cs @@ -106,16 +106,16 @@ public SeStringBuilder PushLink( LinkMacroPayloadType type, uint arg1, uint arg2 /// The 1st argument. /// The 2nd argument. /// The 3rd argument. - /// The optional display name. + /// The optional plain text. /// A reference of this instance after the push operation is completed. /// Nested link can be done, but only the outermost link will be handled by the game, as of patch 6.58. - public SeStringBuilder PushLink( LinkMacroPayloadType type, uint arg1, uint arg2, uint arg3, ReadOnlySeStringSpan displayName ) => + public SeStringBuilder PushLink( LinkMacroPayloadType type, uint arg1, uint arg2, uint arg3, ReadOnlySeStringSpan plainText ) => BeginMacro( MacroCode.Link ) .AppendUIntExpression( (uint) type ) .AppendUIntExpression( arg1 ) .AppendUIntExpression( arg2 ) .AppendUIntExpression( arg3 ) - .AppendStringExpression( displayName ) + .AppendStringExpression( plainText ) .EndMacro(); /// Pushes a link. @@ -123,16 +123,16 @@ public SeStringBuilder PushLink( LinkMacroPayloadType type, uint arg1, uint arg2 /// The 1st argument. /// The 2nd argument. /// The 3rd argument. - /// The optional display name. + /// The optional plain text. /// A reference of this instance after the push operation is completed. /// Nested link can be done, but only the outermost link will be handled by the game, as of patch 6.58. - public SeStringBuilder PushLink( LinkMacroPayloadType type, uint arg1, uint arg2, uint arg3, ReadOnlySpan< char > displayName ) => + public SeStringBuilder PushLink( LinkMacroPayloadType type, uint arg1, uint arg2, uint arg3, ReadOnlySpan< char > plainText ) => BeginMacro( MacroCode.Link ) .AppendUIntExpression( (uint) type ) .AppendUIntExpression( arg1 ) .AppendUIntExpression( arg2 ) .AppendUIntExpression( arg3 ) - .AppendStringExpression( displayName ) + .AppendStringExpression( plainText ) .EndMacro(); /// Pushes a link to a character. @@ -150,10 +150,10 @@ public SeStringBuilder PushLinkItem( uint itemId ) => /// Pushes a link to an item. /// The item ID. - /// The display name. + /// The item name that is copied to the clipboard. /// A reference of this instance after the push operation is completed. - public SeStringBuilder PushLinkItem( uint itemId, ReadOnlySpan< char > displayName ) => - PushLink( LinkMacroPayloadType.Item, itemId, 1u, 0u, displayName ); + public SeStringBuilder PushLinkItem( uint itemId, ReadOnlySpan< char > plainText ) => + PushLink( LinkMacroPayloadType.Item, itemId, 1u, 0u, plainText ); /// Pushes a link to a map position. /// The territory ID. From db88914bbbec6e04d16625b2ea151ae6412daa16 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Fri, 12 Apr 2024 19:37:37 +0200 Subject: [PATCH 7/7] Words --- src/Lumina/Text/Payloads/MacroCode.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Lumina/Text/Payloads/MacroCode.cs b/src/Lumina/Text/Payloads/MacroCode.cs index aab15d7b..657604d2 100644 --- a/src/Lumina/Text/Payloads/MacroCode.cs +++ b/src/Lumina/Text/Payloads/MacroCode.cs @@ -51,7 +51,7 @@ public enum MacroCode : byte /// Parameters: delay in seconds, terminator. [MacroCodeData( null, "n x" )] Wait = 0x11, - /// Adds an icon from gfdata.gfd. + /// Adds an icon from common/font/gfdata.gfd. /// Parameters: icon ID, terminator. [MacroCodeData( null, "n x" )] Icon = 0x12, @@ -105,7 +105,7 @@ public enum MacroCode : byte /// Parameters: integer expression, separator (usually a comma or a dot), terminator. [MacroCodeData( null, ". s x" )] Kilo = 0x22, - /// Adds a readable byte string (possible suffixes: omitted, K, M, G, T). + /// Adds a human-readable byte string (possible suffixes: omitted, K, M, G, T). /// Parameters: integer expression, terminator. [MacroCodeData( null, "n x" )] Byte = 0x23, @@ -184,14 +184,14 @@ public enum MacroCode : byte /// Parameters: row ID in UIColor sheet or 0 to pop(or reset?) the pushed color, terminator. [MacroCodeData( null, "n x" )] EdgeColorType = 0x49, - /// Adds a zero-prefixed number as text. + /// Adds a zero-padded number as text. /// Parameters: integer expression, target length, terminator. [MacroCodeData( null, "n n x" )] Digit = 0x50, /// Adds an ordinal number as text (English only). [MacroCodeData( null, "n x" )] Ordinal = 0x51, - /// Adds a non-visible sound payload. + /// Adds an invisible sound payload. /// Parameters: bool whether this sound is a Jingle (see sheet), the id. [MacroCodeData( null, "n n" )] Sound = 0x60,