ModernUO/dev-docs/regions.md
Kamron Batman 1391c563fe
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 :#
2026-03-01 11:42:19 -08:00

567 lines
22 KiB
Markdown

# ModernUO Region System
This document covers ModernUO's region system: spatial areas on the map that control gameplay rules, spawning, spell restrictions, housing, combat, lighting, and more.
## Overview
Regions are named, polygonal volumes on a map. When a mobile enters a region, the server calls that region's virtual hooks to control behavior. Regions are hierarchical — a child region inherits its parent's behavior and can selectively override it.
Regions are loaded from `Data/regions.json` at startup via polymorphic JSON deserialization, but they can also be created **dynamically** at runtime (e.g., house regions, champion spawn areas).
## Class Hierarchy
```
Region (Server) ← Engine-level base, 30+ virtual hooks
└─ BaseRegion (UOContent) ← Game-level base: CheckTravel, spawn weights, RuneName
├─ GuardedRegion ← NPC guards, vendor access, spell restrictions in town
│ ├─ NoHousingGuardedRegion ← GuardedRegion that also blocks housing overlap checks
│ └─ TownRegion ← Type marker for towns (adds Entrance property)
├─ DungeonRegion ← Dungeon lighting, no housing, young not protected
│ └─ NoTravelSpellsAllowedRegion ← Blocks all travel spells for players
│ └─ MondainRegion ← Mondain's Legacy dungeon base
│ ├─ CrystalFieldRegion, IcyRiverRegion, ... (damage boost regions)
├─ NoHousingRegion ← Blocks housing placement (manual overlap check)
├─ GreenAcresRegion ← No housing, no travel, no Mark spell
├─ JailRegion ← Full lockdown: no skills, spells, travel, combat
├─ HouseRegion ← Dynamic: one per house, access/ban/lockdown control
├─ ChampionSpawnRegion ← Dynamic: champion spawn area effects
└─ (many more specialized regions)
```
## Region (Server Engine)
`Server.Region` is the engine-level base class in `Projects/Server/Regions/Region.cs`. It provides:
### Key Properties
| Property | Type | Description |
|---|---|---|
| `Name` | `string` | Region identifier (unique per map for JSON regions) |
| `Map` | `Map` | The map this region belongs to |
| `Parent` | `Region` | Parent region (null for top-level) |
| `Children` | `List<Region>` | Child regions nested inside this one |
| `Area` | `Rectangle3D[]` | Spatial bounds (one or more 3D rectangles) |
| `Priority` | `int` | Sort priority (default 50); higher = checked first |
| `ChildLevel` | `int` | Nesting depth (0 for top-level) |
| `Dynamic` | `bool` | True if created via constructor (not from JSON) |
| `Registered` | `bool` | Whether the region is active on the map |
| `GoLocation` | `Point3D` | Recall/bind target location |
| `Music` | `MusicName` | Music played for players in this region |
### Sort Order
Regions in each map sector are sorted by:
1. **Dynamic regions first** — runtime regions take precedence over JSON-defined ones
2. **Higher priority first**`DefaultPriority` is 50
3. **Higher child level first** — more deeply nested regions win
This means a child region always takes precedence over its parent, and dynamic regions (like `HouseRegion`) override static JSON regions.
### Static Lookup Methods
```csharp
// Find the most specific region at a world location (fast — uses sector index)
Region region = Region.Find(point3D, map);
// Find a named region on a map (slow — linear scan, use sparingly)
Region region = Region.Find("Britain", Map.Felucca);
```
### Hierarchy Traversal
```csharp
// Walk up the parent chain looking for a specific type
var dungeon = region.GetRegion<DungeonRegion>(); // null if not in a dungeon
var guarded = region.GetRegion<GuardedRegion>();
// Check if this region is a child of (or equal to) a specific region/type
bool inDungeon = region.IsPartOf<DungeonRegion>();
bool inBritain = region.IsPartOf("Britain");
// Check two types at once (avoids double traversal)
bool inDungeonOrGuarded = region.IsPartOf<DungeonRegion, GuardedRegion>();
```
### Registration / Unregistration
```csharp
region.Register(); // Adds to map sectors, Region.Regions list, parent's Children
region.Unregister(); // Removes from all of the above
```
Both are **idempotent** — calling `Register()` on an already-registered region is a no-op.
`Register()` does:
1. Calls `OnRegister()` virtual
2. Adds self to `Parent.Children` (if parent exists)
3. Adds self to `Region.Regions` global list
4. Adds self to relevant `Map.Sector` lists
5. Stores sector references in `Sectors` array
`Unregister()` does the reverse. **Warning**: unregistering a region that still has children logs a warning — unregister children first.
### Querying Region Contents
```csharp
// Get all players in this region (pooled — zero-alloc)
using var players = region.GetPlayersPooled();
// Get all mobiles in this region (pooled)
using var mobiles = region.GetMobilesPooled();
// Get count without allocating
int count = region.GetPlayerCount();
```
## Virtual Hooks
Region provides 30+ virtual methods. Each default implementation **delegates to Parent** (or returns a default if no parent). This means child regions automatically inherit parent behavior — you only override what you need.
### Lifecycle
| Method | Called when |
|---|---|
| `OnRegister()` | Region is registered on the map |
| `OnUnregister()` | Region is unregistered |
| `OnChildAdded(Region)` | A child region registers under this parent |
| `OnChildRemoved(Region)` | A child region unregisters |
### Movement & Entry
| Method | Called when |
|---|---|
| `OnMoveInto(Mobile, Direction, newLoc, oldLoc)` | Mobile attempts to enter; return false to block |
| `OnEnter(Mobile)` | Mobile enters this region |
| `OnExit(Mobile)` | Mobile leaves this region |
| `OnLocationChanged(Mobile, oldLoc)` | Mobile moves within the region |
### Combat & Interaction
| Method | Called when |
|---|---|
| `AllowHarmful(Mobile from, Mobile target)` | PvP/PvM harm attempt |
| `AllowBeneficial(Mobile from, Mobile target)` | Healing/buffing attempt |
| `OnAggressed(Mobile aggressor, Mobile aggressed, bool criminal)` | Aggression committed |
| `OnDidHarmful(Mobile harmer, Mobile harmed)` | Harmful action completed |
| `OnCriminalAction(Mobile, bool message)` | Criminal act committed |
| `OnCombatantChange(Mobile, old, new)` | Target change; return false to block |
| `SpellDamageScalar(Mobile caster, Mobile target, ref double damage)` | Modify spell damage |
### Spells & Skills
| Method | Called when |
|---|---|
| `OnBeginSpellCast(Mobile, ISpell)` | Spell cast attempt; return false to block |
| `OnSpellCast(Mobile, ISpell)` | Spell successfully cast |
| `OnSkillUse(Mobile, int skill)` | Skill use attempt; return false to block |
| `AllowGain(Mobile, Skill, object)` | Skill gain attempt; return false to block |
### World Rules
| Method | Called when |
|---|---|
| `AllowHousing(Mobile, Point3D)` | Housing placement check; return false to block |
| `AllowSpawn()` | Creature spawn check |
| `AcceptsSpawnsFrom(Region)` | Whether spawns from another region are accepted |
| `OnDecay(Item)` | Item decay; return false to prevent |
| `CheckAccessibility(Item, Mobile)` | Item access check; return false to deny |
| `GetResource(Type)` | Alter resource type for mining/harvesting |
| `MakeGuard(Mobile focus)` | Spawn a guard at a location |
### Environment
| Method | Called when |
|---|---|
| `AlterLightLevel(Mobile, ref int global, ref int personal)` | Modify light levels |
| `GetLogoutDelay(Mobile)` | Get logout timer duration |
| `CanUseStuckMenu(Mobile)` | Whether stuck menu is available |
| `OnSpeech(SpeechEventArgs)` | Speech in region (used for guard calls, house commands) |
| `OnResurrect(Mobile)` | Resurrection attempt; return false to block |
| `OnBeforeDeath(Mobile)` / `OnDeath(Mobile)` | Death handling |
## BaseRegion (UOContent)
`Server.Regions.BaseRegion` in `Projects/UOContent/Regions/BaseRegion.cs` extends `Region` with game-specific features:
### Additional Properties
| Property | Type | Default | Description |
|---|---|---|---|
| `YoungProtected` | `virtual bool` | `true` | Young players get warning gump on entry |
| `YoungMayEnter` | `virtual bool` | `true` | Young players allowed to enter |
| `MountsAllowed` | `virtual bool` | `true` | Mounts allowed in region |
| `DeadMayEnter` | `virtual bool` | `true` | Dead players can enter |
| `ResurrectionAllowed` | `virtual bool` | `true` | Resurrection permitted |
| `LogoutAllowed` | `virtual bool` | `true` | Logout permitted |
| `ExcludeFromParentSpawns` | `bool` | `false` | Blocks parent region spawners from placing here |
| `RuneName` | `string` | `null` | Name shown on recall runes |
| `NoLogoutDelay` | `bool` | `false` | Zero logout delay if not in combat |
### CheckTravel
`BaseRegion.CheckTravel` is the key hook for spell travel restrictions:
```csharp
public virtual bool CheckTravel(
Mobile m,
Point3D newLocation,
TravelCheckType travelType,
out TextDefinition message)
```
`TravelCheckType` values: `RecallFrom`, `RecallTo`, `GateFrom`, `GateTo`, `Mark`, `TeleportFrom`, `TeleportTo`.
Both the **current** region and the **destination** region are checked by `SpellHelper.CheckTravel`. If either returns false, the spell is blocked.
### Spawn Distribution
`BaseRegion` provides `InitRectangles()` which breaks overlapping `Area` rectangles into non-overlapping pieces and calculates weights for uniform spawn distribution. Used by `RegionSpawner`.
## JSON Region Loading
Regions are defined in `Distribution/Data/regions.json` and loaded at startup by `RegionJsonSerializer.LoadRegions()`.
### JSON Schema
```json
{
"$type": "DungeonRegion",
"Name": "Shame",
"Map": "Felucca",
"Priority": 50,
"Parent": { "Name": "Felucca", "Map": "Felucca" },
"Area": [
{ "x1": 5369, "y1": 1, "x2": 5627, "y2": 127 }
],
"GoLocation": { "x": 511, "y": 1565, "z": 0 },
"Music": "Dungeon9",
"Entrance": { "x": 511, "y": 1565, "z": 0 },
"MinExpansion": "None",
"MaxExpansion": "EJ"
}
```
Key fields:
- `$type` — Polymorphic type discriminator (must be a registered type name)
- `Parent` — Resolved by `RegionByNameConverter` via `Region.Find(name, map)`
- `MinExpansion` / `MaxExpansion` — Region only loads if server expansion is within range
- `Area` — Array of 2D or 3D rectangles
### Type Registration
All region types that appear in JSON must be registered in `RegionJsonRegistration.Configure()`:
```csharp
RegionJsonSerializer.Register<BaseRegion>();
RegionJsonSerializer.Register<TownRegion>();
RegionJsonSerializer.Register<DungeonRegion>();
RegionJsonSerializer.Register<GuardedRegion>();
// ... 30+ more types
```
On deserialization, each region's `Register()` is called automatically if the server expansion is in range.
## Child Regions
Child regions are regions with a `Parent`. They form a hierarchy:
```
Felucca (default region)
└─ Britain (TownRegion, guarded)
└─ Britain Bank (BaseRegion, NoLogoutDelay)
```
### How Children Work
1. **Behavior inheritance**: Every virtual hook delegates to `Parent` by default. A child only needs to override what it changes.
2. **Priority**: Children have higher `ChildLevel` than parents, so they're checked first in sector lookups.
3. **Entry/exit events**: When a mobile moves from parent to child (or vice versa), `OnExit` is called on regions being left and `OnEnter` on regions being entered, walking the hierarchy.
4. **`IsPartOf<T>()`**: Walks up the parent chain. A region inside "Britain Bank" returns true for `IsPartOf<GuardedRegion>()` because `TownRegion` (Britain) is a `GuardedRegion`.
### When to Use Child Regions
Use a child region when you need to **modify behavior within an existing region** without replacing it:
- **Dungeon sub-areas**: A treasure room inside a dungeon that has different light or spawn rules
- **Town districts**: A bank area with no logout delay inside a guarded town
- **Boss arenas**: A champion spawn area inside a dungeon that adds lighting effects and player ejection
- **Restricted zones**: A no-spell zone within a larger dungeon
**Example — dungeon chest with a localized effect zone**:
If you need a dungeon chest that creates a "cursed area" around it (e.g., damage over time, spell restrictions), create a child region of the dungeon:
```csharp
public class CursedChestRegion : BaseRegion
{
private readonly CursedChest _chest;
public CursedChestRegion(CursedChest chest)
: base(null, chest.Map, Region.Find(chest.Location, chest.Map), // parent = dungeon
new Rectangle2D(chest.X - 5, chest.Y - 5, 11, 11))
{
_chest = chest;
}
// Inherits DungeonRegion behavior (lighting, no housing, etc.)
// Only adds curse-specific effects
public override void OnEnter(Mobile m)
{
base.OnEnter(m);
if (m.Player)
m.SendMessage("You feel a dark presence...");
}
public override void SpellDamageScalar(Mobile caster, Mobile target, ref double damage)
{
base.SpellDamageScalar(caster, target, ref damage);
damage *= 1.25; // 25% more spell damage in cursed area
}
}
```
Because this is a **child** of the dungeon region, it inherits all dungeon rules (lighting, no housing, young protection = false) and only adds the curse effect on top.
## Dynamic Regions
Dynamic regions are created at runtime by game code rather than loaded from JSON. They're identified by `Dynamic = true` (set automatically in the constructor) and sort before static regions.
### Pattern 1: Item-Tracked Region (Register/Unregister)
The most common pattern — an item creates a region around itself and manages its lifecycle:
```csharp
public partial class ChampionSpawn : Item
{
private ChampionSpawnRegion m_Region;
public void UpdateRegion()
{
m_Region?.Unregister(); // Remove old region
if (!Deleted && Map != Map.Internal)
{
m_Region = GetRegion(); // Create new region
m_Region.Register(); // Add to map
}
}
public override void OnLocationChange(Point3D oldLoc)
{
// ... update spawn area coordinates ...
UpdateRegion(); // Re-register at new location
}
public override void OnMapChange()
{
// ... update child items' maps ...
UpdateRegion(); // Re-register on new map
}
public override void OnAfterDelete()
{
base.OnAfterDelete();
// ... cleanup child items ...
UpdateRegion(); // Unregisters (Deleted == true)
}
[AfterDeserialization(false)]
private void AfterDeserialization()
{
// Defer registration to next tick (world may not be fully loaded)
Timer.StartTimer(TimeSpan.Zero, UpdateRegion);
}
}
```
**Key points:**
- Call `UpdateRegion()` on `OnLocationChange`, `OnMapChange`, and `OnAfterDelete`
- In `AfterDeserialization`, defer registration with `Timer.StartTimer(TimeSpan.Zero, ...)` — regions rely on the map and parent regions being loaded first
- The `UpdateRegion()` method is idempotent: unregisters old, creates new if not deleted
### Pattern 2: House Region
Houses create a region with priority `DefaultPriority + 1` (higher than normal regions):
```csharp
public virtual void UpdateRegion()
{
m_Region?.Unregister();
if (Map != null)
{
m_Region = new HouseRegion(this);
m_Region.Register();
}
else
{
m_Region = null;
}
}
```
### Pattern 3: Parent-Aware Dynamic Child
When creating a dynamic region that should be a child of whatever region exists at a location:
```csharp
// Find the existing region at the item's location and use it as parent
var parent = Region.Find(item.Location, item.Map);
var childRegion = new MyCustomRegion(item, parent);
childRegion.Register();
```
The `ChampionSpawnRegion` constructor does exactly this:
```csharp
public ChampionSpawnRegion(ChampionSpawn spawn)
: base(null, spawn.Map, Region.Find(spawn.Location, spawn.Map), spawn.SpawnArea)
```
This makes the champion spawn region a child of whatever region it's placed in (dungeon, wilderness, etc.), inheriting that region's rules.
## Existing Region Types Quick Reference
### Base Types (Use for Most Tasks)
| Type | Inherits | Key Behavior |
|---|---|---|
| `BaseRegion` | `Region` | Game-level base; CheckTravel, RuneName, spawn weights |
| `GuardedRegion` | `BaseRegion` | NPC guards, no housing, town spell restrictions |
| `TownRegion` | `GuardedRegion` | Town marker (adds Entrance); inherits guard behavior |
| `DungeonRegion` | `BaseRegion` | Dungeon lighting, no housing, young not protected, no stuck menu on Felucca |
| `NoHousingRegion` | `BaseRegion` | Blocks housing placement (manual overlap check) |
| `NoHousingGuardedRegion` | `GuardedRegion` | Guards + no housing overlap check |
| `NoTravelSpellsAllowedRegion` | `DungeonRegion` | Blocks all travel spells for players |
| `GreenAcresRegion` | `BaseRegion` | No housing, no travel, no Mark |
| `JailRegion` | `BaseRegion` | Full lockdown: no skills, spells, combat, travel, or gain |
| `HouseRegion` | `BaseRegion` | Dynamic; per-house access control, lockdown, secure items |
### Choosing a Base for Your Region
| You Need | Inherit From |
|---|---|
| Standard overworld area with custom rules | `BaseRegion` |
| Town/city with guards | `TownRegion` or `GuardedRegion` |
| Dungeon area (dark, no housing) | `DungeonRegion` |
| Dungeon + no travel spells | `NoTravelSpellsAllowedRegion` |
| No housing only | `NoHousingRegion` |
| Full spell/skill lockdown | `JailRegion` (or custom `BaseRegion`) |
| Item-controlled dynamic area | `BaseRegion` (with parent = `Region.Find(...)`) |
### Specialized Regions (Registered for JSON)
These are used in `regions.json` for specific areas and can serve as references for custom regions:
| Type | Purpose |
|---|---|
| `MondainRegion` | Mondain's Legacy dungeon base (no travel) |
| `CrystalFieldRegion` | Cold damage boost zone |
| `IcyRiverRegion` | Cold damage boost zone |
| `AcidRiverRegion` | Poison damage boost zone |
| `PoisonedTreeRegion` | Poison damage boost zone |
| `PoisonedCemeteryRegion` | Poison damage boost zone |
| `LostCityEntranceRegion` | Special dungeon entrance |
| `BlackthornDungeonRegion` | Blackthorn-specific rules |
| `ExodusDungeonRegion` | Exodus dungeon rules |
| `DoomGuardianRegion` | Doom gauntlet area |
| `UnderwaterRegion` | Underwater mechanics |
| `ApprenticeRegion` | Apprentice quest zone |
| `SeaMarketRegion` | Sea market mechanics |
| `BattleRegion` | Myrmidex battle zone |
| `NewMaginciaRegion` | New Magincia rules |
| `TokunoDocksRegion` | Tokuno docks mechanics |
| `TombOfKingsRegion` / `ToKBridgeRegion` | Tomb of Kings areas |
| `WrongLevel3Region` / `WrongJailRegion` | Wrong dungeon jail |
| `CousteauPerronHouseRegion` | Special house region |
## Common Patterns
### Blocking Travel Spells
```csharp
public override bool CheckTravel(
Mobile m, Point3D newLocation, TravelCheckType travelType, out TextDefinition message)
{
message = null; // null = use default "Thy spell doth not appear to work"
return m.AccessLevel > AccessLevel.Player; // Staff can always travel
}
```
### Blocking Specific Spells (e.g., Mark)
```csharp
public override bool OnBeginSpellCast(Mobile m, ISpell s)
{
if (m.AccessLevel == AccessLevel.Player && s is MarkSpell)
{
m.SendLocalizedMessage(501802); // Thy spell doth not appear to work...
return false;
}
return base.OnBeginSpellCast(m, s);
}
```
### Blocking Housing
```csharp
public override bool AllowHousing(Mobile from, Point3D p) => false;
```
### Custom Light Level
```csharp
public override void AlterLightLevel(Mobile m, ref int global, ref int personal)
{
global = LightCycle.DungeonLevel; // Dungeon darkness
}
```
### No Logout Delay (Safe Zone)
Set `NoLogoutDelay = true` on a `BaseRegion`. The delay is zero only if the mobile has no aggressors and is not criminal.
### Damage Modification
```csharp
public override void SpellDamageScalar(Mobile caster, Mobile target, ref double damage)
{
base.SpellDamageScalar(caster, target, ref damage);
damage *= 1.5; // 50% more spell damage
}
```
## Common Mistakes
| Mistake | Problem | Fix |
|---|---|---|
| Not calling `Unregister()` before `Register()` | Duplicate region entries | Always unregister old before creating new |
| Registering in deserialization constructor | Map/parent may not exist yet | Use `[AfterDeserialization]` with `Timer.StartTimer(TimeSpan.Zero, ...)` |
| Forgetting to unregister on item delete | Ghost region remains on map | Call `UpdateRegion()` or `Unregister()` in `OnAfterDelete()` |
| Creating a child without finding the parent | Region has no parent hierarchy benefits | Use `Region.Find(location, map)` as parent |
| Not registering type for JSON | Deserialization fails silently | Add `RegionJsonSerializer.Register<T>()` in `RegionJsonRegistration.Configure()` |
| Using `Region.Find(string, Map)` in hot paths | Linear scan, O(n) | Use `Region.Find(Point3D, Map)` which uses sector index |
| Unregistering parent before children | Warning logged, orphaned children | Unregister children first |
## Key File References
| File | Description |
|---|---|
| `Projects/Server/Regions/Region.cs` | Engine-level base class (30+ virtual hooks) |
| `Projects/Server/Regions/RegionJsonSerializer.cs` | JSON loading, type registration |
| `Projects/Server/Json/Converters/RegionByNameConverter.cs` | Parent resolution from JSON |
| `Projects/UOContent/Regions/BaseRegion.cs` | Game-level base: CheckTravel, spawn weights |
| `Projects/UOContent/Regions/GuardedRegion.cs` | NPC guards, town mechanics |
| `Projects/UOContent/Regions/DungeonRegion.cs` | Dungeon lighting and rules |
| `Projects/UOContent/Regions/TownRegion.cs` | Town type marker |
| `Projects/UOContent/Regions/NoHousingRegion.cs` | Housing block |
| `Projects/UOContent/Regions/NoHousingGuardedRegion.cs` | Guarded + housing block |
| `Projects/UOContent/Regions/NoTravelSpellsAllowedRegion.cs` | Travel spell block |
| `Projects/UOContent/Regions/GreenAcresRegion.cs` | Multi-restriction region |
| `Projects/UOContent/Regions/HouseRegion.cs` | Dynamic per-house region |
| `Projects/UOContent/Regions/RegionJsonRegistration.cs` | All registered JSON types |
| `Projects/UOContent/Engines/CannedEvil/ChampionSpawn.cs` | Dynamic region lifecycle example |
| `Projects/UOContent/Multis/Houses/BaseHouse.cs` | House region lifecycle example |
| `Projects/UOContent/Spells/Base/SpellHelper.cs` | Travel check dispatch |
| `Distribution/Data/regions.json` | Static region definitions |