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