feat(opl): add OplTextBlock multi-line builder
This commit is contained in:
parent
d35a527199
commit
374de148fd
3 changed files with 198 additions and 0 deletions
|
|
@ -0,0 +1,88 @@
|
|||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Server;
|
||||
using Xunit;
|
||||
|
||||
namespace Server.Tests;
|
||||
|
||||
public class OplTextBlockTests
|
||||
{
|
||||
private static (int cliloc, string arg)[] Decode(ObjectPropertyList opl)
|
||||
{
|
||||
opl.Terminate();
|
||||
var buffer = opl.Buffer;
|
||||
var entries = new List<(int, string)>();
|
||||
var pos = 15;
|
||||
while (true)
|
||||
{
|
||||
var cliloc = BinaryPrimitives.ReadInt32BigEndian(buffer.AsSpan(pos));
|
||||
pos += 4;
|
||||
if (cliloc == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var byteLen = BinaryPrimitives.ReadUInt16BigEndian(buffer.AsSpan(pos));
|
||||
pos += 2;
|
||||
entries.Add((cliloc, Encoding.Unicode.GetString(buffer, pos, byteLen)));
|
||||
pos += byteLen;
|
||||
}
|
||||
|
||||
return entries.ToArray();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MultipleLines_JoinIntoSingleCyclingEntry()
|
||||
{
|
||||
var opl = new ObjectPropertyList(null);
|
||||
using (var block = opl.TextBlock())
|
||||
{
|
||||
block.Add("Line One".AsSpan());
|
||||
block.Add("Line Two".AsSpan());
|
||||
}
|
||||
|
||||
var entries = Decode(opl);
|
||||
Assert.Single(entries);
|
||||
Assert.Equal((1042971, "Line One\nLine Two"), entries[0]); // first cycling cliloc
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmptyLines_AreSkipped()
|
||||
{
|
||||
var opl = new ObjectPropertyList(null);
|
||||
using (var block = opl.TextBlock())
|
||||
{
|
||||
block.Add("Only".AsSpan());
|
||||
block.Add(ReadOnlySpan<char>.Empty);
|
||||
}
|
||||
|
||||
Assert.Equal((1042971, "Only"), Decode(opl)[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NoLines_EmitsNothing()
|
||||
{
|
||||
var opl = new ObjectPropertyList(null);
|
||||
using (var block = opl.TextBlock())
|
||||
{
|
||||
// add nothing
|
||||
}
|
||||
|
||||
Assert.Empty(Decode(opl));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Interpolated_AppendsZeroAlloc()
|
||||
{
|
||||
var opl = new ObjectPropertyList(null);
|
||||
var v = 5;
|
||||
using (var block = opl.TextBlock())
|
||||
{
|
||||
block.Add($"Luck Bonus: +{v}%");
|
||||
}
|
||||
|
||||
Assert.Equal((1042971, "Luck Bonus: +5%"), Decode(opl)[0]);
|
||||
}
|
||||
}
|
||||
|
|
@ -155,6 +155,7 @@ public sealed class ObjectPropertyList : IPropertyList, IDisposable
|
|||
|
||||
public void Add(ReadOnlySpan<char> argument) => InternalAdd(GetStringNumber(), argument);
|
||||
public void Add(int number, ReadOnlySpan<char> argument) => InternalAdd(number, argument);
|
||||
public OplTextBlock TextBlock() => new(this);
|
||||
|
||||
private void InternalAdd(int number, ReadOnlySpan<char> chars)
|
||||
{
|
||||
|
|
|
|||
109
Projects/Server/PropertyList/OplTextBlock.cs
Normal file
109
Projects/Server/PropertyList/OplTextBlock.cs
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
/*************************************************************************
|
||||
* ModernUO *
|
||||
* Copyright 2019-2026 - ModernUO Development Team *
|
||||
* Email: hi@modernuo.com *
|
||||
* File: OplTextBlock.cs *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
*************************************************************************/
|
||||
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Server.Text;
|
||||
|
||||
namespace Server;
|
||||
|
||||
// Accumulates '\n'-joined free-text lines and emits ONE cycling passthrough entry on dispose.
|
||||
// Use with `using var block = list.TextBlock();`. ref struct: single-threaded OPL build only.
|
||||
public ref struct OplTextBlock
|
||||
{
|
||||
private readonly ObjectPropertyList _list;
|
||||
internal ValueStringBuilder _builder;
|
||||
private bool _any;
|
||||
|
||||
internal OplTextBlock(ObjectPropertyList list)
|
||||
{
|
||||
_list = list;
|
||||
_builder = ValueStringBuilder.Create();
|
||||
_any = false;
|
||||
}
|
||||
|
||||
public void Add(scoped ReadOnlySpan<char> line)
|
||||
{
|
||||
if (line.IsEmpty)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_builder.Append(line);
|
||||
_builder.Append('\n', 1); // ValueStringBuilder has no single-char Append
|
||||
_any = true;
|
||||
}
|
||||
|
||||
// Zero-alloc interpolated overload: block.Add($"Luck Bonus: +{v}%").
|
||||
public void Add([InterpolatedStringHandlerArgument("")] scoped ref OplInterpolationHandler handler)
|
||||
{
|
||||
var wrote = handler._wrote;
|
||||
this = handler._block; // reconcile possibly-grown builder
|
||||
if (wrote)
|
||||
{
|
||||
_builder.Append('\n', 1);
|
||||
_any = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_any)
|
||||
{
|
||||
// Strip the trailing '\n' (Length >= 2 whenever _any: content + separator).
|
||||
_list.Add(_builder.AsSpan(0, _builder.Length - 1));
|
||||
}
|
||||
|
||||
_builder.Dispose();
|
||||
}
|
||||
|
||||
[InterpolatedStringHandler]
|
||||
public ref struct OplInterpolationHandler
|
||||
{
|
||||
internal OplTextBlock _block;
|
||||
internal bool _wrote;
|
||||
|
||||
public OplInterpolationHandler(int literalLength, int formattedCount, OplTextBlock block)
|
||||
{
|
||||
_block = block;
|
||||
_wrote = false;
|
||||
_block._builder.EnsureCapacity(_block._builder.Length + literalLength + formattedCount * 11);
|
||||
}
|
||||
|
||||
public void AppendLiteral(string value)
|
||||
{
|
||||
_block._builder.Append(value);
|
||||
_wrote = true;
|
||||
}
|
||||
|
||||
public void AppendFormatted<T>(T value)
|
||||
{
|
||||
_block._builder.Append(value);
|
||||
_wrote = true;
|
||||
}
|
||||
|
||||
public void AppendFormatted<T>(T value, string format)
|
||||
{
|
||||
_block._builder.Append(value, format);
|
||||
_wrote = true;
|
||||
}
|
||||
|
||||
public void AppendFormatted(scoped ReadOnlySpan<char> value)
|
||||
{
|
||||
_block._builder.Append(value);
|
||||
_wrote = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue