chore: Adds AI instructions and SKILLs for ModernUO codebase (#2347)

Summary

  - Adds CLAUDE.md at repo root with 14 terse code audit rules (always loaded, low token cost)
  - Adds pointer files for other AI tools: AGENTS.md (Codex), GEMINI.md, .github/COPILOT-INSTRUCTIONS.md (Copilot), .cursorrules (Cursor) — all redirect to CLAUDE.md as single source of truth
  - Gitignores /.claude so personal AI config isn't distributed
  - Moves Claude skills to dev-docs/claude-skills/ (opt-in, not auto-loaded)
  - Adds 14 dev-docs covering codebase conventions

  Code Audit Rules (in CLAUDE.md)

  1. LINQ tiered rules (Tier 1 free, Tier 2 warm, Tier 3 forbidden)
  2. No Console.WriteLine — use LogFactory.GetLogger()
  3. No concurrency primitives in game code
  4. No World.Mobiles/World.Items iteration
  5. Clean up refs in OnDelete()/OnAfterDelete()
  6. Cancel timers in OnDelete()/OnAfterDelete()
  7. STArrayPool<T>.Shared not ArrayPool<T>.Shared
  8. PooledRefList<T> not new List<T>() on hot paths
  9. Serialization: partial class, [Constructible], no serialized TimerExecutionToken
  10. No Task.Run/new Thread() in game code
  11. Never assume era — ask which expansion
  12. _camelCase fields, PascalCase properties/methods
  13. No empty gumps — use DisplayTo() pattern
  14. PropertyList string literals must be {} holes, cliloc-as-argument uses :#
This commit is contained in:
Kamron Batman 2026-03-01 11:42:19 -08:00 committed by GitHub
parent e77a566f32
commit 1391c563fe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
34 changed files with 8549 additions and 0 deletions

507
dev-docs/gump-system.md Normal file
View file

@ -0,0 +1,507 @@
# 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);
```
### 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 |