## At a glance
```csharp
public override void GetProperties(IPropertyList list)
{
base.GetProperties(list);
// Accumulate any number of free-text lines. On dispose the block flushes via
// AddChunked, splitting across as many OPL properties as needed so none can
// overflow the legacy 2D client's per-property buffer (which would crash it).
using var block = list.TextBlock();
if (luck > 0)
{
block.Add($"Luck Bonus: +{luck}%"); // zero-alloc interpolation
}
block.Add("Cannot be repaired".AsSpan()); // plain text, no string allocation
// Already holding a '\n'-joined string? Skip the builder and chunk directly:
// list.AddChunked(description);
}
```
## What
Adds a safe path for emitting **variable-length, free-form (non-cliloc) tooltip text**:
- **`ObjectPropertyList.Add(ReadOnlySpan<char>)`** overloads — append raw text with no string allocation. Makes the old single-arg `Add(string)` redundant (a `string` binds to the span overload implicitly), so it's dropped.
- **`AddChunked(ReadOnlySpan<char>)`** on `IPropertyList` — splits newline-joined text at `\n` boundaries across as many passthrough-cliloc properties as needed, so no single property exceeds the cap.
- **`OplTextBlock`** (`ref struct`) + **`IPropertyList.TextBlock()`** — an ergonomic builder that accumulates `\n`-joined lines (with a zero-alloc interpolated `Add($"...")` overload) and flushes via `AddChunked` on dispose. Usage: `using var block = list.TextBlock();`.
- **`MaxArgumentLength` (504)** — per-property cap with a hard backstop that clamps + logs anything that slips through.
## Why
The legacy 2D client copies each OPL property's text into a fixed ~512-char (1024-byte) buffer. A single property longer than that smashes an adjacent world object's vtable on the client heap and crashes the client. `AddChunked`/`OplTextBlock` keep multi-line content safely under the cap instead of risking one oversized `Add`.
## Docs
- `dev-docs/property-lists.md` — new "Multi-Line Free Text" deep-dive section; corrected the stale `IPropertyList` listing.
- `dev-docs/claude-skills/modernuo-property-lists.md` — condensed pattern + anti-pattern.
## Tests
9 tests pass (`OplTextBlockTests`, `ObjectPropertyListSpanAddTests`): line joining, empty-line skipping, no-line no-op, zero-alloc interpolation, and long-content chunking staying under `MaxArgumentLength`. Full `UOContent` build is green, confirming dropping `Add(string)` breaks no call sites.
56 lines
2.4 KiB
C#
56 lines
2.4 KiB
C#
/*************************************************************************
|
|
* ModernUO *
|
|
* Copyright 2019-2026 - ModernUO Development Team *
|
|
* Email: hi@modernuo.com *
|
|
* File: IPropertyList.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;
|
|
|
|
public interface IPropertyList : ISelfInterpolatedStringHandler
|
|
{
|
|
public void Reset();
|
|
public void Terminate();
|
|
|
|
public void Add(int number);
|
|
|
|
/** Convenience method for $"{argument}". */
|
|
public void Add(int number, string argument);
|
|
|
|
/** Convenience method for span-based text without allocating a string. */
|
|
public void Add(ReadOnlySpan<char> argument);
|
|
|
|
/** Convenience method for span-based text without allocating a string. */
|
|
public void Add(int number, ReadOnlySpan<char> argument);
|
|
|
|
/** Emits newline-joined text across multiple properties so none exceeds the legacy client's per-property buffer. */
|
|
public void AddChunked(ReadOnlySpan<char> text);
|
|
|
|
/** Builder for variable-length multi-line free text; flushes via AddChunked on dispose. */
|
|
public OplTextBlock TextBlock();
|
|
|
|
/** Convenience method for $"{value}". */
|
|
public void Add(int number, int value);
|
|
|
|
/** Convenience method for $"{value:#}". */
|
|
public void AddLocalized(int value);
|
|
|
|
/** Convenience method for $"{value:#}". */
|
|
public void AddLocalized(int number, int value);
|
|
|
|
// String Interpolation
|
|
public void Add([InterpolatedStringHandlerArgument("")] ref InterpolatedStringHandler handler);
|
|
public void Add(int number, [InterpolatedStringHandlerArgument("")] ref InterpolatedStringHandler handler);
|
|
}
|