diff --git a/src/Lumina/Text/Expressions/BaseExpression.cs b/src/Lumina/Text/Expressions/BaseExpression.cs index 6b372238..22a0d172 100644 --- a/src/Lumina/Text/Expressions/BaseExpression.cs +++ b/src/Lumina/Text/Expressions/BaseExpression.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Text; namespace Lumina.Text.Expressions; @@ -24,6 +25,18 @@ public abstract class BaseExpression /// Target to write this expression to. public abstract void Encode( Stream stream ); + /// Represent this expression as a part of macro string. + /// Target string builder. + public abstract void AppendMacroStringToStringBuilder( StringBuilder sb ); + + /// + public override string ToString() + { + var sb = new StringBuilder(); + AppendMacroStringToStringBuilder( sb ); + return sb.ToString(); + } + /// /// Parse given Stream into an Expression. /// diff --git a/src/Lumina/Text/Expressions/BinaryExpression.cs b/src/Lumina/Text/Expressions/BinaryExpression.cs index cff78f50..cd50d23d 100644 --- a/src/Lumina/Text/Expressions/BinaryExpression.cs +++ b/src/Lumina/Text/Expressions/BinaryExpression.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Text; namespace Lumina.Text.Expressions; @@ -37,18 +38,37 @@ public BinaryExpression( ExpressionType typeByte, BaseExpression operand1, BaseE public override ExpressionType ExpressionType { get; } /// - public override string ToString() + public override void AppendMacroStringToStringBuilder( StringBuilder sb ) { - return ExpressionType switch + sb.Append( '[' ); + Operand1.AppendMacroStringToStringBuilder( sb ); + + switch( ExpressionType ) { - ExpressionType.GreaterThanOrEqualTo => $"[{Operand1}>={Operand2}]", - ExpressionType.GreaterThan => $"[{Operand1}>{Operand2}]", - ExpressionType.LessThanOrEqualTo => $"[{Operand1}<={Operand2}]", - ExpressionType.LessThan => $"[{Operand1}<{Operand2}]", - ExpressionType.Equal => $"[{Operand1}=={Operand2}]", - ExpressionType.NotEqual => $"[{Operand1}!={Operand2}]", - _ => throw new NotImplementedException() // cannot reach, as this instance is immutable and this field is filtered from constructor - }; + case ExpressionType.GreaterThanOrEqualTo: + sb.Append( ">=" ); + break; + case ExpressionType.GreaterThan: + sb.Append( '>' ); + break; + case ExpressionType.LessThanOrEqualTo: + sb.Append( "<=" ); + break; + case ExpressionType.LessThan: + sb.Append( '<' ); + break; + case ExpressionType.Equal: + sb.Append( "==" ); + break; + case ExpressionType.NotEqual: + sb.Append( "!=" ); + break; + default: + throw new NotSupportedException(); // cannot reach, as this instance is immutable and this field is filtered from constructor + } + + Operand2.AppendMacroStringToStringBuilder( sb ); + sb.Append( ']' ); } /// diff --git a/src/Lumina/Text/Expressions/IntegerExpression.cs b/src/Lumina/Text/Expressions/IntegerExpression.cs index 7f58b135..2bbb813d 100644 --- a/src/Lumina/Text/Expressions/IntegerExpression.cs +++ b/src/Lumina/Text/Expressions/IntegerExpression.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Text; namespace Lumina.Text.Expressions; @@ -94,6 +95,9 @@ public static int CalculateSize( uint value ) /// public override string ToString() => ( (int)Value ).ToString(); + /// + public override void AppendMacroStringToStringBuilder( StringBuilder sb ) => sb.Append( (int) Value ); + /// /// Parse given Stream into an IntegerExpression. /// diff --git a/src/Lumina/Text/Expressions/ParameterExpression.cs b/src/Lumina/Text/Expressions/ParameterExpression.cs index c58c2b64..86efa296 100644 --- a/src/Lumina/Text/Expressions/ParameterExpression.cs +++ b/src/Lumina/Text/Expressions/ParameterExpression.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Text; namespace Lumina.Text.Expressions; @@ -31,16 +32,17 @@ public ParameterExpression( ExpressionType typeByte, BaseExpression operand ) public override ExpressionType ExpressionType { get; } /// - public override string ToString() + public override void AppendMacroStringToStringBuilder( StringBuilder sb ) { - return ExpressionType switch + sb.Append( ExpressionType switch { - ExpressionType.IntegerParameter => $"lnum{Operand}", - ExpressionType.PlayerParameter => $"gnum{Operand}", - ExpressionType.StringParameter => $"lstr{Operand}", - ExpressionType.ObjectParameter => $"gstr{Operand}", - _ => throw new NotImplementedException() // cannot reach, as this instance is immutable and this field is filtered from constructor - }; + ExpressionType.LocalNumber => "lnum", + ExpressionType.GlobalNumber => "gnum", + ExpressionType.LocalString => "lstr", + ExpressionType.GlobalString => "gstr", + _ => throw new NotSupportedException() // cannot reach, as this instance is immutable and this field is filtered from constructor + } ); + Operand.AppendMacroStringToStringBuilder( sb ); } /// diff --git a/src/Lumina/Text/Expressions/PlaceholderExpression.cs b/src/Lumina/Text/Expressions/PlaceholderExpression.cs index 0694c5b9..c2067bbd 100644 --- a/src/Lumina/Text/Expressions/PlaceholderExpression.cs +++ b/src/Lumina/Text/Expressions/PlaceholderExpression.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Text; namespace Lumina.Text.Expressions; @@ -28,9 +29,9 @@ public PlaceholderExpression( ExpressionType expressionType ) public override void Encode( Stream stream ) => stream.WriteByte( (byte)ExpressionType ); /// - public override string ToString() + public override void AppendMacroStringToStringBuilder( StringBuilder sb ) { - return ExpressionType switch + sb.Append( ExpressionType switch { ExpressionType.Millisecond => "t_msec", ExpressionType.Second => "t_sec", @@ -41,8 +42,8 @@ public override string ToString() ExpressionType.Month => "t_mon", ExpressionType.Year => "t_year", ExpressionType.StackColor => "stackcolor", - _ => $"Placeholder#{(byte)ExpressionType:X02}" - }; + _ => $"Placeholder#{(byte) ExpressionType:X02}" + } ); } /// diff --git a/src/Lumina/Text/Expressions/StringExpression.cs b/src/Lumina/Text/Expressions/StringExpression.cs index e71e389a..42b3b608 100644 --- a/src/Lumina/Text/Expressions/StringExpression.cs +++ b/src/Lumina/Text/Expressions/StringExpression.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Linq; +using System.Text; namespace Lumina.Text.Expressions; @@ -45,7 +46,8 @@ public override void Encode( Stream stream ) } /// - public override string ToString() => Value?.ToString() ?? string.Empty; + public override void AppendMacroStringToStringBuilder( StringBuilder sb ) => + Value.AppendMacroStringToStringBuilder( sb, true ); /// /// Parse given Stream into a StringExpression. diff --git a/src/Lumina/Text/Payloads/BasePayload.cs b/src/Lumina/Text/Payloads/BasePayload.cs index a0b219b5..5eb67afb 100644 --- a/src/Lumina/Text/Payloads/BasePayload.cs +++ b/src/Lumina/Text/Payloads/BasePayload.cs @@ -256,5 +256,45 @@ public override string ToString() ex => ex is StringExpression se ? se.Value.ToMacroString() : ex.ToString() ) )})>"; } + + internal void AppendMacroStringToStringBuilder( StringBuilder sb, bool forStringExpression ) + { + if( PayloadType == PayloadType.Text ) + { + foreach( var c in RawString ) + { + switch( forStringExpression ) + { + case true when c is '<' or '>' or '[' or ']' or '(' or ')' or ',' or '\\': + case false when c is '<' or '\\': + sb.Append( '\\' ); + break; + } + + sb.Append( c ); + } + + return; + } + + sb.Append( $"<{( (MacroCode) PayloadType ).GetEncodeName()}" ); + if( Expressions.Count > 0 ) + { + sb.Append( '(' ); + Expressions[ 0 ].AppendMacroStringToStringBuilder( sb ); + if( Expressions.Count > 1 ) + { + for( var i = 1; i < Expressions.Count; i++ ) + { + sb.Append( ',' ); + Expressions[ i ].AppendMacroStringToStringBuilder( sb ); + } + } + + sb.Append( ')' ); + } + + sb.Append( '>' ); + } } } \ No newline at end of file diff --git a/src/Lumina/Text/ReadOnly/ReadOnlySeExpressionSpan.cs b/src/Lumina/Text/ReadOnly/ReadOnlySeExpressionSpan.cs index 088dc64c..65c31a49 100644 --- a/src/Lumina/Text/ReadOnly/ReadOnlySeExpressionSpan.cs +++ b/src/Lumina/Text/ReadOnly/ReadOnlySeExpressionSpan.cs @@ -179,43 +179,90 @@ public override int GetHashCode() /// public override string ToString() + { + var sb = new StringBuilder(); + AppendMacroStringToStringBuilder( sb ); + return sb.ToString(); + } + + /// Writes the encodeable macro representation of this instance of to the given string builder. + /// Target string builder. + /// The encodeable macro representation. + public void AppendMacroStringToStringBuilder( StringBuilder sb ) { if( Body.IsEmpty ) - return "(?)"; + { + sb.Append( "" ); + return; + } if( TryGetUInt( out var u32 ) ) - return u32.ToString(); + { + sb.Append( u32 ); + return; + } if( TryGetString( out var s ) ) - return $"\"{s.ToString().Replace( "\\", "\\\\" ).Replace( "\"", "\\\"" )}\""; + { + s.AppendMacroStringToStringBuilder( sb, true ); + return; + } if( TryGetPlaceholderExpression( out var exprType ) ) { if( ( (ExpressionType) exprType ).GetNativeName() is { } nativeName ) - return nativeName; - return $"?x{exprType:X02}"; + sb.Append( nativeName ); + else + sb.Append( $"" ); + return; } 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( ( (ExpressionType) exprType ).GetNativeName() is not { } nativeName ) + throw new InvalidOperationException( "All native names must be defined for unary expressions." ); + sb.Append( nativeName ); + e1.AppendMacroStringToStringBuilder( sb ); + return; } 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." ); + sb.Append( '[' ); + e1.AppendMacroStringToStringBuilder( sb ); + switch( (ExpressionType)exprType ) + { + case ExpressionType.GreaterThanOrEqualTo: + sb.Append( ">=" ); + break; + case ExpressionType.GreaterThan: + sb.Append( '>' ); + break; + case ExpressionType.LessThanOrEqualTo: + sb.Append( "<=" ); + break; + case ExpressionType.LessThan: + sb.Append( '<' ); + break; + case ExpressionType.Equal: + sb.Append( "==" ); + break; + case ExpressionType.NotEqual: + sb.Append( "!=" ); + break; + default: + throw new NotSupportedException("should not happen"); + } + + e2.AppendMacroStringToStringBuilder( sb ); + sb.Append( ']' ); + return; } - 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(); + sb.EnsureCapacity( sb.Length + 7 + 3 * Body.Length ); + sb.Append( "' ); } } \ No newline at end of file diff --git a/src/Lumina/Text/ReadOnly/ReadOnlySePayloadSpan.cs b/src/Lumina/Text/ReadOnly/ReadOnlySePayloadSpan.cs index 4a016952..ae6a3a34 100644 --- a/src/Lumina/Text/ReadOnly/ReadOnlySePayloadSpan.cs +++ b/src/Lumina/Text/ReadOnly/ReadOnlySePayloadSpan.cs @@ -420,46 +420,81 @@ public override int GetHashCode() /// public override string ToString() + { + var sb = new StringBuilder(); + AppendMacroStringToStringBuilder( sb, false ); + return sb.ToString(); + } + + /// Writes the encodeable macro representation of this instance of to the given string builder. + /// Target string builder. + /// Whether this is being encoded to be used as a string expression. + /// The encodeable macro representation. + public void AppendMacroStringToStringBuilder( StringBuilder sb, bool forStringExpression ) { switch( Type ) { case ReadOnlySePayloadType.Text: - return Encoding.UTF8.GetString( Body ).Replace( "<", "\\<" ); + { + var remaining = Body; + Span< char > buf = stackalloc char[2]; + while( !remaining.IsEmpty ) + { + Rune.DecodeFromUtf8( remaining, out var rune, out var consumed ); + switch( forStringExpression ) + { + case true when rune.Value is '<' or '>' or '[' or ']' or '(' or ')' or ',' or '\\': + case false when rune.Value is '<' or '\\': + sb.Append( '\\' ); + break; + } + + sb.Append( buf[ ..rune.EncodeToUtf16( buf ) ] ); + remaining = remaining[ consumed.. ]; + } + + break; + } case ReadOnlySePayloadType.Macro: { - var sb = new StringBuilder( "<" ); + sb.Append( '<' ); // 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}" ); + sb.Append( $"payload:{(uint) MacroCode:X02}" ); var expre = GetEnumerator(); if( expre.MoveNext() ) { sb.Append( '(' ); - sb.Append( expre.Current.ToString() ); + expre.Current.AppendMacroStringToStringBuilder( sb ); while( expre.MoveNext() ) - sb.Append( ", " ).Append( expre.Current.ToString() ); + { + sb.Append( ',' ); + expre.Current.AppendMacroStringToStringBuilder( sb ); + } + sb.Append( ')' ); } - return sb.Append( '>' ).ToString(); + sb.Append( '>' ); + break; } case var _ when Body.Length == 0: - return ""; + sb.Append( "" ); + break; default: - { - var sb = new StringBuilder( 3 * Body.Length + 3 ); - sb.Append( "' ).ToString(); - } + sb.Append( '>' ); + break; } } diff --git a/src/Lumina/Text/ReadOnly/ReadOnlySeStringSpan.cs b/src/Lumina/Text/ReadOnly/ReadOnlySeStringSpan.cs index 461d24ac..aceb533d 100644 --- a/src/Lumina/Text/ReadOnly/ReadOnlySeStringSpan.cs +++ b/src/Lumina/Text/ReadOnly/ReadOnlySeStringSpan.cs @@ -304,11 +304,20 @@ public override int GetHashCode() public override string ToString() { var sb = new StringBuilder(); - foreach( var v in this ) - sb.Append( v.ToString() ); + AppendMacroStringToStringBuilder( sb, false ); return sb.ToString(); } + /// Writes the encodeable macro representation of this instance of to the given string builder. + /// Target string builder. + /// Whether this is being encoded to be used as a string expression. + /// The encodeable macro representation. + public void AppendMacroStringToStringBuilder( StringBuilder sb, bool forStringExpression ) + { + foreach( var v in this ) + v.AppendMacroStringToStringBuilder( sb, forStringExpression ); + } + /// [MethodImpl( MethodImplOptions.AggressiveInlining )] public Enumerator GetEnumerator() => new( new( this ) ); diff --git a/src/Lumina/Text/SeString.cs b/src/Lumina/Text/SeString.cs index 76d947c7..51c94e8e 100644 --- a/src/Lumina/Text/SeString.cs +++ b/src/Lumina/Text/SeString.cs @@ -187,5 +187,11 @@ public string ToMacroString() { return string.Concat( Payloads ); } + + internal void AppendMacroStringToStringBuilder( StringBuilder sb, bool forStringExpression ) + { + foreach( var p in Payloads ) + p.AppendMacroStringToStringBuilder( sb, forStringExpression ); + } } } \ No newline at end of file