ModernUO/dev-docs/claude-skills/modernuo-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

7.9 KiB

name description
modernuo-regions Trigger when creating custom regions, dynamic item-controlled areas, dungeon sub-zones, travel restrictions, housing blocks, spawn control, or any spatial gameplay rule. When working with Region, BaseRegion, GuardedRegion, DungeonRegion, HouseRegion, or CheckTravel.

ModernUO Regions

When This Activates

  • Creating a custom region (dungeon zone, boss arena, restricted area)
  • Creating a dynamic region tied to an item (chest effect zone, spawn area)
  • Adding travel spell restrictions, housing blocks, or spawn control
  • Working with Region, BaseRegion, GuardedRegion, DungeonRegion, HouseRegion
  • Overriding CheckTravel, AllowHousing, OnEnter, OnBeginSpellCast, etc.
  • Modifying region JSON or RegionJsonRegistration

Key Rules

  1. Inherit from the right baseBaseRegion for general, DungeonRegion for dungeons, GuardedRegion for towns, NoTravelSpellsAllowedRegion for no-travel dungeons
  2. Dynamic regions must Register/Unregister — call Register() after creation, Unregister() before recreation or deletion
  3. Use Region.Find(location, map) as parent for dynamic child regions — this inherits the existing region's behavior
  4. Defer registration in [AfterDeserialization] — use Timer.StartTimer(TimeSpan.Zero, UpdateRegion) since map/parents may not be loaded yet
  5. Always override AllowHousing returning false if your region should block housing
  6. Staff bypass — travel/spell checks should allow AccessLevel > Player
  7. Virtual hooks delegate to Parent — you only need to override what you change

Choosing a Base Class

You Need Inherit From
Custom overworld area BaseRegion
Dungeon area (dark, no housing) DungeonRegion
Dungeon + no travel spells NoTravelSpellsAllowedRegion
Town with guards GuardedRegion or TownRegion
No housing only NoHousingRegion
Item-controlled dynamic area BaseRegion (parent = Region.Find(...))

Quick Patterns

Static Region (JSON-Defined)

Add to Distribution/Data/regions.json:

{
  "$type": "NoTravelSpellsAllowedRegion",
  "Name": "My Dungeon",
  "Map": "Felucca",
  "Parent": { "Name": "Felucca", "Map": "Felucca" },
  "Area": [{ "x1": 100, "y1": 200, "x2": 300, "y2": 400 }],
  "GoLocation": { "x": 150, "y": 250, "z": 0 },
  "Music": "Dungeon9"
}

Register the type (if new) in RegionJsonRegistration.Configure():

RegionJsonSerializer.Register<MyCustomRegion>();

Custom Region Class

public class MyCustomRegion : BaseRegion
{
    public MyCustomRegion(string name, Map map, Region parent, params Rectangle3D[] area)
        : base(name, map, parent, area)
    {
    }

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

    public override bool CheckTravel(
        Mobile m, Point3D newLocation, TravelCheckType travelType, out TextDefinition message)
    {
        message = null;
        return m.AccessLevel > AccessLevel.Player;
    }
}

Dynamic Item-Tracked Region

Full pattern for an item that creates a region around itself:

public partial class MySpecialItem : Item
{
    private MyItemRegion _region;

    public void UpdateRegion()
    {
        _region?.Unregister();

        if (!Deleted && Map != Map.Internal)
        {
            _region = new MyItemRegion(this);
            _region.Register();
        }
    }

    public override void OnLocationChange(Point3D oldLoc)
    {
        base.OnLocationChange(oldLoc);
        UpdateRegion();
    }

    public override void OnMapChange()
    {
        base.OnMapChange();
        UpdateRegion();
    }

    public override void OnAfterDelete()
    {
        base.OnAfterDelete();
        UpdateRegion(); // Unregisters because Deleted == true
    }

    [AfterDeserialization(false)]
    private void AfterDeserialization()
    {
        Timer.StartTimer(TimeSpan.Zero, UpdateRegion); // Defer to next tick
    }
}

public class MyItemRegion : BaseRegion
{
    public MySpecialItem Item { get; }

    public MyItemRegion(MySpecialItem item)
        : base(null, item.Map,
            Region.Find(item.Location, item.Map),  // Parent = existing region
            new Rectangle2D(item.X - 5, item.Y - 5, 11, 11))
    {
        Item = item;
    }

    public override void OnEnter(Mobile m)
    {
        base.OnEnter(m);
        // Custom enter logic
    }
}

Child Region Inside a Dungeon

When you want effects that don't break dungeon rules (inherits lighting, no housing, etc.):

public class BossArenaRegion : BaseRegion
{
    public BossArenaRegion(Item source)
        : base(null, source.Map,
            Region.Find(source.Location, source.Map),  // Parent = dungeon
            new Rectangle2D(source.X - 10, source.Y - 10, 21, 21))
    {
    }

    // Inherits DungeonRegion behavior from parent automatically
    // Only add what's different:

    public override bool CheckTravel(
        Mobile m, Point3D newLocation, TravelCheckType travelType, out TextDefinition message)
    {
        message = null;
        return m.AccessLevel > AccessLevel.Player; // No escape during boss fight
    }

    public override void SpellDamageScalar(Mobile caster, Mobile target, ref double damage)
    {
        base.SpellDamageScalar(caster, target, ref damage);
        damage *= 1.25; // Bonus damage in arena
    }
}

Blocking Specific Spells

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);
}

Region Hierarchy & IsPartOf

Virtual hooks delegate to Parent — child regions inherit all parent behavior:

// A child of DungeonRegion automatically has:
// - Dungeon lighting
// - No housing
// - YoungProtected = false

// Check hierarchy:
region.IsPartOf<DungeonRegion>()   // true if this or any ancestor is DungeonRegion
region.GetRegion<GuardedRegion>()  // returns first GuardedRegion in ancestor chain

Existing Region Types Reference

Type Key Behavior
BaseRegion CheckTravel, spawn weights, RuneName
GuardedRegion NPC guards, no housing, town spell restrictions
TownRegion Guard behavior + Entrance property
DungeonRegion Dungeon lighting, no housing, young not protected
NoTravelSpellsAllowedRegion Blocks all travel spells (extends DungeonRegion)
NoHousingRegion Blocks housing placement
NoHousingGuardedRegion Guards + housing block
GreenAcresRegion No housing, no travel, no Mark
JailRegion Full lockdown: no skills, spells, combat, travel
HouseRegion Dynamic per-house; access, bans, lockdowns
MondainRegion Mondain's Legacy dungeon (no travel)

Anti-Patterns

  • Registering regions in deserialization constructors: Map/parents may not be loaded — use [AfterDeserialization] with Timer.StartTimer(TimeSpan.Zero, ...)
  • Forgetting Unregister() on item delete: Ghost region stays on map — always unregister in OnAfterDelete()
  • Not using parent for dynamic regions: Loses inherited behavior (lighting, guards, etc.) — pass Region.Find(location, map) as parent
  • Using Region.Find(string, Map) in hot paths: Linear O(n) scan — use Region.Find(Point3D, Map) (sector-indexed)
  • Overriding a hook without calling base: Breaks parent delegation chain — always call base.Method() unless intentionally blocking

See Also

  • dev-docs/regions.md — Complete region system documentation
  • dev-docs/timers.md — Timer system (for deferred registration)
  • dev-docs/claude-skills/modernuo-content-patterns.md — Item/Mobile lifecycle (OnAfterDelete, AfterDeserialization)
  • dev-docs/claude-skills/modernuo-serialization.md — Serialization and AfterDeserialization hooks