From d35a527199ea5284ec8e23f0bd5e0d65f0b8a859 Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Mon, 22 Jun 2026 07:47:38 -0700 Subject: [PATCH] feat(opl): add ObjectPropertyList.Add(ReadOnlySpan) overloads --- .../ObjectPropertyListSpanAddTests.cs | 61 +++++++++++++++++++ Projects/Server/PropertyList/IPropertyList.cs | 4 ++ .../Server/PropertyList/ObjectPropertyList.cs | 34 +++++++++++ 3 files changed, 99 insertions(+) create mode 100644 Projects/Server.Tests/Tests/PropertyList/ObjectPropertyListSpanAddTests.cs diff --git a/Projects/Server.Tests/Tests/PropertyList/ObjectPropertyListSpanAddTests.cs b/Projects/Server.Tests/Tests/PropertyList/ObjectPropertyListSpanAddTests.cs new file mode 100644 index 000000000..7972739e1 --- /dev/null +++ b/Projects/Server.Tests/Tests/PropertyList/ObjectPropertyListSpanAddTests.cs @@ -0,0 +1,61 @@ +using System; +using System.Buffers.Binary; +using System.Text; +using Server; +using Xunit; + +namespace Server.Tests; + +public class ObjectPropertyListSpanAddTests +{ + // Decodes a terminated OPL buffer into (cliloc, argument) entries. + // The OPL packet is big-endian (SpanWriter default), so use BinaryPrimitives. + private static (int cliloc, string arg)[] Decode(ObjectPropertyList opl) + { + opl.Terminate(); + var buffer = opl.Buffer; + var entries = new System.Collections.Generic.List<(int, string)>(); + var pos = 15; // header is 15 bytes + while (true) + { + var cliloc = BinaryPrimitives.ReadInt32BigEndian(buffer.AsSpan(pos)); + pos += 4; + if (cliloc == 0) + { + break; + } + + var byteLen = BinaryPrimitives.ReadUInt16BigEndian(buffer.AsSpan(pos)); + pos += 2; + var arg = Encoding.Unicode.GetString(buffer, pos, byteLen); + pos += byteLen; + entries.Add((cliloc, arg)); + } + + return entries.ToArray(); + } + + [Fact] + public void SpanAdd_ProducesSameBytesAsStringAdd() + { + var fromString = new ObjectPropertyList(null); + fromString.Add("Hello World"); + + var fromSpan = new ObjectPropertyList(null); + fromSpan.Add("Hello World".AsSpan()); + + Assert.Equal(Decode(fromString), Decode(fromSpan)); + Assert.Equal(fromString.Hash, fromSpan.Hash); + } + + [Fact] + public void SpanAdd_WithNumber_EmitsClilocAndArgument() + { + var opl = new ObjectPropertyList(null); + opl.Add(1070722, "Custom".AsSpan()); + + var entries = Decode(opl); + Assert.Single(entries); + Assert.Equal((1070722, "Custom"), entries[0]); + } +} diff --git a/Projects/Server/PropertyList/IPropertyList.cs b/Projects/Server/PropertyList/IPropertyList.cs index 54b41e4c6..0ab0fc054 100644 --- a/Projects/Server/PropertyList/IPropertyList.cs +++ b/Projects/Server/PropertyList/IPropertyList.cs @@ -13,6 +13,7 @@ * along with this program. If not, see . * *************************************************************************/ +using System; using System.Runtime.CompilerServices; using Server.Text; @@ -31,6 +32,9 @@ public interface IPropertyList : ISelfInterpolatedStringHandler /** Convenience method for $"{text}". */ public void Add(string text); + public void Add(ReadOnlySpan argument); + public void Add(int number, ReadOnlySpan argument); + /** Convenience method for $"{value}". */ public void Add(int number, int value); diff --git a/Projects/Server/PropertyList/ObjectPropertyList.cs b/Projects/Server/PropertyList/ObjectPropertyList.cs index d1e15abd8..99d538333 100644 --- a/Projects/Server/PropertyList/ObjectPropertyList.cs +++ b/Projects/Server/PropertyList/ObjectPropertyList.cs @@ -153,6 +153,40 @@ public sealed class ObjectPropertyList : IPropertyList, IDisposable public void AddLocalized(int value) => InternalAdd(GetStringNumber(), $"{value:#}"); public void AddLocalized(int number, int value) => InternalAdd(number, $"{value:#}"); + public void Add(ReadOnlySpan argument) => InternalAdd(GetStringNumber(), argument); + public void Add(int number, ReadOnlySpan argument) => InternalAdd(number, argument); + + private void InternalAdd(int number, ReadOnlySpan chars) + { + if (number == 0) + { + return; + } + + if (Header == 0) + { + Header = number; + HeaderArgs = chars.ToString(); + } + + AddHash(number); + AddHash(string.GetHashCode(chars, StringComparison.Ordinal)); + + var strLength = chars.Length * 2; + var length = _bufferPos + 6 + strLength; + while (length > _buffer.Length) + { + Flush(); + } + + var writer = new SpanWriter(_buffer.AsSpan(_bufferPos)); + writer.Write(number); + writer.Write((ushort)strLength); + writer.Write(chars, TextEncoding.UnicodeLE); + + _bufferPos += writer.BytesWritten; + } + private int GetStringNumber() => _stringNumbers[_stringNumbersIndex++ % _stringNumbers.Length]; // String Interpolation