Mobile templates
Mobile templates are the YAML source of truth for every id
mobile.create_from_template can spawn. They ship under
src/Moongate.Server/Assets/Templates/Mobiles/ (recursively, including the
base/ subfolder), and unlike item templates they support a real
inheritance step (BaseMobile) resolved at load time.
| Concern | Type |
|---|---|
| DTO | MobileTemplate (Moongate.UO.Data.Mobiles.Templates) |
| Nested DTOs | MobileAppearance, MobileEquipmentEntry, MobileVariant (same namespace) |
| Loader | Moongate.Server.Loaders.MobileTemplatesLoader |
| Deserializer | Moongate.Server.Services.Mobiles.MobileTemplateYamlDeserializer |
| Base-merge resolver | Moongate.Server.Services.Mobiles.MobileTemplateBaseResolver |
| Spawn-time factory | Moongate.Server.Services.Mobiles.MobileFactoryService |
This page documents every key the DTO binds. For a walkthrough of spawning
and reading mobiles see the Mobiles guide and the
mobile module reference.
Note
There is no MobileTemplateValidator. Beyond the deserializer's own
null/duplicate-key checks, the loader does not reject a duplicate Id —
the last template registered for a given Id (case-insensitive) silently
wins, both while resolving BaseMobile and in the final template
dictionary. Unknown skill names and unknown equipment layers are also not
load errors: they are resolved lazily at spawn time and simply skipped
with a warning if they don't match (see Skills and
Equipment below).
File shape and BaseMobile resolution
Each YAML file is a top-level list of mobile template objects. The loader
concatenates every *.yaml file under the mobiles directory (case-insensitive
path order) into one flat list, then MobileTemplateBaseResolver resolves
BaseMobile references before anything is registered.
BaseMobile: <id>merges the named template's fields onto the current one — the current (derived) template wins per the per-field rules in the tables below. Bases can themselves have aBaseMobile(multi-level inheritance is resolved recursively).- An unknown
BaseMobileid, or an inheritance cycle, is a load error (InvalidDataException). - The resolved template's
BaseMobileis always cleared tonull— once merged, a template no longer carries a base reference.
Top-level keys
| Key | Type | Required / default | Merge rule when BaseMobile is set |
|---|---|---|---|
Id |
string |
required (no format check) | Not merged — the derived Id is kept. |
Name |
string |
optional, default "" |
Derived value wins if non-empty, else the base's. |
Gender |
MobileTemplateGenderType enum |
optional, default Male |
Derived value wins only if it is not Male; see Gender for the caveat this creates. |
Title |
string |
optional, default "" |
Derived wins if non-empty, else base's. Displayed under the mobile's name (e.g. "the guard"). |
Category |
string |
optional, default "" |
Derived wins if non-empty, else base's. Matched by GetByCategory. |
Description |
string |
optional, default "" |
Derived wins if non-empty, else base's. |
Tags |
List<string> |
optional, default [] |
Concatenated: base tags then derived tags, de-duplicated case-insensitively — not a wholesale replace. |
BaseMobile |
string? |
optional, default null |
The id to merge onto. Always null on the resolved output. |
Strength |
int |
optional, default 50 |
Derived wins only if it differs from 50 — see the same caveat as Gender. |
Dexterity |
int |
optional, default 50 |
Same rule as Strength. |
Intelligence |
int |
optional, default 50 |
Same rule as Strength. |
Skills |
Dictionary<string, int> (case-insensitive keys) |
optional, default {} |
Real union: base entries are copied in first, then derived entries are applied on top — a derived template can add or override individual skills without repeating the whole base set. See Skills. |
Appearance |
MobileAppearance |
optional, default new() |
Merged per-field — see MobileAppearance. |
Equipment |
List<MobileEquipmentEntry> |
optional, default [] |
Wholesale replace: if the derived template declares any equipment at all, the entire base list is discarded (not merged item-by-item). |
Variants |
List<MobileVariant> |
optional, default [] |
Wholesale replace, same rule as Equipment. See Variants. |
LootTableId |
string? |
optional, default null |
Derived value wins if set, else the base's (derived ?? base). |
BrainScript |
string? |
optional, default null |
Derived value wins if set, else the base's (derived ?? base). Reserved — copied onto the spawned mobile's BrainScriptId but no AI dispatcher in this codebase reads it yet. |
Gender
MobileTemplateGenderType has three members: Male (0, the default),
Female (1), Random (2). At spawn time MobileFactoryService resolves it
to an actual gender:
Template Gender |
Spawned gender |
|---|---|
Male |
always male |
Female |
always female |
Random |
coin-flip (50/50) per spawn |
No shipped template currently sets Gender: Random; Female is used (e.g.
archer_guard_female_npc, warrior_guard_female_npc in
Mobiles/guards.yaml). Male doubles as both "explicitly male" and "not
set", which has one consequence worth calling out: because the merge rule
only takes the derived value when it is not Male, a derived template
cannot force a base's Female/Random gender back to Male — omitting
Gender and writing Gender: Male are indistinguishable, and both mean
"inherit the base's gender" whenever BaseMobile is set.
The same caveat applies to Strength/Dexterity/Intelligence: because
50 is both the DTO default and a legitimate explicit value, a derived
template cannot use 50 to override a non-50 base stat — it will always
inherit the base's stat in that case.
Skills
Skills values are skill points in tenths (Swordsmanship: 1200 means
a starting skill of 120.0). Keys are matched against the SkillName enum
(see skill_name) at spawn time, not
load time: MobileFactoryService strips everything but letters from the key
and parses the remainder case-insensitively, so "Detecting Hidden" and
DetectingHidden both resolve. An unresolved key is not a load or spawn
error — it is logged as a warning and that skill is skipped.
MobileAppearance
Nested under Appearance:.
| Key | Type | Required / default | Merge rule |
|---|---|---|---|
Body |
int |
optional, default 0 |
Derived wins only if non-zero, else base's. UO body/graphic id. |
SkinHue |
string? |
optional, default null |
derived ?? base. A hue spec. |
HairStyle |
int |
optional, default 0 |
Derived wins only if non-zero, else base's. 0 = no hair. |
HairHue |
string? |
optional, default null |
derived ?? base. A hue spec. |
FacialHairStyle |
int |
optional, default 0 |
Derived wins only if non-zero, else base's. 0 = no facial hair. |
FacialHairHue |
string? |
optional, default null |
derived ?? base. A hue spec. |
The same variant-level override applies again at spawn time: a
Variants entry's Appearance fields override the template's
per-field, using the identical zero/null-means-fallback rule.
Hue specs
SkinHue, HairHue, FacialHairHue (and MobileEquipmentEntry.Hue) all
share one small grammar, resolved by HueSpec.Resolve at spawn time:
| Written as | Resolves to |
|---|---|
| omitted, blank, or unparsable | 0 |
a plain integer, e.g. '1153' |
that hue, verbatim |
'hue(a:b)', e.g. 'hue(1002:1058)' |
a random integer in [a, b] inclusive, re-rolled on every spawn |
Equipment
Equipment (top-level) and MobileVariant.Equipment are both lists of
MobileEquipmentEntry:
| Key | Type | Required / default | Meaning |
|---|---|---|---|
Item |
string |
optional, default "" |
An item template Id. Created via the item factory when the mobile is spawned (see item templates). |
Layer |
string |
optional, default "" |
Parsed case-insensitively into a LayerType at spawn time. An unresolved layer name is not a load error — it is logged as a warning and that entry is skipped. |
Hue |
string? |
optional, default null |
A hue spec; resolved independently per equipment entry. |
Variants
A template may declare weighted variants — alternates picked once per
spawn — under Variants::
| Key | Type | Required / default | Meaning |
|---|---|---|---|
Name |
string |
optional, default "" |
Descriptive label only; not referenced elsewhere. |
Weight |
int |
optional, default 1 |
Share of the weighted pick among all variants (floored at 1 even if written lower). |
Gender |
MobileTemplateGenderType? |
optional, default null |
Overrides the template's Gender for this variant when set. |
LootTableId |
string? |
optional, default null |
Overrides the template's LootTableId for this variant when set. |
Appearance |
MobileAppearance |
optional, default new() |
Per-field override of the template's Appearance (same zero/null-means-fallback rule as the base merge). |
Equipment |
List<MobileEquipmentEntry> |
optional, default [] |
Wholesale replace of the template's Equipment when non-empty. |
At spawn time MobileFactoryService picks one variant by weight (each
variant's share is Math.Max(1, Weight) out of the sum of all shares), then
overlays its Gender/LootTableId/Appearance/Equipment onto the
template's own. No shipped template currently declares Variants — the
mechanism is fully wired but unused in the current data set.
Full annotated example
base_human_npc (the base) and archer_guard_female_npc (a derived
template that uses it), both from
src/Moongate.Server/Assets/Templates/Mobiles/:
# src/Moongate.Server/Assets/Templates/Mobiles/base/human.yaml
- Id: base_human_npc
Title: a human
Category: npc
Description: Base human NPC template for derived vendor/citizen profiles.
Tags:
- npc
- human
- base
Strength: 50 # the DTO default; the derived template below overrides all three
Dexterity: 50 # since its own Strength/Dexterity/Intelligence differ from 50
Intelligence: 50
Appearance:
Body: 400
SkinHue: 'hue(1002:1058)'
HairStyle: 8251
HairHue: 'hue(1102:1149)'
Equipment:
- Item: shirt
Layer: Shirt
- Item: long_pants
Layer: Pants
- Item: shoes
Layer: Shoes
# src/Moongate.Server/Assets/Templates/Mobiles/guards.yaml
- Id: archer_guard_female_npc
Title: the guard
Category: npc
Gender: Female # base has no Gender -> Female wins
Description: Town female archer guard NPC inspired by ModernUO ArcherGuard.
Tags:
- npc
- guard
- archer
- town
- female # concatenated with base's [npc, human, base]
BaseMobile: base_human_npc
Strength: 100 # != 50 -> overrides the base's 50
Dexterity: 125
Intelligence: 25
Skills:
Anatomy: 1200 # 120.0
Tactics: 1200
Archery: 1200
ResistingSpells: 1200
DetectingHidden: 1000
Appearance:
Body: 401 # != 0 -> overrides the base's 400
SkinHue: 'hue(1002:1058)'
HairStyle: 8251
HairHue: 'hue(1102:1149)'
Equipment: # non-empty -> replaces the base's shirt/pants/shoes wholesale
- Item: studded_chest
Layer: InnerTorso
- Item: studded_arms
Layer: Arms
- Item: studded_gloves
Layer: Gloves
- Item: studded_gorget
Layer: Neck
- Item: studded_legs
Layer: Pants
- Item: boots
Layer: Shoes
- Item: skull_cap
Layer: Helm
- Item: bow
Layer: TwoHanded
LootTableId: guard.archer # see the Loot tables page
BrainScript: guard
After resolution, archer_guard_female_npc keeps base_human_npc's
Title/Category/Description fallback shape but ends up with its own
Strength/Dexterity/Intelligence, Body, full Tags union, and a
completely replaced Equipment list — while BaseMobile itself is cleared
on the merged result. guard.archer (its LootTableId) is documented as
the full example on the Loot tables page.