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 :#
188 lines
5.7 KiB
Markdown
188 lines
5.7 KiB
Markdown
# ModernUO Era & Expansion System
|
|
|
|
This document covers ModernUO's expansion system, era checks, and how to write era-conditional code.
|
|
|
|
## Overview
|
|
|
|
ModernUO supports all Ultima Online expansions from the original game through Endless Journey. The server's target expansion is configured at startup and affects gameplay mechanics, damage formulas, loot tables, skill systems, and more.
|
|
|
|
## Expansion Enum
|
|
|
|
Defined in `Projects/Server/ExpansionInfo.cs`:
|
|
|
|
```csharp
|
|
public enum Expansion
|
|
{
|
|
None, // 0 - Original UO (pre-T2A)
|
|
T2A, // 1 - The Second Age (October 1998)
|
|
UOR, // 2 - Renaissance (May 2000)
|
|
UOTD, // 3 - Third Dawn (March 2001)
|
|
LBR, // 4 - Lord Blackthorn's Revenge (February 2002)
|
|
AOS, // 5 - Age of Shadows (February 2003)
|
|
SE, // 6 - Samurai Empire (November 2004)
|
|
ML, // 7 - Mondain's Legacy (August 2005)
|
|
SA, // 8 - Stygian Abyss (September 2009)
|
|
HS, // 9 - High Seas (October 2010)
|
|
TOL, // 10 - Time of Legends (October 2015)
|
|
EJ // 11 - Endless Journey (March 2018)
|
|
}
|
|
```
|
|
|
|
## Era Check Properties
|
|
|
|
Each property returns `true` when `Core.Expansion >= that expansion`:
|
|
|
|
```csharp
|
|
Core.Expansion // Exact expansion (Expansion enum value)
|
|
Core.T2A // bool: >= The Second Age
|
|
Core.UOR // bool: >= Renaissance
|
|
Core.UOTD // bool: >= Third Dawn
|
|
Core.LBR // bool: >= Blackthorn's Revenge
|
|
Core.AOS // bool: >= Age of Shadows
|
|
Core.SE // bool: >= Samurai Empire
|
|
Core.ML // bool: >= Mondain's Legacy
|
|
Core.SA // bool: >= Stygian Abyss
|
|
Core.HS // bool: >= High Seas
|
|
Core.TOL // bool: >= Time of Legends
|
|
Core.EJ // bool: >= Endless Journey
|
|
```
|
|
|
|
## Major Era Boundaries
|
|
|
|
### Pre-AOS (None through LBR)
|
|
- Simple damage model: flat random damage
|
|
- Skill-based resistance
|
|
- No item properties system
|
|
- Simple loot tables
|
|
|
|
### AOS (Age of Shadows) -- The Big Divide
|
|
AOS fundamentally changed UO's combat and item systems:
|
|
- **Resistance system**: 5 damage types (Physical, Fire, Cold, Poison, Energy) each with resistances
|
|
- **Item properties**: Magic items with bonus properties (hit chance, damage increase, etc.)
|
|
- **New damage formula**: `GetNewAosDamage()` replaces flat random
|
|
- **Luck system**: Affects magic item quality from loot
|
|
- **Insurance**: Players can insure items against loss
|
|
- **Necromancy**: New spell school
|
|
- **Chivalry**: New spell school (Paladin)
|
|
|
|
### SE (Samurai Empire)
|
|
- **Bushido/Ninjitsu**: Two new skill/spell schools
|
|
- **Adjusted loot packs**: `SePoor`, `SeMeager`, `SeAverage`, etc.
|
|
- **Reduced hit delay**: `Core.SE ? 250 : Core.AOS ? 500 : 1000`
|
|
|
|
### ML (Mondain's Legacy)
|
|
- **Spellweaving**: New magic school
|
|
- **Adjusted skill requirements**: Different min skill values for spells
|
|
- **Container display**: Shows weight limits in tooltips
|
|
- **Stat gain changes**: Faster stat gain option
|
|
|
|
### SA (Stygian Abyss)
|
|
- **Gargoyle race**: New playable race
|
|
- **Mysticism/Throwing**: New skills
|
|
- **Extended mobile status**: Additional stats in status bar
|
|
|
|
### HS (High Seas)
|
|
- **Ship combat**: Naval warfare system
|
|
- **Extended status bar**: More stats visible
|
|
|
|
## Writing Era-Conditional Code
|
|
|
|
### Simple Value Selection
|
|
```csharp
|
|
// Ternary chain (most common pattern)
|
|
var delay = Core.SE ? 250 : Core.AOS ? 500 : 1000;
|
|
var statMax = Core.LBR ? 125 : 100;
|
|
```
|
|
|
|
### Logic Branching
|
|
```csharp
|
|
if (Core.AOS)
|
|
{
|
|
// AOS+ damage formula
|
|
damage = GetNewAosDamage(10, 1, 4, target);
|
|
}
|
|
else
|
|
{
|
|
// Pre-AOS damage formula
|
|
damage = Utility.Random(4, 4);
|
|
if (CheckResisted(target))
|
|
{
|
|
damage *= 0.75;
|
|
target.SendLocalizedMessage(501783);
|
|
}
|
|
damage *= GetDamageScalar(target);
|
|
}
|
|
```
|
|
|
|
### Display Branching
|
|
```csharp
|
|
public override void GetProperties(IPropertyList list)
|
|
{
|
|
base.GetProperties(list);
|
|
|
|
if (Core.ML)
|
|
{
|
|
// ML+ shows full container info
|
|
list.Add(1072241, $"{TotalItems}\t{MaxItems}\t{TotalWeight}\t{MaxWeight}");
|
|
}
|
|
else
|
|
{
|
|
// Pre-ML shows basic info
|
|
list.Add(1050044, $"{TotalItems}\t{TotalWeight}");
|
|
}
|
|
}
|
|
```
|
|
|
|
### Skill Requirements by Era
|
|
```csharp
|
|
// From MagerySpell.cs - spell difficulty varies by era
|
|
private static readonly double[] _requiredSkill = Core.ML
|
|
? new[] { -46.0, -32.0, -18.0, -4.0, 10.0, 24.0, 38.0, 52.0, 66.0, 80.0 }
|
|
: new[] { -50.0, -30.0, 0.0, 10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0 };
|
|
```
|
|
|
|
### Loot by Era
|
|
LootPack properties auto-select era-appropriate variants:
|
|
```csharp
|
|
LootPack.Poor // Selects: OldPoor / AosPoor / SePoor
|
|
LootPack.Meager // Selects: OldMeager / AosMeager / SeMeager
|
|
LootPack.Average // etc.
|
|
LootPack.Rich
|
|
LootPack.FilthyRich
|
|
LootPack.UltraRich
|
|
LootPack.SuperBoss
|
|
```
|
|
|
|
## Configuration
|
|
|
|
Expansion is set in `Distribution/Configuration/expansion.json`:
|
|
```json
|
|
{
|
|
"expansion": "ML"
|
|
}
|
|
```
|
|
|
|
Full expansion metadata is in `Distribution/Data/expansions.json`, containing:
|
|
- Required client version
|
|
- Supported feature flags
|
|
- Character list flags
|
|
- Housing flags
|
|
- Mobile status version
|
|
- Map selection flags
|
|
|
|
## Best Practices
|
|
|
|
1. **Always ask the user** which expansion to target if not specified
|
|
2. **Test both branches** when writing era-conditional code
|
|
3. **Use `Core.XYZ` properties** (not `Core.Expansion >= Expansion.XYZ`)
|
|
4. **Chain ternaries** from newest to oldest for value selection
|
|
5. **Document era requirements** in comments when the logic is complex
|
|
6. **Use era-aware LootPack** properties instead of hardcoding specific era packs
|
|
|
|
## Key File References
|
|
- Expansion enum: `Projects/Server/ExpansionInfo.cs`
|
|
- Core properties: `Projects/Server/Core.cs`
|
|
- Expansion data: `Distribution/Data/expansions.json`
|
|
- Loot packs: `Projects/UOContent/Misc/LootPack.cs`
|
|
- Spell circles: `Projects/UOContent/Spells/Base/MagerySpell.cs`
|
|
- Skill check: `Projects/UOContent/Skills/SkillCheck.cs`
|