ModernUO/dev-docs/claude-skills/modernuo-gump-system.md
Kamron Batman 9ea1b54758
docs(messages): document interpolation anti-patterns and :L format spec (#2441)
## Summary

Captures the durable learnings from the message-interpolation work (PRs #2434, #2436, #2437, #2438, #2440) as reference documentation. **Doc-only PR — no code changes.**

The original Phase 2 audit (PR #2435) was development scaffolding and was closed unmerged once Phase 3 consumed it. This PR replaces it with proper reference docs that future authors can consult.

## What's added

### `dev-docs/string-handling.md`
- Promote `RawInterpolatedStringHandler` from a one-line note to a proper section listing all APIs that accept it (messages, OPL, gumps, packets).
- Document the `:L` lowercase format specifier.
- New comprehensive **"Interpolation Anti-Patterns"** section covering 8 patterns with before/after examples — applies to any handler-aware API:
  1. Ternary with interpolated branches
  2. Switch expression with interpolated arms
  3. Pre-built local typed as `string`
  4. `.ToString()` (or any string-returning method) inside a hole
  5. String concatenation inside a hole
  6. `string.Format` feeding a handler-aware API
  7. LINQ-built strings inside a hole
  8. Pre-built concat var

### `dev-docs/networking-packets.md`
- Add **"Player-Facing Message APIs"** section listing `Mobile` / `Item` / `NetState` message methods with their handler overloads.
- Note the `IBroadcastFilter` pattern for new spatial-broadcast helpers.

### `dev-docs/property-lists.md`, `dev-docs/gump-system.md`
- Cross-reference the new anti-patterns section.
- Add explicit `.ToString()` inside holes warning to property-lists (it had no such guidance before).

### `dev-docs/claude-skills/`
- Mirror the same content (condensed) in `modernuo-string-handling.md`, `modernuo-networking.md`, `modernuo-property-lists.md`, `modernuo-gump-system.md`.
- Add audit rule #17 to `modernuo-code-audit.md` covering all 8 anti-patterns with severity WARNING, plus the `:L` format spec.

### `CLAUDE.md`
- Add audit rule #18 summarizing the interpolation anti-patterns + `:L`, pointing to `dev-docs/string-handling.md` for details.

## Why this matters

Before this PR there was no documentation explaining when an interpolated string call site silently allocates a string despite the receiving API providing a handler overload. The Phase 3 cleanup (PRs #2436/#2437/#2438) discovered ~28 such sites in the codebase; without these docs the same patterns would re-emerge. The new audit rule + CLAUDE.md entry will catch them at write time.
2026-05-03 18:23:50 -07:00

359 lines
12 KiB
Markdown

---
name: modernuo-gump-system
description: >
Trigger when creating or modifying gumps (UI dialogs). When working with BaseGump, StaticGump, DynamicGump, or GumpSystem.
---
# ModernUO Gump System
## When This Activates
- Creating new gumps (UI dialogs/windows)
- Modifying existing gump layouts
- Handling gump button responses
- Using `mobile.SendGump()`, `HasGump<T>()`, `CloseGump<T>()`
## Key Rules
1. **Use `StaticGump<TSelf>`** when layout is fixed (cached, better performance)
2. **Use `DynamicGump`** when layout depends on instance data (rebuilt each time)
3. **Use `mobile.SendGump(gump)`** to send -- requires `using Server.Gumps;`
4. **Override `Singleton => true`** if only one instance should be open per player
5. **Never forget `using Server.Gumps;`** -- extension methods won't resolve without it
## Hierarchy
```
BaseGump (abstract)
├── StaticGump<TSelf> -- Cached layout, use for menus/dialogs
└── DynamicGump -- Rebuilt layout, use for dynamic content
```
## StaticGump Pattern (Preferred for Fixed Layouts)
```csharp
using Server.Gumps;
namespace Server.Gumps;
public class MyGump : StaticGump<MyGump>
{
private readonly Mobile _player;
private readonly string _message;
public override bool Singleton => true; // One per player
public MyGump(Mobile player, string message) : base(50, 50)
{
_player = player;
_message = message;
}
protected override void BuildLayout(ref StaticGumpBuilder builder)
{
builder.AddPage();
builder.AddBackground(0, 0, 400, 300, 5054);
// Static text (cached)
builder.AddHtmlLocalized(10, 10, 380, 20, 1060635, 0x7800); // "Warning"
// Dynamic text placeholder (filled per-instance)
builder.AddHtmlPlaceholder(10, 40, 380, 220, "content", false, true);
// Buttons
builder.AddButton(150, 265, 4005, 4007, 1); // OK button (buttonID=1)
builder.AddHtmlLocalized(185, 267, 100, 20, 1011036); // "OK"
builder.AddButton(250, 265, 4017, 4019, 0); // Cancel (buttonID=0 = close)
builder.AddHtmlLocalized(285, 267, 100, 20, 1011012); // "Cancel"
}
protected override void BuildStrings(ref GumpStringsBuilder builder)
{
// Fill dynamic placeholders
builder.SetHtmlText("content", _message, "#FFC000");
}
public override void OnResponse(NetState sender, in RelayInfo info)
{
if (info.ButtonID == 1)
{
_player.SendMessage("You clicked OK!");
}
}
}
```
## DynamicGump Pattern (For Variable Layouts)
```csharp
using Server.Gumps;
namespace Server.Gumps;
public class InventoryGump : DynamicGump
{
private readonly Mobile _player;
private readonly List<Item> _items;
public override bool Singleton => true;
public InventoryGump(Mobile player, List<Item> items) : base(50, 50)
{
_player = player;
_items = items;
}
protected override void BuildLayout(ref DynamicGumpBuilder builder)
{
builder.AddPage();
builder.AddBackground(0, 0, 400, 40 + _items.Count * 30, 5054);
builder.AddHtml(10, 10, 380, 20, "Inventory");
for (var i = 0; i < _items.Count; i++)
{
var y = 40 + i * 30;
builder.AddLabel(10, y, 0, _items[i].Name ?? "Unknown");
builder.AddButton(350, y, 4005, 4007, i + 1); // Button per item
}
}
public override void OnResponse(NetState sender, in RelayInfo info)
{
if (info.ButtonID > 0 && info.ButtonID <= _items.Count)
{
var item = _items[info.ButtonID - 1];
_player.SendMessage($"Selected: {item.Name}");
}
}
}
```
## Sending and Managing Gumps
```csharp
using Server.Gumps;
// Send gump
mobile.SendGump(new MyGump(mobile, "Hello!"));
// Check if gump is open
if (mobile.HasGump<MyGump>())
{
// Already open
}
// Find existing gump
var gump = mobile.FindGump<MyGump>();
// Close gump
mobile.CloseGump<MyGump>();
```
## Builder Methods Reference
### Layout Elements
```csharp
builder.AddPage(int page = 0);
builder.AddBackground(int x, int y, int width, int height, int gumpID);
builder.AddAlphaRegion(int x, int y, int width, int height);
builder.AddImageTiled(int x, int y, int width, int height, int gumpID);
builder.AddImage(int x, int y, int gumpID, int hue = 0);
builder.AddItem(int x, int y, int itemID, int hue = 0);
```
### Text
```csharp
builder.AddLabel(int x, int y, int hue, ReadOnlySpan<char> text);
builder.AddHtml(int x, int y, int w, int h, ReadOnlySpan<char> text, ...);
builder.AddHtmlLocalized(int x, int y, int w, int h, int number, ...);
builder.AddLabelPlaceholder(int x, int y, int hue, ReadOnlySpan<char> slotKey);
builder.AddHtmlPlaceholder(int x, int y, int w, int h, ReadOnlySpan<char> slotKey, ...);
```
Text-accepting builders take `ROS<char>` and have `ref RawInterpolatedStringHandler` overloads — `$"..."` literals at the call site are zero-allocation. Same for `Html.Center`/`Html.Color`/`Html.Right` helpers used for HTML markup wrapping.
```csharp
builder.AddHtml(20, 20, 200, 100, $"<center>{Title}: {Score:N0}</center>");
builder.AddLabel(20, 40, hue, $"You have {gold} gold");
```
Avoid the interpolation anti-patterns (ternaries with `$"..."` branches, `.ToString()` inside holes, pre-built `var msg = $"..."` locals, `string.Format`, etc.) — they silently fall back to string-allocating overloads. See `dev-docs/claude-skills/modernuo-string-handling.md` § "Interpolation Anti-Patterns" for the full list.
### Interactive
```csharp
builder.AddButton(int x, int y, int normalID, int pressedID, int buttonID, ...);
builder.AddCheckbox(int x, int y, int inactiveID, int activeID, bool selected, int switchID);
builder.AddRadio(int x, int y, int inactiveID, int activeID, bool selected, int switchID);
builder.AddTextEntry(int x, int y, int w, int h, int hue, int entryID, ...);
```
### Modifiers
```csharp
builder.SetNoClose(); // Cannot close with right-click
builder.SetNoMove(); // Cannot move the gump
builder.SetNoResize(); // Cannot resize
builder.SetNoDispose(); // Cannot dispose
builder.AddTooltip(int number); // Hover tooltip
```
## Response Handling
```csharp
public override void OnResponse(NetState sender, in RelayInfo info)
{
var buttonID = info.ButtonID; // 0 = close, 1+ = button clicks
var isChecked = info.IsSwitched(0); // Checkbox/radio state by switchID
var text = info.GetTextEntry(0); // Text entry value by entryID
}
```
## Static vs Dynamic: When to Use Which
| Use StaticGump | Use DynamicGump |
|---|---|
| Confirmation dialogs | Lists of variable length |
| Static menus | Player-specific content |
| Warning prompts | Crafting interfaces |
| Settings panels | Search results |
| Help pages | Dynamic data display |
## Important Properties
### Singleton
```csharp
public override bool Singleton => true;
```
Automatically closes any existing instance of this gump type for the player before sending a new one. **Always set this for gumps that shouldn't stack.** Without it, repeated sends create duplicates the player must close individually.
### Cached (StaticGump only)
```csharp
protected virtual bool Cached => true; // default
```
Controls whether `StaticGump<T>` caches its compiled layout bytes. Override to `false` **during development only** to force layout rebuild each send — useful for iterating on layout without restarting the server:
```csharp
protected override bool Cached => false; // TEMPORARY — remove before commit
```
## Empty Gump Rule (CRITICAL AUDIT RULE)
**NEVER send a gump with no visual components.** An empty gump cannot be closed by the client — no close button, no right-click dismiss. This causes a **gump leak** on both client and server.
Empty gumps happen when short-circuiting inside a constructor or `BuildLayout`:
```csharp
// BAD — early return in constructor leaves gump empty but it still gets sent
public MyGump(Mobile from) : base(50, 50)
{
if (!from.Alive) return; // Empty gump — LEAK!
// ...layout...
}
```
### Fix: Static DisplayTo Pattern
Validate prerequisites **before** constructing the gump. Make the constructor `private`:
```csharp
public class MyGump : DynamicGump
{
public override bool Singleton => true;
private MyGump(Mobile from, SomeData data) : base(50, 50) { /* store fields */ }
protected override void BuildLayout(ref DynamicGumpBuilder builder)
{
builder.AddPage();
builder.AddBackground(0, 0, 400, 300, 5054);
// Always has visual content — DisplayTo guarantees valid state
}
public static void DisplayTo(Mobile from)
{
if (!from.Alive || from.NetState == null) return; // No gump created
var data = GetData(from);
if (data == null) return; // No gump created
from.SendGump(new MyGump(from, data));
}
}
```
Reference: `Projects/UOContent/Gumps/Go/GoGump.cs`
## Converting Legacy Gump to DynamicGump / StaticGump
### Choose target type
- Layout is fixed structure → `StaticGump<T>` (cached, best performance)
- Layout varies per instance (loops, conditionals) → `DynamicGump`
- When in doubt → `DynamicGump` (simpler, still much better than legacy)
### Conversion checklist
1. Change base class: `Gump``DynamicGump` or `StaticGump<MyGump>`
2. Move layout code from constructor into `BuildLayout(ref DynamicGumpBuilder builder)` or `BuildLayout(ref StaticGumpBuilder builder)`
3. Store any state the constructor used as fields (constructor now just stores state, doesn't build layout)
4. Replace property flags with builder methods:
- `Closable = false``builder.SetNoClose()`
- `Draggable = false``builder.SetNoMove()`
- `Resizable = false``builder.SetNoResize()`
- `Disposable = false``builder.SetNoDispose()`
5. `AddPage(0)``builder.AddPage()` (0 is default)
6. For `StaticGump<T>`: extract dynamic text into placeholders (`AddLabelPlaceholder` / `AddHtmlPlaceholder`) and fill in `BuildStrings(ref GumpStringsBuilder builder)`
7. Update `OnResponse` signature: `RelayInfo info``in RelayInfo info`
8. Add `public override bool Singleton => true;` if appropriate
9. Make constructor `private`, add `public static void DisplayTo(Mobile from)` method
10. Move all validation/short-circuit logic into `DisplayTo` (never leave `BuildLayout` empty)
### Example: legacy → modern
```csharp
// BEFORE (legacy Gump)
public class OldGump : Gump
{
public OldGump(Mobile from) : base(50, 50)
{
Closable = false;
AddPage(0);
AddBackground(0, 0, 400, 300, 5054);
AddLabel(20, 20, 0x480, from.Name);
AddButton(20, 260, 4005, 4007, 1);
}
public override void OnResponse(NetState sender, RelayInfo info) { }
}
// AFTER (DynamicGump)
public class NewGump : DynamicGump
{
private readonly Mobile _from;
public override bool Singleton => true;
private NewGump(Mobile from) : base(50, 50) => _from = from;
protected override void BuildLayout(ref DynamicGumpBuilder builder)
{
builder.SetNoClose();
builder.AddPage();
builder.AddBackground(0, 0, 400, 300, 5054);
builder.AddLabel(20, 20, 0x480, _from.Name);
builder.AddButton(20, 260, 4005, 4007, 1);
}
public override void OnResponse(NetState sender, in RelayInfo info) { }
public static void DisplayTo(Mobile from) => from.SendGump(new NewGump(from));
}
```
## Anti-Patterns
- **Missing `using Server.Gumps;`**: `SendGump()`, `HasGump<T>()` won't resolve
- **Creating DynamicGump for static layouts**: Wastes CPU rebuilding every time
- **Not setting `Singleton`**: Multiple copies of same gump stack up
- **ButtonID 0 for actions**: 0 means "close" -- use 1+ for action buttons
- **Empty gumps**: Short-circuiting in constructor/BuildLayout causes gump leaks — use `DisplayTo` pattern
- **Leaving `Cached => false` in production**: Defeats the purpose of StaticGump — only disable during development
## Real Examples
- StaticGump warning: `Projects/UOContent/Gumps/StaticWarningGump.cs`
- DynamicGump craft: `Projects/UOContent/Engines/Craft/Core/CraftGump.cs`
- GumpSystem extensions: `Projects/UOContent/Gumps/Base/GumpSystem.cs`
- StaticGumpBuilder: `Projects/UOContent/Gumps/Base/StaticGumpBuilder.cs`
- DynamicGumpBuilder: `Projects/UOContent/Gumps/Base/DynamicGumpBuilder.cs`
- BaseGump: `Projects/UOContent/Gumps/Base/BaseGump.cs`
## See Also
- `dev-docs/gump-system.md` - Complete gump documentation
- `dev-docs/claude-skills/modernuo-commands-targeting.md` - Commands that open gumps