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

22 KiB

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

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

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

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

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

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

{
  "$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():

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:

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:

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

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:

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

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

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)

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

public override bool AllowHousing(Mobile from, Point3D p) => false;

Custom Light Level

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

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