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 :#
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:
- Dynamic regions first — runtime regions take precedence over JSON-defined ones
- Higher priority first —
DefaultPriorityis 50 - 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:
- Calls
OnRegister()virtual - Adds self to
Parent.Children(if parent exists) - Adds self to
Region.Regionsglobal list - Adds self to relevant
Map.Sectorlists - Stores sector references in
Sectorsarray
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 byRegionByNameConverterviaRegion.Find(name, map)MinExpansion/MaxExpansion— Region only loads if server expansion is within rangeArea— 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
- Behavior inheritance: Every virtual hook delegates to
Parentby default. A child only needs to override what it changes. - Priority: Children have higher
ChildLevelthan parents, so they're checked first in sector lookups. - Entry/exit events: When a mobile moves from parent to child (or vice versa),
OnExitis called on regions being left andOnEnteron regions being entered, walking the hierarchy. IsPartOf<T>(): Walks up the parent chain. A region inside "Britain Bank" returns true forIsPartOf<GuardedRegion>()becauseTownRegion(Britain) is aGuardedRegion.
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()onOnLocationChange,OnMapChange, andOnAfterDelete - In
AfterDeserialization, defer registration withTimer.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 |