ModernUO/dev-docs/runuo-migration-docs/06-property-lists.md
Kamron Batman 4f9bc1d9f6
feat: Adds AI skills to migrate from RunUO (#2366)
## Summary

Adds comprehensive RunUO → ModernUO migration documentation and Claude AI skills to help shard owners and script authors convert RunUO 2.7 code to ModernUO.

- **10 migration skills** (`dev-docs/claude-skills/migrate-from-runuo/`) — system-by-system conversion guides (foundation, serialization, timers, gumps, packets, property lists, commands/events, persistence, items/mobiles, systems/engines)
- **12 reference docs** (`dev-docs/runuo-migration-docs/`) — deep-reference with before/after examples, API mapping tables, edge cases, and gotchas
- **Updated existing skills** — `modernuo-timers`, `modernuo-serialization`, and `modernuo-threading` now document that `Serialize()` runs on background threads and timers are not thread-safe
- **Updated `CLAUDE.md`** — added migration skill lookup table

### Key migration patterns covered
- Manual `Serialize()`/`Deserialize()` → source-generated `[SerializableField]`
- `Packet` class hierarchy → static `SpanWriter`/`SpanReader` methods
- `Timer` subclasses → `TimerExecutionToken` fire-and-forget
- `Gump` → `StaticGump<T>`/`DynamicGump` with builders
- `EventSink.WorldSave` → `GenericPersistence`
- `ObjectPropertyList` → `IPropertyList` with string hole rules
- Universal changes: naming (`m_` → `_`), `[Constructable]` → `[Constructible]`, logging, spatial queries
2026-03-13 00:33:45 -07:00

194 lines
6.1 KiB
Markdown

# Property Lists (Tooltips) Migration
## Overview
RunUO uses `ObjectPropertyList` as both the interface and implementation for item/mobile tooltips. ModernUO introduces an `IPropertyList` interface and has a critical rule: string literals in interpolated strings must be wrapped as holes (`{"text"}` not `text`).
## RunUO Pattern
```csharp
public override void GetProperties(ObjectPropertyList list)
{
base.GetProperties(list);
list.Add(1060741, m_Charges.ToString()); // "charges: ~1_val~"
list.Add(1060658, "Map\t" + m_MapDest); // "~1_val~: ~2_val~"
list.Add(1060637, "{0}\t{1}", m_Current, m_Max); // "~1_val~ / ~2_val~"
list.Add("Custom text line");
}
```
## ModernUO Equivalent
```csharp
public override void GetProperties(IPropertyList list)
{
base.GetProperties(list);
list.Add(1060741, $"{_charges}"); // "charges: ~1_val~"
list.Add(1060658, $"{"Map"}\t{_mapDest}"); // "~1_val~: ~2_val~" — "Map" is a hole!
list.Add(1060637, $"{_current}\t{_max}"); // "~1_val~ / ~2_val~"
list.Add($"{"Custom text line"}"); // Raw string — still a hole
}
```
## Migration Mapping Table
| RunUO | ModernUO | Notes |
|---|---|---|
| `GetProperties(ObjectPropertyList list)` | `GetProperties(IPropertyList list)` | Interface instead of class |
| `list.Add(number, string.Format(...))` | `list.Add(number, $"...")` | Interpolated string |
| `list.Add(number, value.ToString())` | `list.Add(number, $"{value}")` | Interpolated |
| `list.Add(number, "text\t" + value)` | `list.Add(number, $"{"text"}\t{value}")` | Text must be hole |
| `list.Add(number, string.Format("{0}\t{1}", a, b))` | `list.Add(number, $"{a}\t{b}")` | Tab-separated args |
| `list.Add("raw string")` | `list.Add($"{"raw string"}")` | String literal as hole |
| `list.Add(number)` | `list.Add(number)` | Same — cliloc only |
| `list.Add(number, "#" + cliloc)` | `list.Add(number, $"{cliloc:#}")` | Cliloc as argument |
## The String Literal Rule (CRITICAL)
The `IPropertyList` interpolated string handler distinguishes between **literals** (text between `{}` holes) and **holes** (values inside `{}`). Literals are treated as delimiters (like `\t`). Holes are treated as cliloc arguments.
**Rule**: The only bare literal text should be `\t` (argument separator). All other text must be inside `{}` holes.
```csharp
// BAD — "Map" is a literal, treated as a delimiter
list.Add(1060658, $"Map\t{_mapDest}");
// GOOD — "Map" is a hole, treated as argument ~1_val~
list.Add(1060658, $"{"Map"}\t{_mapDest}");
```
Why this matters: The property list data is consumed beyond just the game client (e.g., web renderers). The system must distinguish arguments from delimiters to correctly format tooltips for all consumers.
### Cliloc Number as Argument
When a cliloc argument is itself a cliloc number to resolve:
```csharp
// BAD — string "#1060000" is not properly handled by all consumers
list.Add(1050039, $"{_amount}\t{"#1060000"}");
// GOOD — :# format specifier marks it as a cliloc reference
list.Add(1050039, $"{_amount}\t{1060000:#}");
```
Or use the convenience methods:
```csharp
list.AddLocalized(clilocNumber); // Single cliloc value
list.AddLocalized(1050039, clilocNumber); // Cliloc with cliloc argument
```
## Step-by-Step Conversion
### Step 1: Change Method Signature
```csharp
// RunUO
public override void GetProperties(ObjectPropertyList list)
// ModernUO
public override void GetProperties(IPropertyList list)
```
### Step 2: Always Call Base First
```csharp
base.GetProperties(list); // Unchanged
```
### Step 3: Convert Each list.Add() Call
**Simple value:**
```csharp
// RunUO
list.Add(1060741, m_Charges.ToString());
// ModernUO
list.Add(1060741, $"{_charges}");
```
**Multiple tab-separated arguments:**
```csharp
// RunUO
list.Add(1060637, string.Format("{0}\t{1}", m_Current, m_Max));
// ModernUO
list.Add(1060637, $"{_current}\t{_max}");
```
**String literal arguments:**
```csharp
// RunUO
list.Add(1060658, "Coords\t" + m_Location.ToString());
// ModernUO
list.Add(1060658, $"{"Coords"}\t{_location}");
```
**Raw text:**
```csharp
// RunUO
list.Add("Soulbound");
// ModernUO
list.Add($"{"Soulbound"}");
```
### Step 4: Add [InvalidateProperties] to Serialized Fields
If the RunUO property setter called `InvalidateProperties()`, add the attribute:
```csharp
[SerializableField(0)]
[InvalidateProperties] // Auto-refreshes tooltip
[SerializedCommandProperty(AccessLevel.GameMaster)]
private int _charges;
```
## Before/After Example
**RunUO:**
```csharp
public override void GetProperties(ObjectPropertyList list)
{
base.GetProperties(list);
list.Add(1060741, m_Charges.ToString());
list.Add(1060658, "Map\t" + m_MapDest);
list.Add(1060659, "Coords\t" + m_PointDest);
list.Add(1060660, "Creatures\t" + (m_Creatures ? "Yes" : "No"));
list.Add(1060661, "Range\t" + m_Range);
if (m_Active)
list.Add(1060742); // "active"
}
```
**ModernUO:**
```csharp
public override void GetProperties(IPropertyList list)
{
base.GetProperties(list);
list.Add(1060741, $"{_charges}");
list.Add(1060658, $"{"Map"}\t{_mapDest}");
list.Add(1060659, $"{"Coords"}\t{_pointDest}");
list.Add(1060660, $"{"Creatures"}\t{(Creatures ? "Yes" : "No")}");
list.Add(1060661, $"{"Range"}\t{_range}");
if (_active)
list.Add(1060742);
}
```
## Edge Cases & Gotchas
### 1. Integer Overload vs String Interpolation
`list.Add(number, int)` exists and is different from `list.Add(number, $"{int}")`. The integer overload passes the raw int; the interpolation formats it as a string. Use whichever matches the cliloc expectation.
### 2. Era-Conditional Properties
Check era when properties differ between expansions:
```csharp
if (Core.ML)
list.Add(1072241, $"{TotalItems}\t{MaxItems}\t{TotalWeight}\t{MaxWeight}");
else
list.Add(1050044, $"{TotalItems}\t{TotalWeight}");
```
### 3. Don't Call InvalidateProperties() in Loops
It triggers hash computation and potential network sends. Batch changes first.
## See Also
- `dev-docs/property-lists.md` — Complete ModernUO property list reference
- `02-serialization.md` — [InvalidateProperties] on serialized fields
- `01-foundation-changes.md` — Foundation changes