## 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.
519 lines
17 KiB
Markdown
519 lines
17 KiB
Markdown
# ModernUO Gump System
|
|
|
|
This document covers ModernUO's gump (UI dialog) system, including BaseGump, StaticGump, DynamicGump, builders, response handling, and best practices.
|
|
|
|
## Overview
|
|
|
|
Gumps are custom UI dialogs displayed to players. ModernUO provides a modern gump system with two main types:
|
|
- **StaticGump**: Layout is cached and reused across instances (better performance)
|
|
- **DynamicGump**: Layout is rebuilt each time (for variable content)
|
|
|
|
## Class Hierarchy
|
|
|
|
```
|
|
BaseGump (abstract)
|
|
├── StaticGump<TSelf> -- Cached layout, for fixed-structure gumps
|
|
└── DynamicGump -- Rebuilt layout, for dynamic-structure gumps
|
|
```
|
|
|
|
All gump classes are in `Projects/UOContent/Gumps/Base/`.
|
|
|
|
## Sending Gumps
|
|
|
|
```csharp
|
|
using Server.Gumps; // REQUIRED for extension methods
|
|
|
|
// Send a gump
|
|
mobile.SendGump(new MyGump(mobile));
|
|
|
|
// Check if gump is open
|
|
if (mobile.HasGump<MyGump>()) { }
|
|
|
|
// Find an open gump
|
|
var gump = mobile.FindGump<MyGump>();
|
|
|
|
// Close a gump
|
|
mobile.CloseGump<MyGump>();
|
|
```
|
|
|
|
**Important**: `using Server.Gumps;` is required. Without it, `SendGump()`, `HasGump<T>()`, `FindGump<T>()`, and `CloseGump<T>()` extension methods won't resolve.
|
|
|
|
These methods are also available on `NetState`:
|
|
```csharp
|
|
mobile.NetState.SendGump(gump);
|
|
mobile.NetState.HasGump<MyGump>();
|
|
```
|
|
|
|
## StaticGump
|
|
|
|
Use for gumps where the layout structure is the same for all instances. The layout is compiled and cached on first use, then reused.
|
|
|
|
### Template
|
|
```csharp
|
|
using Server.Gumps;
|
|
|
|
namespace Server.Gumps;
|
|
|
|
public class MyStaticGump : StaticGump<MyStaticGump>
|
|
{
|
|
private readonly Mobile _player;
|
|
private readonly string _data;
|
|
|
|
public override bool Singleton => true; // Only one per player
|
|
|
|
public MyStaticGump(Mobile player, string data) : base(50, 50)
|
|
{
|
|
_player = player;
|
|
_data = data;
|
|
}
|
|
|
|
protected override void BuildLayout(ref StaticGumpBuilder builder)
|
|
{
|
|
builder.AddPage();
|
|
builder.AddBackground(0, 0, 400, 300, 5054);
|
|
builder.AddAlphaRegion(10, 10, 380, 280);
|
|
|
|
// Static text (baked into cached layout)
|
|
builder.AddHtmlLocalized(15, 15, 370, 20, 1060635, 0x7800); // "Warning"
|
|
|
|
// Dynamic text (placeholder filled per-instance via BuildStrings)
|
|
builder.AddHtmlPlaceholder(15, 45, 370, 200, "content", false, true);
|
|
|
|
// Buttons
|
|
builder.AddButton(100, 265, 4005, 4007, 1); // OK (buttonID=1)
|
|
builder.AddHtmlLocalized(135, 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)
|
|
{
|
|
builder.SetHtmlText("content", _data, "#FFC000", 4);
|
|
}
|
|
|
|
public override void OnResponse(NetState sender, in RelayInfo info)
|
|
{
|
|
if (info.ButtonID == 1)
|
|
{
|
|
_player.SendMessage("Confirmed!");
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### How Caching Works
|
|
1. First instance calls `BuildLayout()` -- the layout bytes are compiled and cached
|
|
2. Subsequent instances reuse the cached layout bytes
|
|
3. `BuildStrings()` is called per-instance to fill dynamic text placeholders
|
|
4. Use `AddLabelPlaceholder`/`AddHtmlPlaceholder` for text that changes per instance
|
|
5. Use `AddLabel`/`AddHtml`/`AddHtmlLocalized` for text baked into the cache
|
|
|
|
### Placeholders
|
|
```csharp
|
|
// In BuildLayout:
|
|
builder.AddLabelPlaceholder(x, y, hue, "slotKey");
|
|
builder.AddHtmlPlaceholder(x, y, w, h, "slotKey", background, scrollbar);
|
|
builder.AddTextEntryPlaceholder(x, y, w, h, hue, entryId, "slotKey");
|
|
|
|
// In BuildStrings:
|
|
builder.SetStringSlot("slotKey", "text value");
|
|
builder.SetHtmlText("slotKey", "html content", "#color", fontSize);
|
|
```
|
|
|
|
## DynamicGump
|
|
|
|
Use for gumps where the layout structure varies per instance (e.g., lists of items, search results).
|
|
|
|
### Template
|
|
```csharp
|
|
using Server.Gumps;
|
|
|
|
namespace Server.Gumps;
|
|
|
|
public class MyDynamicGump : DynamicGump
|
|
{
|
|
private readonly Mobile _player;
|
|
private readonly List<Item> _items;
|
|
|
|
public override bool Singleton => true;
|
|
|
|
public MyDynamicGump(Mobile player, List<Item> items) : base(50, 50)
|
|
{
|
|
_player = player;
|
|
_items = items;
|
|
}
|
|
|
|
protected override void BuildLayout(ref DynamicGumpBuilder builder)
|
|
{
|
|
var height = 60 + _items.Count * 30;
|
|
builder.AddPage();
|
|
builder.AddBackground(0, 0, 400, height, 5054);
|
|
builder.AddAlphaRegion(10, 10, 380, height - 20);
|
|
builder.AddHtml(15, 15, 370, 20, "Select an item:");
|
|
|
|
for (var i = 0; i < _items.Count; i++)
|
|
{
|
|
var y = 45 + i * 30;
|
|
var item = _items[i];
|
|
builder.AddLabel(20, y, 0x480, item.Name ?? "Unknown");
|
|
builder.AddButton(350, y, 4005, 4007, i + 1);
|
|
}
|
|
}
|
|
|
|
public override void OnResponse(NetState sender, in RelayInfo info)
|
|
{
|
|
if (info.ButtonID > 0 && info.ButtonID <= _items.Count)
|
|
{
|
|
var selected = _items[info.ButtonID - 1];
|
|
_player.SendMessage($"You selected: {selected.Name}");
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Builder Methods Reference
|
|
|
|
### Layout Structure
|
|
```csharp
|
|
builder.AddPage(int page = 0); // Add page (0 = all pages)
|
|
builder.AddBackground(x, y, w, h, gumpID);
|
|
builder.AddAlphaRegion(x, y, w, h); // Transparent background
|
|
builder.AddImageTiled(x, y, w, h, gumpID); // Tiled background image
|
|
builder.AddGroup(int groupId); // Radio button group
|
|
```
|
|
|
|
### Images
|
|
```csharp
|
|
builder.AddImage(x, y, gumpID, hue); // Gump art image
|
|
builder.AddItem(x, y, itemID, hue); // Item graphic
|
|
builder.AddImageTiledButton(x, y, normalID, pressedID, buttonID, type, param, itemID, hue, w, h);
|
|
```
|
|
|
|
### Text
|
|
```csharp
|
|
builder.AddLabel(x, y, hue, text); // Single-line text
|
|
builder.AddLabelCropped(x, y, w, h, hue, text); // Cropped text
|
|
builder.AddHtml(x, y, w, h, text, bg, scrollbar); // HTML text
|
|
builder.AddHtml(x, y, w, h, text, color, size, fontStyle, align, bg, scrollbar);
|
|
builder.AddHtmlLocalized(x, y, w, h, clilocNumber); // Localized text
|
|
builder.AddHtmlLocalized(x, y, w, h, clilocNumber, color);
|
|
```
|
|
|
|
#### Interpolation in text
|
|
|
|
Most text-accepting builders take a `ReadOnlySpan<char>` and have a `ref RawInterpolatedStringHandler` overload, so `$"..."` literals at the call site are zero-allocation. The same applies to `Html.Center`, `Html.Color`, `Html.Right` helpers used when wrapping text in HTML markup:
|
|
|
|
```csharp
|
|
// Zero allocation — interpolation handler renders directly into a pooled buffer
|
|
builder.AddHtml(20, 20, 200, 100, $"<center>{Title}: {Score:N0}</center>");
|
|
builder.AddLabel(20, 40, hue, $"You have {gold} gold");
|
|
```
|
|
|
|
Several call-site shapes silently defeat the handler overload selection (ternaries with interpolated branches, `.ToString()` inside holes, pre-built `var msg = $"..."` locals, etc.). See [`dev-docs/string-handling.md`](string-handling.md#interpolation-anti-patterns) for the full list and fixes — they apply equally inside `BuildLayout`.
|
|
|
|
### Interactive Elements
|
|
```csharp
|
|
builder.AddButton(x, y, normalID, pressedID, buttonID);
|
|
builder.AddButton(x, y, normalID, pressedID, buttonID, GumpButtonType.Page, pageNum);
|
|
builder.AddCheckbox(x, y, inactiveID, activeID, selected, switchID);
|
|
builder.AddRadio(x, y, inactiveID, activeID, selected, switchID);
|
|
builder.AddTextEntry(x, y, w, h, hue, entryID, initialText);
|
|
builder.AddTextEntryLimited(x, y, w, h, hue, entryID, initialText, maxLength);
|
|
```
|
|
|
|
### Modifiers
|
|
```csharp
|
|
builder.SetNoClose(); // Disable right-click close
|
|
builder.SetNoMove(); // Disable dragging
|
|
builder.SetNoResize(); // Disable resizing
|
|
builder.SetNoDispose(); // Disable dispose
|
|
builder.AddTooltip(num); // Tooltip on hover
|
|
builder.AddItemProperty(serial); // Item property tooltip
|
|
```
|
|
|
|
## Response Handling
|
|
|
|
```csharp
|
|
public override void OnResponse(NetState sender, in RelayInfo info)
|
|
{
|
|
var mobile = sender.Mobile;
|
|
|
|
// Button ID (0 = close/cancel, 1+ = custom buttons)
|
|
switch (info.ButtonID)
|
|
{
|
|
case 0: return; // Closed
|
|
case 1:
|
|
// Handle button 1
|
|
break;
|
|
}
|
|
|
|
// Check checkbox/radio state
|
|
bool isChecked = info.IsSwitched(switchID);
|
|
|
|
// Get text entry value
|
|
string text = info.GetTextEntry(entryID);
|
|
}
|
|
```
|
|
|
|
### Button ID Convention
|
|
- `0` = Close/Cancel (default when player closes gump)
|
|
- `1+` = Custom action buttons
|
|
- Use `GumpButtonType.Page` for page navigation buttons (don't trigger OnResponse)
|
|
|
|
## BaseGump Properties
|
|
|
|
```csharp
|
|
public int X { get; set; } // Gump X position
|
|
public int Y { get; set; } // Gump Y position
|
|
public virtual bool Singleton => false; // Only one instance per player
|
|
public int TypeID { get; } // Unique type identifier
|
|
public Serial Serial { get; } // Gump serial
|
|
```
|
|
|
|
## Common Gump IDs (Background Art)
|
|
|
|
| ID | Description |
|
|
|---|---|
|
|
| 5054 | Dark stone background |
|
|
| 9200 | Scroll background |
|
|
| 9250 | Light parchment |
|
|
| 3600 | Brown wood panel |
|
|
| 5120 | Gray stone border |
|
|
| 2620 | Ornate gold frame |
|
|
|
|
## Common Button IDs (Art)
|
|
|
|
| Normal/Pressed | Description |
|
|
|---|---|
|
|
| 4005/4007 | Small right arrow (green) |
|
|
| 4017/4019 | Small X (red) |
|
|
| 4023/4025 | Small left arrow |
|
|
| 4020/4022 | Small checkmark |
|
|
| 4029/4031 | Large right arrow |
|
|
| 247/248 | Large green gem |
|
|
| 241/242 | Large red gem |
|
|
|
|
## Important Properties
|
|
|
|
### Singleton
|
|
```csharp
|
|
public override bool Singleton => true;
|
|
```
|
|
When `true`, the gump system 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 duplicate gumps the player must close individually.
|
|
|
|
### Cached (StaticGump only)
|
|
```csharp
|
|
protected virtual bool Cached => true; // default
|
|
```
|
|
Controls whether `StaticGump<T>` caches its compiled layout. The layout is compiled once on first send, then reused for all subsequent instances.
|
|
|
|
**Set to `false` during development** to force the layout to rebuild every send — useful for hot-reload iteration and debugging layout changes without restarting the server:
|
|
```csharp
|
|
// Temporary: disable caching while iterating on layout
|
|
protected override bool Cached => false;
|
|
```
|
|
**Remove the override (or set back to `true`) before committing.** Leaving it `false` in production wastes CPU recompiling identical layouts.
|
|
|
|
## Empty Gump Rule (CRITICAL)
|
|
|
|
**NEVER send a gump with no visual components.** An empty gump (no background, no buttons, no content) has no close button and no right-click dismiss — the client cannot close it. This causes a **gump leak** on both the client and server: the gump stays in the tracking list forever, the client renders an invisible undismissable element, and the slot is consumed until the player relogs.
|
|
|
|
Empty gumps typically happen when a developer short-circuits inside the constructor or `BuildLayout`:
|
|
```csharp
|
|
// BAD: Short-circuit in constructor creates an empty gump
|
|
public MyGump(Mobile from) : base(50, 50)
|
|
{
|
|
if (!from.Alive)
|
|
return; // Gump is already constructed — it's empty but will still be sent!
|
|
|
|
AddPage(0);
|
|
AddBackground(0, 0, 400, 300, 5054);
|
|
// ...
|
|
}
|
|
```
|
|
|
|
### The Fix: Static DisplayTo Pattern
|
|
|
|
Use a static entry-point method that validates prerequisites **before** constructing the gump. The constructor is private — the only way to create the gump is through `DisplayTo`, which guarantees the gump is never empty. See `GoGump.cs` for the canonical example:
|
|
|
|
```csharp
|
|
public class MyGump : DynamicGump // or StaticGump<MyGump>, or Gump
|
|
{
|
|
public override bool Singleton => true;
|
|
|
|
// Private constructor — can only be called from DisplayTo
|
|
private MyGump(Mobile from, SomeData data) : base(50, 50)
|
|
{
|
|
// Safe to build layout — prerequisites already validated
|
|
}
|
|
|
|
protected override void BuildLayout(ref DynamicGumpBuilder builder)
|
|
{
|
|
// Always produces visual content — DisplayTo guarantees valid state
|
|
builder.AddPage();
|
|
builder.AddBackground(0, 0, 400, 300, 5054);
|
|
// ...
|
|
}
|
|
|
|
// Static entry point — validates before constructing
|
|
public static void DisplayTo(Mobile from)
|
|
{
|
|
if (!from.Alive || from.NetState == null)
|
|
return; // No gump created at all
|
|
|
|
var data = GetData(from);
|
|
if (data == null)
|
|
return; // No gump created at all
|
|
|
|
from.SendGump(new MyGump(from, data));
|
|
}
|
|
}
|
|
```
|
|
|
|
**Key points:**
|
|
- Constructor is `private` — enforces that `DisplayTo` is the only entry point
|
|
- All validation/short-circuiting happens in `DisplayTo` before `new MyGump(...)` is called
|
|
- If prerequisites fail, no gump is constructed or sent
|
|
- The constructor and `BuildLayout` can assume valid state and always produce visual output
|
|
- Reference implementation: `Projects/UOContent/Gumps/Go/GoGump.cs`
|
|
|
|
## Converting Legacy Gump to DynamicGump / StaticGump
|
|
|
|
The legacy `Gump` class (in `Gumps/Base/Legacy/Gump.cs`) builds layouts by appending `GumpEntry` objects to a list. The modern `DynamicGump` and `StaticGump<T>` use ref struct builders that write directly to buffers — fewer allocations, better performance.
|
|
|
|
### Step-by-Step Conversion
|
|
|
|
#### 1. Choose the target type
|
|
|
|
| If the layout... | Convert to |
|
|
|---|---|
|
|
| Is the same structure every time (fixed elements, maybe some dynamic text) | `StaticGump<T>` |
|
|
| Changes shape based on instance data (loops, conditionals that add/remove elements) | `DynamicGump` |
|
|
|
|
When in doubt, use `DynamicGump` — it's simpler and still much better than legacy `Gump`.
|
|
|
|
#### 2. Change the class declaration
|
|
|
|
```csharp
|
|
// Legacy
|
|
public class MyGump : Gump
|
|
|
|
// Modern — pick one:
|
|
public class MyGump : DynamicGump
|
|
public class MyGump : StaticGump<MyGump>
|
|
```
|
|
|
|
#### 3. Move layout code into BuildLayout
|
|
|
|
Legacy gumps build their layout in the constructor. Modern gumps build it in `BuildLayout`:
|
|
|
|
```csharp
|
|
// Legacy — layout in constructor
|
|
public class OldGump : Gump
|
|
{
|
|
public OldGump(Mobile from) : base(50, 50)
|
|
{
|
|
AddPage(0);
|
|
AddBackground(0, 0, 400, 300, 5054);
|
|
AddLabel(20, 20, 0x480, "Hello");
|
|
AddButton(20, 260, 4005, 4007, 1);
|
|
}
|
|
}
|
|
|
|
// Modern DynamicGump — layout in BuildLayout
|
|
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.AddPage();
|
|
builder.AddBackground(0, 0, 400, 300, 5054);
|
|
builder.AddLabel(20, 20, 0x480, "Hello");
|
|
builder.AddButton(20, 260, 4005, 4007, 1);
|
|
}
|
|
|
|
public static void DisplayTo(Mobile from)
|
|
{
|
|
from.SendGump(new NewGump(from));
|
|
}
|
|
}
|
|
```
|
|
|
|
#### 4. Key API differences
|
|
|
|
| Legacy `Gump` | Modern builder |
|
|
|---|---|
|
|
| `AddPage(0)` | `builder.AddPage()` (0 is the default) |
|
|
| `AddHtml(x, y, w, h, text, bg, scroll)` | `builder.AddHtml(x, y, w, h, text, background: bg, scrollbar: scroll)` |
|
|
| `Closable = false` | `builder.SetNoClose()` |
|
|
| `Draggable = false` | `builder.SetNoMove()` |
|
|
| `Resizable = false` | `builder.SetNoResize()` |
|
|
| `Disposable = false` | `builder.SetNoDispose()` |
|
|
| `AddLabel(x, y, hue, string)` | `builder.AddLabel(x, y, hue, ReadOnlySpan<char>)` |
|
|
| `Intern(string)` / string list | Not needed — builder handles strings internally |
|
|
|
|
#### 5. For StaticGump: extract dynamic text into placeholders
|
|
|
|
If converting to `StaticGump<T>` and some text varies per instance, replace those `AddLabel`/`AddHtml` calls with placeholder versions and fill them in `BuildStrings`:
|
|
|
|
```csharp
|
|
// In BuildLayout:
|
|
builder.AddLabelPlaceholder(20, 20, 0x480, "playerName");
|
|
builder.AddHtmlPlaceholder(20, 50, 360, 200, "description", false, true);
|
|
|
|
// In BuildStrings:
|
|
protected override void BuildStrings(ref GumpStringsBuilder builder)
|
|
{
|
|
builder.SetStringSlot("playerName", _from.Name);
|
|
builder.SetHtmlText("description", _description, "#FFC000", 4);
|
|
}
|
|
```
|
|
|
|
#### 6. Update OnResponse signature
|
|
|
|
```csharp
|
|
// Legacy
|
|
public override void OnResponse(NetState sender, RelayInfo info)
|
|
|
|
// Modern (RelayInfo is passed by ref)
|
|
public override void OnResponse(NetState sender, in RelayInfo info)
|
|
```
|
|
|
|
#### 7. Add DisplayTo and make constructor private
|
|
|
|
Always add a static `DisplayTo` method and make the constructor private to prevent empty gumps (see Empty Gump Rule above).
|
|
|
|
## When to Use Which
|
|
|
|
| Scenario | Type | Reason |
|
|
|---|---|---|
|
|
| Confirmation dialog | StaticGump | Fixed layout, shown frequently |
|
|
| Warning prompt | StaticGump | Fixed layout |
|
|
| Settings menu | StaticGump | Fixed structure |
|
|
| Item list (variable length) | DynamicGump | Layout depends on data |
|
|
| Craft menu | DynamicGump | Player-specific recipes |
|
|
| Search results | DynamicGump | Variable result count |
|
|
| Vendor inventory | DynamicGump | Different items per vendor |
|
|
|
|
## Key File References
|
|
|
|
| File | Description |
|
|
|---|---|
|
|
| `Projects/UOContent/Gumps/Base/BaseGump.cs` | Abstract base class |
|
|
| `Projects/UOContent/Gumps/Base/StaticGump.cs` | Cached static gump |
|
|
| `Projects/UOContent/Gumps/Base/DynamicGump.cs` | Dynamic gump |
|
|
| `Projects/UOContent/Gumps/Base/StaticGumpBuilder.cs` | Static layout builder |
|
|
| `Projects/UOContent/Gumps/Base/DynamicGumpBuilder.cs` | Dynamic layout builder |
|
|
| `Projects/UOContent/Gumps/Base/GumpStringsBuilder.cs` | String slot builder |
|
|
| `Projects/UOContent/Gumps/Base/GumpLayoutBuilder.cs` | Shared layout methods |
|
|
| `Projects/UOContent/Gumps/Base/GumpSystem.cs` | Extension methods |
|
|
| `Projects/UOContent/Gumps/StaticWarningGump.cs` | Example static gump |
|