Era & Expansions
Overview
ModernUO supports all Ultima Online expansions from the original release through the most recent era. The target expansion controls game mechanics, damage formulas, loot tables, available features, and which maps are accessible. A single configuration setting determines which era your server emulates, and all era-aware code branches automatically based on that setting.
Expansions
ModernUO defines the following expansions in chronological order:
| Expansion | Enum Value | Core Check | Year | Key Changes |
|---|---|---|---|---|
| None | Expansion.None | -- | 1997 | Original release, no expansion features |
| The Second Age | Expansion.T2A | Core.T2A | 1998 | Lost Lands, new dungeons, stat cap 225 |
| Renaissance | Expansion.UOR | Core.UOR | 2000 | Trammel/Felucca split, power hour |
| Third Dawn | Expansion.UOTD | Core.UOTD | 2001 | 3D client, Ilshenar, new monsters |
| Lord Blackthorn's Revenge | Expansion.LBR | Core.LBR | 2002 | Pet bonding, new quests |
| Age of Shadows | Expansion.AOS | Core.AOS | 2003 | Item properties, Malas, Paladin/Necromancer |
| Samurai Empire | Expansion.SE | Core.SE | 2004 | Tokuno, Bushido/Ninjitsu, new housing |
| Mondain's Legacy | Expansion.ML | Core.ML | 2005 | Elves, new dungeons, ML artifacts |
| Stygian Abyss | Expansion.SA | Core.SA | 2009 | Gargoyles, Ter Mur, Enhanced Client |
| High Seas | Expansion.HS | Core.HS | 2010 | Ship combat, fishing overhaul |
| Time of Legends | Expansion.TOL | Core.TOL | 2015 | Valley of Eodon, account gold |
| Endless Journey | Expansion.EJ | Core.EJ | 2018 | Free-to-play tier, restrictions on F2P |
Era Checks
The Core class provides boolean properties for each expansion. Each property returns true when the server's configured expansion is equal to or later than that era.
Core.AOS // true if Age of Shadows or later
Core.ML // true if Mondain's Legacy or later
Core.SA // true if Stygian Abyss or later
This means that on a Mondain's Legacy server, Core.AOS, Core.SE, and Core.ML all return true, while Core.SA and later return false.
The AOS Divide
Age of Shadows (AOS) is the most significant dividing line in Ultima Online's history. It fundamentally restructured combat and itemization:
- Five damage types -- Physical, Fire, Cold, Poison, Energy replaced the single damage model.
- Item properties -- Weapons and armor gained randomized magical properties (hit chance, damage increase, resistances).
- Luck system -- Luck stat influences loot quality.
- Insurance -- Players can insure items against loss on death.
- New spell schools -- Chivalry (Paladin) and Necromancy were added.
- Property lists (tooltips) -- Items display their properties on hover.
Because of this, the majority of era-conditional code in the codebase checks Core.AOS. If you are writing mechanics that differ between classic and modern UO, this is almost always the branch point.
Writing Era-Conditional Code
Ternary chains
For simple value selection, chain ternaries from newest to oldest expansion:
var delay = Core.SE ? 250 : Core.AOS ? 500 : 1000;
This reads as: "If SE or later, use 250ms. Otherwise if AOS or later, use 500ms. Otherwise use 1000ms."
If/else branching
For more complex logic like damage formulas, use if/else blocks:
if (Core.AOS)
{
// AOS+ damage: uses item properties, resistances, and 5 damage types
var baseDamage = weapon.MaxDamage;
var bonus = attacker.GetDamageBonus();
damage = ScaleDamage(baseDamage, bonus);
}
else
{
// Pre-AOS damage: simpler formula based on weapon damage and tactics
damage = weapon.MaxDamage;
damage += (int)(attacker.Skills.Tactics.Value * 0.5);
}
Property display branching
Tooltips often show different information depending on the era:
if (Core.ML)
{
list.Add(1060847, $"{"crafted by"}\t{_crafter?.Name}");
}
Era-aware loot
LootPack automatically selects era-appropriate loot tables. Use the built-in properties:
LootPack.Rich // Selects the correct rich loot table for the current era
LootPack.Average // Era-appropriate average loot
Configuration
The server's target expansion is set in expansion.json. This file is generated during first-run setup, but you can edit it manually.
{
"Id": 8,
"Name": "Stygian Abyss",
"ClientFlags": "Felucca,Trammel,Ilshenar,Malas,Tokuno,TerMur",
"MapSelectionFlags": {
"Felucca": true,
"Trammel": true,
"Ilshenar": true,
"Malas": true,
"Tokuno": true,
"TerMur": true
}
}
The Id field corresponds to the expansion's numeric value (0 = None, 1 = T2A, ..., 8 = SA, etc.). The MapSelectionFlags control which maps are available to players.
A companion file, expansions.json, contains the full metadata for all expansions, including supported features, character creation flags, and housing flags. The server uses this as a reference when applying expansion.json.
Best Practices
- Use
Core.XYZproperties -- WriteCore.AOS, notCore.Expansion >= Expansion.AOS. The properties are clearer and less error-prone. - Chain ternaries from newest to oldest --
Core.SE ? x : Core.AOS ? y : zreads naturally and avoids logic bugs. - Test both branches -- When adding era-conditional code, verify behavior on both sides of the branch. A feature that works on AOS but crashes on pre-AOS (or vice versa) is a bug.
- Use era-aware
LootPackproperties -- Do not hardcode loot tables. The built-in properties handle era selection automatically. - Do not assume an era -- If you are unsure which expansion a piece of code should target, ask. The correct branch points depend on the specific mechanic.