fix(opl): cap per-property length + chunk OplTextBlock to avoid legacy 2D-client crash

The legacy 2D (EA) client copies each OPL property's argument into a fixed ~512-char
(1024-byte) buffer; a single property that exceeds it overruns the heap and crashes the
client. OplTextBlock previously emitted one entry regardless of length.

- ObjectPropertyList.MaxArgumentLength (504 chars, under the ~510 ceiling).
- ClampArgument backstop in both InternalAdd paths (span + interpolation): truncates an
  oversized argument and logs the offending entity/cliloc.
- AddChunked(ReadOnlySpan<char>): splits newline-joined text across multiple cycling
  passthrough properties, breaking only at '\n', so none exceeds the cap.
- OplTextBlock.Dispose emits via AddChunked; AddChunked added to IPropertyList.
This commit is contained in:
Kamron Batman 2026-06-22 18:53:00 -07:00
parent 42ae2a1146
commit c94084b48f
5 changed files with 140 additions and 3 deletions

View file

@ -19,7 +19,9 @@ using Server.Text;
namespace Server;
// Accumulates '\n'-joined free-text lines and emits ONE cycling passthrough entry on dispose.
// Accumulates '\n'-joined free-text lines and emits them on dispose via AddChunked, which splits
// into as many cycling passthrough entries as needed so no single OPL property exceeds the legacy
// client's per-property buffer (ObjectPropertyList.MaxArgumentLength).
// Use with `using var block = list.TextBlock();`. ref struct: single-threaded OPL build only.
public ref struct OplTextBlock
{
@ -63,7 +65,9 @@ public ref struct OplTextBlock
if (_any)
{
// Strip the trailing '\n' (Length >= 2 whenever _any: content + separator).
_list.Add(_builder.AsSpan(0, _builder.Length - 1));
// AddChunked splits across multiple properties so a long block never overflows the
// legacy client's per-property tooltip buffer.
_list.AddChunked(_builder.AsSpan(0, _builder.Length - 1));
}
_builder.Dispose();