ModernUO/dev-docs/content-patterns.md
Kamron Batman 843a29e7dc
refactor: master views over BaseCreature's one master reference (#2674)
## Summary

After #2670, `BaseCreature` stores one master. Callers still reached it through three differently gated names plus hand-rolled combinations of the `Controlled`/`Summoned` flags. Each gate is now one named view:

| Member | Definition | Meaning |
|---|---|---|
| `Master` | the reference; its setter does the follower bookkeeping; serialized as before | whoever the creature answers to |
| `ControlMaster` | `Controlled ? Master : null` | the owner |
| `SummonMaster` | `Summoned ? Master : null` | the summoner |
| `GetMaster()` | `ControlMaster ?? SummonMaster` | who answers for the creature |

`ControlMaster` and `SummonMaster` stay settable so custom code keeps compiling: both setters assign `Master`. There is no guard, so a write through either one can read back as null until the matching flag is set.

An enraged creature is the one shape with a master and neither flag. It keeps its meer through `Master`, while `GetMaster()` stays null, so the meer never answers for it (notoriety, kill credit).

Readers move onto the view they mean:
- "Owner or summoner" pairs and `ControlMaster ?? SummonMaster` collapse into `GetMaster()`: pack instinct, familiars, golem, house access, mounts, pack horse, Solen friendship, spell target and guild checks, and the `[` pet command.
- Checks such as `Controlled && ControlMaster == from` drop the flag the view already applies. Checks that don't reduce keep it: `Controlled && ControlMaster != from`, a possibly-null `from`, and `MageAI.CanDispel`'s `Summoned &&`.

**No behavior change, save format unchanged** (the schema generator produces no diff). Edge cases for custom code and shard operators:
- `SummonMaster = x` on a plain pet now replaces its owner (#2670 made that a no-op), and `ControlMaster = null` on an uncontrolled summon now clears its caster (it did nothing before).
- `SpellHelper.GetGuildFor` no longer falls back to an enraged creature's meer. Meer mages have no guild.

## Docs

- `dev-docs/content-patterns.md` § Masters: which view answers which question, how to set a master, and which flag checks are redundant.
- `dev-docs/runuo-migration-docs/09-items-mobiles-creatures.md` § Masters: RunUO's two-field reads mapped onto the views, plus the gotcha for creatures that set `SummonMaster` without `Summoned` (the write compiles, the read now returns null; use `Master`).
- The `modernuo-content-patterns` and `migrate-items-mobiles` skills point at both.

## Test plan

- [x] `MasterViewsTests`: all four views across wild, pet, controlled summon, uncontrolled summon and enraged shapes; reassigning `Master` moves follower slots; `SetControlMaster(null)` keeps an uncontrolled summon's caster; deleting a summon returns the caster's slots
- [x] Serialization tests updated to the new views (round-trip, legacy v22, v23 migration)
- [x] UOContent.Tests (1178 passed, 2 skipped) and Server.Tests (907) green
2026-09-24 22:45:24 -07:00

26 KiB
Raw Blame History

ModernUO Content Creation Patterns

This document covers the patterns and templates for creating game content in ModernUO: items, creatures, spells, skills, loot, context menus, and file organization.

Table of Contents

  1. New Item
  2. New Creature
  3. New Spell
  4. Skill Implementation
  5. Loot System
  6. Context Menus
  7. Entity Lifecycle
  8. File Organization

New Item

Minimal Item

using ModernUO.Serialization;

namespace Server.Items;

[SerializationGenerator(0)]
public partial class SimpleItem : Item
{
    [Constructible]
    public SimpleItem() : base(0x1234)  // itemID from UO art
    {
        Weight = 1.0;
    }

    public override string DefaultName => "a simple item";
    // OR: public override int LabelNumber => 1234567;  // cliloc number
}

Item with Properties and Behavior

using ModernUO.Serialization;

namespace Server.Items;

[SerializationGenerator(0)]
public partial class MagicLantern : Item
{
    [SerializableField(0)]
    [InvalidateProperties]
    [SerializedCommandProperty(AccessLevel.GameMaster)]
    private int _charges;

    [SerializableField(1)]
    [SerializedCommandProperty(AccessLevel.GameMaster)]
    private Mobile _owner;

    private TimerExecutionToken _glowTimer;

    [Constructible]
    public MagicLantern() : base(0xA25)
    {
        _charges = Utility.RandomMinMax(5, 15);
        Weight = 2.0;
        Light = LightType.Circle300;
        StartGlow();
    }

    public override string DefaultName => "a magic lantern";

    private void StartGlow()
    {
        Timer.StartTimer(TimeSpan.FromSeconds(3), TimeSpan.FromSeconds(3), Glow, out _glowTimer);
    }

    [AfterDeserialization]
    private void AfterDeserialization() => StartGlow();

    public override void OnAfterDelete()
    {
        _glowTimer.Cancel();
        base.OnAfterDelete();
    }

    private void Glow()
    {
        if (_charges > 0)
            Effects.SendLocationParticles(this, 0x376A, 9, 10, 5042);
    }

    public override void OnDoubleClick(Mobile from)
    {
        if (!IsChildOf(from.Backpack))
        {
            from.SendLocalizedMessage(1042001);  // Must be in your backpack
            return;
        }

        if (_charges <= 0)
        {
            from.SendMessage("The lantern is depleted.");
            return;
        }

        Charges--;
        from.SendMessage("The lantern flares brightly!");
        from.FixedParticles(0x376A, 9, 32, 5042, EffectLayer.Waist);
    }

    public override void GetProperties(IPropertyList list)
    {
        base.GetProperties(list);
        list.Add(1060741, $"{_charges}");  // "charges: ~1_val~"
    }
}

Common Item Base Classes

Base Class Use For
Item Generic items
BaseWeapon Melee weapons
BaseRanged Ranged weapons (bows, crossbows)
BaseArmor Armor pieces
BaseShield Shields
BaseClothing Wearable clothing
BaseJewel Rings, bracelets, necklaces
BaseContainer Containers (bags, boxes)
BasePotion Potions
BaseReagent Spell reagents
Food Edible items
SpellScroll Spell scrolls

Key Item Properties

Weight = 1.0;              // Item weight in stones
Stackable = true;          // Can stack with same type
Amount = 1;                // Stack amount
Movable = true;            // Can be picked up
Visible = true;            // Visible to players
Hue = 0;                   // Color hue (0 = default)
Light = LightType.Circle300; // Light emission
LootType = LootType.Regular; // Regular, Newbied, Blessed, Cursed
Layer = Layer.OneHanded;   // Equipment layer

New Creature

Basic Creature

using ModernUO.Serialization;
using Server.Items;

namespace Server.Mobiles;

[SerializationGenerator(0)]
public partial class ForestWolf : BaseCreature
{
    [Constructible]
    public ForestWolf() : base(AIType.AI_Melee, FightMode.Closest)
    {
        Body = 225;          // Wolf body graphic
        BaseSoundID = 0xE5;  // Base sound ID

        SetStr(80, 120);
        SetDex(90, 110);
        SetInt(20, 40);

        SetHits(60, 80);
        SetMana(0);

        SetDamage(8, 14);

        SetDamageType(ResistanceType.Physical, 100);

        SetResistance(ResistanceType.Physical, 25, 35);
        SetResistance(ResistanceType.Fire, 5, 10);
        SetResistance(ResistanceType.Cold, 15, 25);
        SetResistance(ResistanceType.Poison, 10, 15);
        SetResistance(ResistanceType.Energy, 5, 10);

        SetSkill(SkillName.MagicResist, 30.0, 50.0);
        SetSkill(SkillName.Tactics, 50.0, 70.0);
        SetSkill(SkillName.Wrestling, 50.0, 70.0);

        Fame = 600;
        Karma = 0;

        VirtualArmor = 28;

        Tamable = true;
        ControlSlots = 1;
        MinTameSkill = 50.1;
    }

    public override string CorpseName => "a wolf corpse";
    public override string DefaultName => "a forest wolf";
    public override int Meat => 1;
    public override int Hides => 6;
    public override HideType HideType => HideType.Regular;
    public override FoodType FavoriteFood => FoodType.Meat;
    public override PackInstinct PackInstinct => PackInstinct.Canine;

    public override void GenerateLoot()
    {
        AddLoot(LootPack.Meager);
    }
}

AI Types

AIType Use For
AI_Melee Warriors, melee fighters
AI_Mage Spellcasters
AI_Archer Ranged attackers
AI_Animal Passive animals (flee/fight back)
AI_Predator Hunting animals
AI_Healer Healing NPCs
AI_Vendor Shop NPCs
AI_Berserk Mindless aggressors
AI_Thief Pickpockets

Bespoke policy: ForcedAI. A creature whose behavior is not one of the stock AIs overrides protected override BaseAI ForcedAI => new MyAI(this); and ChangeAIType uses that instance regardless of AIType (CloneAI for mirror images, FamiliarAI for necromancy familiars, FactionGuardAI, SpellbinderAI). The property is read exactly once per ChangeAIType; do not put side effects in it. Movement or order decisions belong in the AI class — OnThink is for content extras (see the excess-call contract below). A controlled creature is dispatched through Obey(), an uncontrolled one through Think(); an AI that owns both routes them into one decision (FamiliarAI.Act). Overriding IssueOrder to return a fixed order is how "command immunity" is expressed without bypassing the order machinery. OnAggressiveAction(aggressor) -> bool is the retaliation policy: BaseCreature sets no combatant of its own and runs StopFlee/ForceReacquire only on true, so an AI that returns false has refused the fight outright.

Fight Modes

FightMode Behavior
None Never attacks
Aggressor Only retaliates
Strongest Targets highest-stat enemy
Weakest Targets lowest-stat enemy
Closest Targets nearest enemy
Evil Attacks aggressors or evil-karma targets

Creature Stats Guide

Creature Level Str Dex Int Hits Damage Fame
Weak 30-60 30-50 10-20 20-40 2-6 100-300
Average 80-120 60-90 20-40 60-100 6-14 500-1500
Strong 150-250 80-120 50-100 120-200 12-22 2000-5000
Elite 300-500 100-150 100-200 250-500 18-30 5000-15000
Boss 500-1000 150-250 200-400 500-2000 25-40 15000+

Optional Creature Overrides

public override Poison PoisonImmune => Poison.Regular;   // Poison immunity
public override Poison HitPoison => Poison.Lesser;       // Melee poison
public override double HitPoisonChance => 0.2;           // 20% poison chance
public override bool CanRummageCorpses => true;           // Loots corpses
public override bool BardImmune => true;                  // Cannot be provoked/peaced
public override bool Unprovokable => true;                // Cannot be provoked
public override bool CanFly => true;                      // Can fly
public override int TreasureMapLevel => 3;               // Drops treasure map
public override double WeaponAbilityChance => 0.4;        // Weapon ability chance

Masters: owner, summoner, responsible party

A creature stores one master. Read it through the view that matches the question:

Read Returns Use it for
ControlMaster the master while Controlled, else null the owner: commands, bonding, friends, stabling
SummonMaster the master while Summoned, else null the summoner: dispel, summon acquire rules
GetMaster() ControlMaster ?? SummonMaster whoever answers for the creature: notoriety, kill credit, guild and party checks
Master the stored reference, flags ignored a creature's own AI following or defending a master it has no flag for

A master with neither flag is legal. A meer mage's enraged creatures carry their meer in Master without being Controlled or Summoned, so GetMaster() is null and the meer never answers for them.

To set a master, tame through SetControlMaster(m) (it raises Controlled, resets the order and checks follower slots) and summon through BaseCreature.Summon(...). Assign Master directly only for a master with no flag. The ControlMaster and SummonMaster setters are aliases for Master: they don't set Controlled or Summoned, so the value reads back as null until the flag is set. Every assignment moves the creature's follower slots from the old master to the new one.

The view already checks its flag, so don't repeat it:

// Redundant: ControlMaster is null unless Controlled
if (Controlled && ControlMaster == from) { ... }
if (ControlMaster == from) { ... }

// Not equivalent: without the flag this is true for every wild creature
if (Controlled && ControlMaster != from) { ... }

Creature Speeds (think vs move clocks)

All "speed" values are delays in seconds (smaller = faster). A creature runs two clocks:

  • Think clock — ActiveSpeed/PassiveSpeed/CurrentSpeed: seconds per AI decision (combat decisions, target acquisition, spell timing).
  • Move clock — ActiveMoveSpeed/PassiveMoveSpeed/CurrentMoveSpeed: seconds per step. Inherits the matching think value until overridden, so a creature configured with only think speeds behaves as one clock. The properties read the raw override (0 = inheriting); CurrentMoveSpeed is the resolved pace. Any value is legal — steps are scheduled independently of think ticks, so the two need not divide evenly.

Speeds normally come from Distribution/Data/npc-speeds.json (via SpeedClass or type lists); activeMove/passiveMove are optional per bucket. Prefer data over code:

public override SpeedLevel SpeedClass => SpeedLevel.Slow;  // bucket in npc-speeds.json

Code-level overrides for special cases:

SetSpeed(0.5, 2.0);          // think clock; ALSO clears move overrides (one-clock legacy semantics)
SetMoveSpeed(0.45, 0.9);     // move clock only — call after SetSpeed if both are wanted
ClearMoveSpeed();            // back to inheriting the think clock

All four are [props-tunable per instance (move values: set 0 to re-inherit); per-instance move overrides serialize. Being badly hurt slows steps, never decisions (RunUO parity).

Two conditions cap the resolved step pace without touching either clock, so nothing is stored and nothing needs undoing when the condition ends:

  • Herding — a creature with a TargetLocation is driven at a fixed HerdingMoveSpeed.
  • Pacing to the master — a pet following its master, or guarding from outside guard range, is capped at FollowMoveSpeed (AOS 0.1, earlier eras 0; RunUO's pet sprint). It is a cap, not an override: a creature configured faster keeps its own pace, and its ActiveMoveSpeed/PassiveMoveSpeed are left untouched. Override the virtual to change the pace or to enable it in an era that has it off. Decisions are unaffected — a following pet thinks on its active clock.

The client's Running bit is derived from the step pace, never passed by callers (BaseAI.ShouldRun, stamped in DoMoveImpl): a step shorter than the client's walk interpolation — 400 ms on foot, 200 ms mounted/flying (Movement.WalkFootDelay / WalkMountDelay) — is flagged as a run, or the client falls behind and snaps. An isolated step (resuming after at least a walk interval standing) goes out as a walk regardless of pace — the client renders each step alone, so a run-flagged single step darts — unless the pace beats the run interpolation (a true sprinter), where a walk-rendered first step would flood the client's step queue. Movement APIs (MoveTo, WalkMobileRange, ApproachTarget, MoveToPoint) take no run argument; to make a creature run, make it fast. Creatures step at most once per CurrentMoveSpeed period, paced from the step just taken — a stall never banks catch-up steps, so a resumed chase restarts at full pace.

Target Acquisition: the reaction-time gradient

Acquisition is event-driven, not polled. The periodic scan (AcquireFocusMob) is gated by ReacquireDelay (10 s default) and every scan re-arms it in full, success or failure — it is target stickiness plus the fallback for what movement cannot signal (reveals, doors, summons). Reaction time comes from BaseCreature.OnMovement: an enemy moving inside AcquireOnApproachRange (10 — on-screen; the periodic scan keeps the wider RangePerception) clamps the next scan to at most AcquireOnApproachDelay — the intelligence gradient. TimeSpan.Zero (paragons) also prods the AI, so the ranked scan engages within a timer-wheel turn; the 2 s default reads as "took a beat to notice you"; larger is dumber; a creature that overrides the delay above ReacquireDelay is effectively oblivious to approach. Repeated steps cannot shorten the clamp, so an armed creature scans once per delay period, not once per step or think. ReacquireOnMovement remains the broader hook (any mover, no enemy check, scan next think). The gate self-heals: a deadline further out than ReacquireDelay is illegal and reads as open, so no wedged or wrapped value can silence acquisition beyond one delay period.

OnThink: the excess-call contract

OnThink() is a scheduler pass, not an action. The AI timer calls it at least at the think cadence (CurrentSpeed), but it can and does fire more often: a player command wakes the AI immediately (AITimer.Prod()), a speed-up reschedules the pending wake, and players run command macros that drive extra thinks deliberately (order spam is spam-safe by design — reaction, never action). RunUO had the same property (its timer restarted with a random delay on every speed change), so this has never been a fixed-rate callback.

Every OnThink override must be excess-call tolerant. An extra call must never grant an extra action:

  • Gate consequential work on its own deadline field, compared in subtraction form (Core.TickCount - _nextX >= 0 — see tick-counts.md), or make it idempotent.
  • Never pace a consequential action with a bare per-call Utility.RandomDouble() roll — its frequency then scales with think rate, which players can influence. Per-call rolls are acceptable only for pure cosmetics (idle animations, flavor sounds).
  • The engine already gates the expensive things: steps (the NextMove budget), weapon swings, spell casts, detect-hidden, and the base BaseCreature.OnThink actions (heal, rummage, aura) all carry their own clocks. Follow that pattern.
private long _nextSpecial;

public override void OnThink()
{
    base.OnThink();

    if (Core.TickCount - _nextSpecial >= 0)
    {
        DoSpecial();
        _nextSpecial = Core.TickCount + 5000; // the real rate limit lives here
    }
}

MonsterAbility: same contract

MonsterAbility.CanTrigger is sampled once per think for Think- and CombatAction-triggered abilities, so abilities live under the same rule:

  • MinTriggerCooldown/MaxTriggerCooldown is the real rate limit — the floor holds no matter how often thinks fire. Always give a triggered ability a real cooldown.
  • ChanceToTrigger is a per-sample roll: above the cooldown floor, the expected trigger delay shrinks as think rate rises. Treat the chance as flavor jitter, never as the rate limiter, and keep cooldowns long relative to the think interval so the jitter stays negligible (fire breath — chance 0.5, cooldown 30–45s — varies under 1% between natural and spammed think rates).
  • A zero-cooldown ability records no cooldown at all and triggers on every sampled think that passes its chance — only ever correct for passive alteration hooks, never for Think/CombatAction triggers.
  • An ability that breaks pet orders (fear-style effects) must own its duration explicitly (a hold state, or a "refuses orders until" deadline checked in the order handlers) — pets react to re-issued commands immediately, so think latency is not a hold.

New Spell

Targeted Damage Spell (Magery)

using System;
using Server.Targeting;

namespace Server.Spells.Third;

public class FireballSpellCustom : MagerySpell, ITargetingSpell<Mobile>
{
    private static readonly SpellInfo _info = new(
        "Fireball",             // Name
        "Vas Flam",             // Mantra
        212,                    // Cast animation
        9041,                   // Cast sound
        Reagent.BlackPearl      // Reagents (comma-separated)
    );

    public FireballSpellCustom(Mobile caster, Item scroll = null) : base(caster, scroll, _info) { }

    public override SpellCircle Circle => SpellCircle.Third;
    public override bool DelayedDamage => true;

    public void Target(Mobile m)
    {
        if (CheckHSequence(m))  // Harmful spell sequence check
        {
            var source = Caster;
            SpellHelper.Turn(source, m);
            SpellHelper.CheckReflect((int)Circle, ref source, ref m);

            double damage;
            if (Core.AOS)
            {
                damage = GetNewAosDamage(19, 1, 5, m);
            }
            else
            {
                damage = Utility.Random(10, 7);
                if (CheckResisted(m))
                {
                    damage *= 0.75;
                    m.SendLocalizedMessage(501783);  // You resist
                }
                damage *= GetDamageScalar(m);
            }

            source.MovingParticles(m, 0x36D4, 7, 0, false, true, 9502, 4019, 0x160);
            source.PlaySound(0x15E);

            // Damage types must sum to 100
            SpellHelper.Damage(this, m, damage, 0, 100, 0, 0, 0);
            //                                    phys fire cold pois energy
        }
    }

    public override void OnCast()
    {
        Caster.Target = new SpellTarget<Mobile>(this, TargetFlags.Harmful);
    }
}

Spell Helper Methods

SpellHelper.Turn(caster, target);           // Face target
SpellHelper.CheckReflect(circle, ref source, ref target);  // Magic reflect
SpellHelper.Damage(spell, target, damage, phys, fire, cold, poison, energy);
SpellHelper.AddStatCurse(caster, target, stat);
SpellHelper.AddStatBonus(caster, target, stat);
SpellHelper.CanRevealCaster(spell);

CheckHSequence(target);    // Harmful spell checks (LOS, range, criminal)
CheckBSequence(target);    // Beneficial spell checks
CheckResisted(target);     // Resistance check
GetNewAosDamage(bonus, dice, sides, target);  // AOS damage formula
GetDamageScalar(target);   // Pre-AOS damage multiplier

Spell Circles (Magery)

Circle Mana Base Delay
First 4 0.25s + circle
Second 6 0.50s + circle
Third 9 0.75s + circle
Fourth 11 1.00s + circle
Fifth 14 1.25s + circle
Sixth 20 1.50s + circle
Seventh 40 1.75s + circle
Eighth 50 2.00s + circle

Skill Implementation

Registering a Skill Handler

namespace Server.SkillHandlers;

public static class MySkillHandler
{
    public static void Initialize()
    {
        SkillInfo.Table[(int)SkillName.Tracking].Callback = OnUse;
    }

    public static TimeSpan OnUse(Mobile from)
    {
        from.SendMessage("You begin tracking...");
        from.Target = new TrackingTarget();
        return TimeSpan.FromSeconds(10.0);  // Cooldown
    }
}

Skill Check

// Difficulty-based check (with skill gain chance)
if (from.CheckSkill(SkillName.Mining, 0.0, 100.0))
{
    // Success
}

// Direct chance check
if (from.CheckSkill(SkillName.Hiding, minSkill: 25.0, maxSkill: 75.0))
{
    // Success
}

SkillName Enum (58 skills)

Key skills: Alchemy, Anatomy, AnimalLore, AnimalTaming, Archery, ArmsLore, Begging, Blacksmith, Bushido, Camping, Carpentry, Cartography, Chivalry, Cooking, DetectHidden, Discordance, EvalInt, Fencing, Fishing, Fletching, Focus, Forensics, Healing, Herding, Hiding, Inscribe, ItemID, Lockpicking, Lumberjacking, Macing, Magery, MagicResist, Meditation, Mining, Musicianship, Necromancy, Ninjitsu, Parry, Peacemaking, Poisoning, Provocation, RemoveTrap, Snooping, Spellweaving, SpiritSpeak, Stealing, Stealth, Swords, Tactics, Tailoring, TasteID, Tinkering, Tracking, Veterinary, Wrestling


Loot System

Using Predefined Packs

public override void GenerateLoot()
{
    AddLoot(LootPack.Poor);        // ~50g equivalent
    AddLoot(LootPack.Meager);      // ~100g equivalent
    AddLoot(LootPack.Average);     // ~250g equivalent
    AddLoot(LootPack.Rich);        // ~500g equivalent
    AddLoot(LootPack.FilthyRich);  // ~1000g equivalent
    AddLoot(LootPack.UltraRich);   // ~2000g equivalent
    AddLoot(LootPack.SuperBoss);   // Boss-level loot

    // Auxiliary packs
    AddLoot(LootPack.Gems, 2);     // 2 random gems
    AddLoot(LootPack.Potions);     // Random potion
    AddLoot(LootPack.LowScrolls);  // Low circle scroll
    AddLoot(LootPack.MedScrolls);  // Med circle scroll
    AddLoot(LootPack.HighScrolls); // High circle scroll
}

Packs auto-select era-appropriate loot (Pre-AOS, AOS, SE variants).

Specific Items

PackItem(new Arrow(Utility.RandomMinMax(20, 40)));
PackGold(100, 200);
PackItem(new Bandage(Utility.RandomMinMax(5, 10)));

Context Menus

public override void GetContextMenuEntries(Mobile from, ref PooledRefList<ContextMenuEntry> list)
{
    base.GetContextMenuEntries(from, ref list);

    if (from.Alive && from.InRange(this, 2))
    {
        list.Add(new RepairEntry(this));
    }
}

private class RepairEntry : ContextMenuEntry
{
    private readonly Item _item;

    public RepairEntry(Item item) : base(6100)  // Cliloc number
    {
        _item = item;
        Enabled = item is { Deleted: false };
    }

    public override void OnClick(Mobile from, IEntity target)
    {
        if (_item.Deleted || !from.InRange(_item, 2))
            return;

        from.SendMessage("You repair the item.");
    }
}

Entity Lifecycle

Two-Phase Deletion

// Phase 1: Pre-removal cleanup
public override void OnDelete()
{
    _timerToken.Cancel();       // Cancel managed timers
    // Remove from tracking systems
    base.OnDelete();
}

// Phase 2: Post-removal cleanup
public override void OnAfterDelete()
{
    _timer?.Stop();             // Stop Timer references
    _timer = null;
    _owner = null;              // Null Item/Mobile refs
    base.OnAfterDelete();
}

OnDoubleClick Validation

public override void OnDoubleClick(Mobile from)
{
    if (!IsChildOf(from.Backpack))
    {
        from.SendLocalizedMessage(1042001);  // Must be in backpack
        return;
    }

    if (!from.InRange(GetWorldLocation(), 2))
    {
        from.SendLocalizedMessage(500446);  // Too far away
        return;
    }

    // Item logic here
}

File Organization

Projects/UOContent/
├── Items/
│   ├── Weapons/Swords/       # Swords
│   ├── Weapons/Maces/        # Maces
│   ├── Weapons/Ranged/       # Bows, crossbows
│   ├── Armor/Plate/          # Plate armor
│   ├── Armor/Chain/          # Chain armor
│   ├── Armor/Leather/        # Leather armor
│   ├── Clothing/             # Wearable clothing
│   ├── Containers/           # Bags, boxes
│   ├── Misc/                 # General items
│   ├── Special/              # Unique/quest items
│   └── Resources/            # Crafting materials
├── Mobiles/
│   ├── Animals/Bears/        # Bears (BlackBear, GrizzlyBear)
│   ├── Animals/Birds/        # Birds
│   ├── Monsters/AOS/         # AOS-era monsters
│   ├── Monsters/SE/          # SE-era monsters
│   ├── Monsters/ML/          # ML-era monsters
│   ├── Special/              # Champions, bosses
│   ├── Vendors/              # NPC vendors
│   └── Townfolk/             # NPCs
├── Spells/
│   ├── Base/                 # Spell base classes
│   ├── First/ - Eighth/     # Magery circles
│   ├── Necromancy/           # Necromancer spells
│   ├── Chivalry/             # Paladin spells
│   ├── Bushido/              # Samurai abilities
│   ├── Ninjitsu/             # Ninja abilities
│   └── Spellweaving/         # Spellweaving
├── Skills/                   # Skill handlers
├── Gumps/                    # UI dialogs
│   └── Base/                 # Gump base classes
├── Engines/                  # Complex systems
│   ├── Craft/                # Crafting system
│   ├── CannedEvil/           # Champion spawns
│   ├── Factions/             # Faction system
│   └── Quests/               # Quest system
└── Misc/                     # Miscellaneous
    └── LootPack.cs           # Loot tables

Naming Rules

  • File name = class name
  • One primary class per file
  • Group related items in subdirectories
  • Era-specific content goes in era-named subdirectories