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

12 KiB

name description
modernuo-gump-system 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)

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)

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

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

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

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.

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

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

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

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

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)

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:

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:

// 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:

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: GumpDynamicGump 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 = falsebuilder.SetNoClose()
    • Draggable = falsebuilder.SetNoMove()
    • Resizable = falsebuilder.SetNoResize()
    • Disposable = falsebuilder.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 infoin 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

// 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