Skip to main content

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:

ExpansionEnum ValueCore CheckYearKey Changes
NoneExpansion.None--1997Original release, no expansion features
The Second AgeExpansion.T2ACore.T2A1998Lost Lands, new dungeons, stat cap 225
RenaissanceExpansion.UORCore.UOR2000Trammel/Felucca split, power hour
Third DawnExpansion.UOTDCore.UOTD20013D client, Ilshenar, new monsters
Lord Blackthorn's RevengeExpansion.LBRCore.LBR2002Pet bonding, new quests
Age of ShadowsExpansion.AOSCore.AOS2003Item properties, Malas, Paladin/Necromancer
Samurai EmpireExpansion.SECore.SE2004Tokuno, Bushido/Ninjitsu, new housing
Mondain's LegacyExpansion.MLCore.ML2005Elves, new dungeons, ML artifacts
Stygian AbyssExpansion.SACore.SA2009Gargoyles, Ter Mur, Enhanced Client
High SeasExpansion.HSCore.HS2010Ship combat, fishing overhaul
Time of LegendsExpansion.TOLCore.TOL2015Valley of Eodon, account gold
Endless JourneyExpansion.EJCore.EJ2018Free-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.XYZ properties -- Write Core.AOS, not Core.Expansion >= Expansion.AOS. The properties are clearer and less error-prone.
  • Chain ternaries from newest to oldest -- Core.SE ? x : Core.AOS ? y : z reads 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 LootPack properties -- 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.