feat(opl): OplTextBlock multi-line tooltip builder + AddChunked (#2507)
## 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.
This commit is contained in:
parent
f7c44f7c10
commit
d7668df5ee
7 changed files with 588 additions and 15 deletions
|
|
@ -26,11 +26,14 @@ description: >
|
|||
```csharp
|
||||
public interface IPropertyList
|
||||
{
|
||||
void Add(int number); // Cliloc number only
|
||||
void Add(int number, string argument); // Cliloc with ~1_val~ arg
|
||||
void Add(string text); // Raw string (uses internal cliloc)
|
||||
void Add(int number, int value); // Cliloc with int arg
|
||||
void AddLocalized(int value); // Cliloc number as value
|
||||
void Add(int number); // Cliloc number only
|
||||
void Add(int number, string argument); // Cliloc with ~1_val~ arg
|
||||
void Add(ReadOnlySpan<char> argument); // Raw text, no string alloc (uses passthrough cliloc)
|
||||
void Add(int number, ReadOnlySpan<char> argument);
|
||||
void AddChunked(ReadOnlySpan<char> text); // Newline-joined text, split across properties
|
||||
OplTextBlock TextBlock(); // Builder that flushes via AddChunked on dispose
|
||||
void Add(int number, int value); // Cliloc with int arg
|
||||
void AddLocalized(int value); // Cliloc number as value
|
||||
void AddLocalized(int number, int value); // Cliloc wrapper for cliloc
|
||||
|
||||
// String interpolation overloads
|
||||
|
|
@ -39,6 +42,9 @@ public interface IPropertyList
|
|||
}
|
||||
```
|
||||
|
||||
> No `Add(string)` overload — pass a span (`text.AsSpan()`) or, preferably, an interpolated `$"..."`
|
||||
> literal so the handler formats straight into the pooled buffer.
|
||||
|
||||
## Patterns
|
||||
|
||||
### Basic GetProperties Override
|
||||
|
|
@ -177,6 +183,29 @@ public override void GetProperties(IPropertyList list)
|
|||
}
|
||||
```
|
||||
|
||||
### Multi-Line Free Text (`AddChunked` / `OplTextBlock`)
|
||||
|
||||
For variable-length, free-form (non-cliloc) tooltip text — e.g. a consolidated attribute dump or
|
||||
staged ID text — **never emit it as one property**. The legacy 2D client copies each OPL property into
|
||||
a fixed ~512-char buffer; a longer property smashes the client heap and crashes it.
|
||||
`ObjectPropertyList.MaxArgumentLength` (504) is the safe per-property cap.
|
||||
|
||||
Two safe APIs split `\n`-joined text across multiple passthrough-cliloc properties (each ≤ cap):
|
||||
|
||||
```csharp
|
||||
// Already have a '\n'-joined string? Use the IPropertyList primitive directly:
|
||||
list.AddChunked(_description);
|
||||
|
||||
// Building lines conditionally? Use the OplTextBlock builder (flushes via AddChunked on dispose):
|
||||
using var block = list.TextBlock(); // IPropertyList.TextBlock()
|
||||
block.Add($"Lower Parry Cap {cap}%"); // zero-alloc interpolated overload
|
||||
block.Add("Cannot be repaired".AsSpan()); // plain span, no string alloc
|
||||
```
|
||||
|
||||
- Lines join with `\n`; empty lines are skipped; no lines → nothing emitted.
|
||||
- `block.Add($"...")` is a real `[InterpolatedStringHandler]` — same hole anti-patterns apply (no `.ToString()`, ternaries, concat).
|
||||
- `OplTextBlock` is a `ref struct` for the single-threaded build pass — always `using`, never store/await across it.
|
||||
|
||||
## Common Cliloc Numbers
|
||||
|
||||
| Number | Text | Usage |
|
||||
|
|
@ -205,6 +234,7 @@ public override void GetProperties(IPropertyList list)
|
|||
- **Not using cliloc**: Raw strings don't get localized
|
||||
- **Excessive rebuilds**: Don't call `InvalidateProperties()` in tight loops
|
||||
- **Assuming tooltip support**: Check `ObjectPropertyList.Enabled` if needed
|
||||
- **One giant `Add()` for multi-line text**: A property over ~512 chars crashes the legacy 2D client. Use `AddChunked`/`OplTextBlock` for variable-length free text
|
||||
|
||||
## Real Examples
|
||||
- Item properties: `Projects/Server/Items/Item.cs` (AddNameProperties, GetProperties)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue