ModernUO/Projects/Server/PropertyList/OplTextBlock.cs
Kamron Batman c94084b48f 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.
2026-06-26 00:45:51 -07:00

113 lines
3.8 KiB
C#

/*************************************************************************
* 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 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
{
private readonly IPropertyList _list;
internal ValueStringBuilder _builder;
private bool _any;
internal OplTextBlock(IPropertyList 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).
// 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();
}
[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;
}
}
}