Captain appearance: bodies, armour, colour and scars
How a captain's look is chosen, stored and drawn: the appearance object, the seed that rolls body, size, head and colours, armour designs and the items that carry them, the army's level-based outfits and title looks, painted skins, scars, limbs and shoulder pads, tint layers, eyes and decals on screen, how grunts are dressed, and when a changed look reaches the world and the Army screen.
A captain's look is a small appearance object built from his seed and his class, then dressed by the items his class kit, army, tribe, title, personality and scars give him. The same object feeds the Army screen's copies and, through the character's visual, the body in the world.
The appearance object
The appearance object is a separate allocation at [FM+0x2D8] (save key SMCStorage; see The
Nemesis system). It is built by the SimpleModel builder 0x4C5194, written piece by piece by
SetPieceSlot 0x4C62EC, and copied field for field by 0x2EBA60, which fixes its layout:
| Offset | Type | Meaning |
|---|---|---|
| +0x00 | u32 | body type (2 Slim, 3 Buff, 4 Stocky, 5 Olog; Easterlings use 12) |
| +0x08 / +0x10 | vector | appearance tags, 16-byte entries {tag record, u32 count} |
| +0x20 | record | the base Model (Model/SimpleModel.Base: orc_anim_base, olog_anim_base), which carries the skeleton |
| +0x28 | record[23] | piece slots (Model/SimpleModelPieces, 1,727 records) |
| +0xE0 | record[15] | design cells (Inventory/ArmorData), indexed by armour slot |
| +0x158 / +0x160 | vector | material-override records added by the tattoo and paint rolls |
| +0x170 | record | the head record (Model/SimpleModelHeads, 335 records) |
| +0x178 | u32[17] | the colour palette, ARGB (0xAARRGGBB); alpha 0xFF = set |
A spawned character's visual carries a structure of the same layout at [visual+0xB8] (see
Characters).
The object is 0x1C0 bytes. The FactionMember constructor 0x1BB4C1C creates it through 0x2EB490
(malloc(0x1C0), constructor 0x2EB4B8) and moves it into FM+0x2D8 (0x2EB56C); the heap block header
in front of it reads 0x1CB on 60 of 60 seated captains. Nothing past +0x1C0 belongs to it: the next
heap block starts there (an inventory item instance, vt_0x1FDABF8, in 20 of those 60 cases). The head
category Model/SimpleModelHeads is held in the category-cache slot 0x26FFDC8 (see GameDB), which
0x1C05970, 0x1C0546C and 0x1C0606C read.
Piece slots and armour slots
Every design (Inventory/ArmorData, 1,349 records) names an ArmorSlot. 0x36B700 maps it to a piece
slot and 0x4C63F4 maps a piece slot back to its design cell, so the design cell index is the armour
slot itself:
| ArmorSlot | Designs | Piece slot (offset) | Content |
|---|---|---|---|
| 0 | 99 | 7 (+0x60) | helmet |
| 1 | 454 | 8 (+0x68) | shoulders (and the player's cloaks) |
| 2 | 479 | 9 (+0x70) | chest (and the player's armours) |
| 3 | 54 | 10 (+0x78) | arms |
| 4 | 69 | 11 (+0x80) | pants |
| 5 | 27 | 12 (+0x88) | legs |
| 6 | 38 | 3 (+0x40) | quiver (and scabbards) |
| 7 | 50 | 4 (+0x48) | accessory |
| 8 | 7 | 13 (+0x90) | body LOD |
| 9 | 10 | 15 (+0xA0) | head extra: metal jaw, gold tooth, masks, hoods, hair |
| 10 | 9 | 0 (+0x28) | body (no design cell) |
| 11 | 18 | 1 (+0x30) | head (no design cell) |
| 12 | 35 | none | rings |
The enum EInventoryArmorSlot (0x7D6E9C, 15 values) names them in order: Helmet, Shoulder, Chest, Arms,
Pants, Legs, Accessory1, Accessory2, LowLOD, HeadAccessory1, BodyReplacement, HeadReplacement, Ring,
Accessory3, Accessory4. ArmorSlots 13 and 14 map to piece slots 16 and 17; no shipped design uses them
(the 1,349 designs stop at ArmorSlot 12). The build itself fills slot 2 (hair) and slot 5 (body flair),
which no design reaches.
The 23 piece slots
The names are the engine's own: the stored forms of an appearance for slots 0-15, the slot enum 0x836D18 for slots 16 and 17, and the character node table 0x2A40950 for the five FX slots. "Built on" is the object the model load puts the piece on: 0x4C71D0 sends slots 1, 14 and 15 to the head object when the character has one, and every other slot to the body model.
| Slot | Offset | Name | ArmorSlot | Design cell | Built on | In the save |
|---|---|---|---|---|---|---|
| 0 | +0x28 | BodyModel |
10 | none | body | yes |
| 1 | +0x30 | HeadModel |
11 | none | head | yes |
| 2 | +0x38 | HairModel |
none | none | body | yes |
| 3 | +0x40 | AccessoryModel1 |
6 | 6 | body | yes |
| 4 | +0x48 | AccessoryModel2 |
7 | 7 | body | yes |
| 5 | +0x50 | BodyFlairModel |
none | none | body | yes |
| 6 | +0x58 | BodyScarModel |
none | none | body | yes |
| 7 | +0x60 | HelmetModel |
0 | 0 | body | yes |
| 8 | +0x68 | ArmorShoulder |
1 | 1 | body | yes |
| 9 | +0x70 | ArmorChest |
2 | 2 | body | yes |
| 10 | +0x78 | ArmorArms |
3 | 3 | body | yes |
| 11 | +0x80 | ArmorPants |
4 | 4 | body | yes |
| 12 | +0x88 | ArmorLegs |
5 | 5 | body | yes |
| 13 | +0x90 | LowLOD |
8 | 8 | body | yes |
| 14 | +0x98 | HeadScarModel |
none | none | head | yes |
| 15 | +0xA0 | HeadAccessory1 |
9 | 9 | head | yes |
| 16 | +0xA8 | Accessory3 |
13 | 13 | body | no |
| 17 | +0xB0 | Accessory4 |
14 | 14 | body | no |
| 18-22 | +0xB8 to +0xD8 | FX1 to FX5 |
none | none | not built | no |
The model load 0x4C67B0 builds a fixed list of slots, not a range: 0, 5, 6, 8-13, 3, 4, 16 and 17, and through 0x103978 slots 1, 14, 7, 2 and 15. 0x36B248 walks all 23 but only refreshes pieces that already exist (0x4C6718 returns on an empty model slot), and 0x2EBA60 (copy), 0x369F60 (compare) and 0x36B430 (tints) walk all 23 as well, so a value in any slot is carried along whether or not it is ever built.
What writes the array
Eight routines write it. SetPieceSlot, the copy 0x2EBA60 and the three load routines below carry records; the constructor 0x2EB4B8, the reset 0x2EA718 and 0xD587B0 only zero it. 0xD587B0 does so with inlined stores rather than the accessor, then calls the build, which is how a rebuild empties every slot the build does not fill. The executable backs that list up three ways. Only 6 functions bounds-check a 23-element array: 0x36A018, 0x4C62EC, 0xFC93A0 and 0xFC93C0 on this one, 0x1A9A068 and 0x1AE9F18 on others. No block fill or block copy has a count of 23 cells or of the whole object, which holds two vectors and so cannot be copied raw. And following appearance pointers across 2,984 functions finds no other store to a record slot; that walk reproduces all 48 calls to the write accessor 0xFC93A0, but it does not follow virtual calls.
SetPieceSlot is always called with a known slot: the build 0x4C5194 (0-5 and 7), the item application
through the ArmorSlot map (0x61BBC4, 0x4C61BC; never 2, 5, 6, 14 or 18-22), 0x619C1C for
Inventory/Flair/BodyModelReplace (3 records; BodySlot 0 is slot 1, 1 is slot 0), 0x1C049D4, which
empties slots 7-13, the Firebird import 0x1C04F20 (0, 1, 2, 7), 0x79B700 (0, 1, 2) and 0x103CB8 (1).
The slots the game barely uses
- Slot 5,
BodyFlairModel, comes from theBodyFlairof aModel/SimpleModelentry. 4 of the 207 entries have one:ZhojaandZhoja_Skirt(Zhoja_Flair),GraftEltariel_WraithandGraftEltariel_DarkWraith(GraftEltariel_Wraith_Cloak). No orc or olog entry does. - Slots 6 and 14, the scar slots, come from Shadow of Mordor, which stored
SMC_BodyScarModel,SMC_HeadScarModelandSMC_ScarSetuptogether and filled them fromModel/Scar/Setup(45 records),Model/Scar/Scars(122, such asOrc_Head06_BurnandOrc_Slim_Head04_Eyepatch) andModel/Scar/Types(Wraith,Burn,Pin_Cusion,Sack_Head,Eyepatch,Melee). NoModel/Scarcategory ships here: scars are items underCombat/Scar(see Scars below). The slots stayed in the array, the model load and every stored form, but no run-time route fills them, so only a stored value could. The slot enum still listsBodyScarand skips 14. - Slots 16 and 17,
Accessory3andAccessory4, are the third and fourth accessory slots, which Mordor's table does not have. The engine groups them with slots 3 and 4: 0x1BEA070 removes all four pieces from the model and 0x1BEA544 restores them from the appearance, on the player's animation commandsHIDE_ACCESSORIESandSHOW_ACCESSORIES(0x4CD070). No design uses them and no stored form carries them, so shipped content leaves them empty. - Slots 18-22,
FX1toFX5, also come from Mordor, whose piece table numbers them 13-17 and whose stored fields omit them. No routine fills them or builds a piece there: the model-piece creator 0x4C72B4 is only reached with a known slot up to 17, or with an empty piece from 0x1D641C4. SetPieceSlot with a visual would build one (0x4C6C8C, 0x4C7274), but no caller passes such a slot.
Hiding pieces
Inventory/ModelHide (7 records) hides slots and weapon slots by count. 0x1C68F08 keeps one counter per
slot, 18 of them, and calls 0x1BEA4AC when a slot's visibility changes. Its ModelSlots values use the
slot enum 0x836D18, which has 17 names: Body 0, Head 1, Hair 2, Accessory1 3, Accessory2 4,
BodyFlair 5, BodyScar 6, Helmet 7, ArmorShoulder 8, ArmorChest 9, ArmorArms 10, ArmorPants
11, ArmorLegs 12, LowLOD 13, HeadAccessory1 15, Accessory3 16, Accessory4 17. Meatshield,
Bound_Prisoner and FailSpy_Balcony hide Accessory1. Blinded_Dissolve lists 15 of the names in
alphabetical order, all but Accessory3 and Accessory4, so those two stay visible through it.
The character node table
0x47888D0 fills a static table at 0x2A40950: 23 rows of 0x30 bytes, each with a name, readers for the
<name>_Visible and <name>_AnimatedShape properties, two accessors and the Visible key. The rows
run Body, Head, Hair, Accessory1, Accessory2, BodyFlair, BodyScar, Helmet, Shoulder, Chest, Arms, Pants,
Legs, LowLOD, Accessory3, Accessory4, HeadScar, HeadAccessory1, FX1 to FX5. 0x1D641C4 and 0x1D67D20
index a model directly by row, so rows 14-17 do not line up with slots 14-17: Accessory3_Visible acts
on slot 14 and HeadScar_Visible on slot 16. Rows 18-22 are what ties FX1 to FX5 to slots 18-22.
Live inspection of a running game: slots 6, 14 and 16-22 were empty on all 527 captains (slots 8 and 9 held a piece on 514) and on 65 spawned characters, and no model of 66 spawned characters had a piece in slots 18-22. So none of these slots holds anything in normal play. They are not inert: a record written to slot 5, 6, 16 or 17 of a captain's appearance is built on his spawned character alongside slot 4.
Stored forms
An appearance is stored in three forms, and all three stop at slot 15, so slots 16-22 do not survive a save and reload.
- The save. The pool serializer 0x59460C writes each member through 0x594788, which hands the
appearance to 0x36BA30:
+0x20,+0x170, slots 0-15, design cells 0-5 and 8, the 17 colours and the material-override list. 0x1C0606C reads the same fields back. Design cells 6, 7 and 9-14 are not stored. - The document form, keys
smc_body_piece_record,smc_bodyfair_piece_record,smc_bodyscar_piece_record,smc_headscar_piece_record,smc_accessory1_piece_recordand so on: written by 0x1C06810, read by 0x1C05970. - The property form, the names in the table above: written by 0x1C06444, read by 0x1C0546C.
SetPieceSlot(appearance, slot, piece, design, visual) returns early when the slot already holds
that piece; otherwise it removes the old piece's appearance tags (0x4C64EC), adds the new piece's
(0x4C647C), writes the design into its cell, and, when a visual is passed, swaps the piece on that
visual as well.
Building a look
The build sequence
Every member build runs the same sequence:
- Parameters. 0x577190 clears a parameter block and 0x1BEA190 fills it from the member:
+0x00the seedFM+0x168,+0x250a size fraction (below),+0x210the class modelFM+0x278,+0x228personality,+0x238role,+0x240row (a story character'sDisplayFactionRowwhen he has none), the active traits, and the class kit's appearance-tag setup from the combat treeFM+0x188(0x576FFC). - The SimpleModel. 0x4C5194(appearance, params) takes
Character/Models.SimpleModelof the class, adds the class kit's appearance tags (0x6FEA04, 0x1C06064), then rolls body, head, helmet, hair, colours, accessories and flair (below). - Items. 0x1C10F00 empties the inventory (0x1C7BF4: every slot, the colour list and the
look-record list), then 0x1C10AB0 gives, in order, the personality's
Items, the role'sItemlist, the role'sScarTypeitems, the tribe'sItemlist and the class kit'sItemsFactionMember, then the kit's named list for a follower (Named_PlayerFollowerItems) or an enemy (Named_SauronFollowerItems), chosen by flag bit 27. - The army outfit. 0x61BA74 adds pieces of the army's outfit by level and rank (below).
- Apply. 0x61BAD4 writes every item's look into the appearance object.
The builder build 0x1BBF928, the preset build 0x1BC06C0 and 0x1BC00C8 run these steps inline (the
builder build reaches step 4 when it sets the member's level). RebuildDirect 0x1BC3AA8(fm) is the
whole sequence on an existing member; its only caller is
0x1C15B0C, reached only from the console command Faction.RebuildSMC 0x1C5BDA8, which rebuilds every
member of the current region's army ([FS+0x1B8]) and then refreshes that army's Army-screen data.
The seed
Every roll of the look derives from the 64-bit seed FM+0x168:
Lcg(s) = s * 0xBB38435 + 0x3619636B
Unit(s) = (u32)(s >> 32) / 4294967295.0
The size fraction is Unit(Lcg(seed)) (plus 0.0). 0x4C5194 copies the seed once and passes that copy
to the body roll 0x6A9F38, the head pick 0x2EDBB4, the helmet pick 0x701A98, the hair pick 0x6C8BB4,
the colour roll 0x2E9A2C and the accessory picks 0x6F4CA4 / 0x6F5614, in that order; every draw
advances it. A class whose body table draws a different number of times therefore also changes the
face, hair and colours that follow.
Item builds start again from the seed: the give path passes FM+0x168 to the item build, which draws
from a local copy, so a member draws the same design from the same builder at every build. Colour
items (FlairColors) are rolled from the seed the same way. The army outfit's choice of slots is the
exception: it draws from the engine's global generator (0x1B3C6C).
The weighted pick used throughout is the reservoir 0x1B3B90: a weight of zero or less never draws;
otherwise s = Lcg(s), r = Unit(s) * (acc + w), the entry wins when w > r, and acc += w.
Body type
0x6A9F38 walks Model/SimpleModel.Models (_StructuresModelSimpleModelEntry). For each entry, the
weight is the BodyWeights[j] of the last RequiredScaleRange[j] (a float pair) that contains the
size fraction; an entry with no matching range or no Body.Model is skipped. The chosen entry's
BodyType goes to +0x00 and its Body to piece slot 0. The orc classes' tables:
| Class | Buff (3) | Slim (2) | Stocky (4) |
|---|---|---|---|
Orc_Infantry |
1 over 0.3 - 0.7 | 1 over 0 - 0.33 | 1 over 0.67 - 1 |
Orc_Defender |
0.3 over 0 - 0.6 | 0 | 1 over 0 - 1 |
Orc_Berserker |
1 over 0 - 1 | 0 | 0.15 over 0.6 - 1 |
Orc_Spearman |
0.15 over 0.3 - 0.7 | 1 over 0 - 1 | 0 |
Orc_Archer |
0.15 over 0.3 - 0.7 | 1 over 0 - 1 | 0.01 over 0.67 - 1 |
OlogHai_Infantry has one entry (body type 5). A Defender is Buff for about 14% of seeds
(0.6 x 0.3 / 1.3); a Spearman or an Archer is mostly Slim. The bodies are Orc_Buff_Body001,
Orc_Slim_Body001 and Orc_Stk_Body001V2. Changing a member's class therefore re-rolls his body at
the next build, and a non-orc class gives a non-orc body and head.
Size
The in-world size uses the same fraction. 0x7F8488(fm) takes the army he was born into (FM+0x180, InitialFaction, else FM+0x178),
the seed and the class, and 0x4E541C returns
size = min + (max - min) * Unit(Lcg(seed)) + army.ScaleAdditiveModifier
with (min, max) the class's Character/Models.AIScaleRange (Faction_MtnPass adds 0.1). 0x4E3CF0
stores it into the character's scale component (char+0x2738, at +0x10 and +0x6C) only when it
differs by more than 0.01. Because body and size share the fraction, an Infantry captain is Slim at
the small end of his class's size range, Buff in the middle and Stocky at the large end.
Head, hair and helmet
0x2EDBB4 picks a Model/SimpleModelHeads record from the chosen entry's DynamicHead list (the
entry's Head list when that is empty), weighted per head. When the parameter byte +0x254 is set
it considers only heads whose DynamicBundle is available (0x2EDDFC); member builds leave it clear.
The record goes to +0x170 and its Head piece to slot 1; the helmet comes from the record's
Helmets (slot 7) and the hair from its Hair (slot 2, only when SimpleModel.CanApplyHair). Every
orc class lists the same heads per body: 24 Buff, 24 Slim and 23 Stocky, in two head families each.
Unique heads are the 18 designs of armour slot 11. Given by a title or a scar, such a design
replaces the head piece; the head record (the face) and the hair stay. Smp_Empty_Set_NoHair differs
from Smp_Empty_Set only by the tag Tag_I_ARE_BALD, which hides the hair.
The colour roll
0x2E9A2C rolls palette entries from the SimpleModel's colour setups (Model/SimpleModelColorSetup,
465 records, entries _StructuresSimpleModelColorEntry {SelectionWeight, Hue in degrees
+- HueDeltaRange, Saturation / Value minimum and maximum 0-255, AppearanceTag}). The chosen
entry's tag is added to the appearance:
| Palette slot | Rolled from | Slot | Rolled from |
|---|---|---|---|
| 0 skin | SkinTint |
8 bone | ClothingTint2 |
| 1 eye | EyeTint |
9 trim | Trim |
| 2 hair | HairTint |
10 faction metal | FactionMetal |
| 3 tattoo | TattooTint, if a roll passes TattooChance |
11 faction leather | FactionLeather |
| 4 war paint | PaintTint, if a roll passes PaintChance |
12 faction cloth | FactionCloth |
| 5 metal | ArmorTint1 |
13 faction bone | FactionBone |
| 6 leather | ArmorTint2 |
14 faction trim | FactionTrim |
| 7 cloth | ClothingTint1 |
15 faction war paint | items only |
A passed tattoo or paint roll also picks one record of TattooMaterialOverrides /
PaintMaterialOverrides into the list at +0x158. Slot 16 exists in the array but no tint layer
reads it. Shipped orc tables: skin Orc_Infantry_Skin_Color (every orc class) is greenish (hue 63),
fleshtone (32) or bluish (187) at weight 1, with charcoal (Tag_DarkSkin), albino (Tag_WhiteSkin)
and three more at 0.01; eyes Eye_Color red 3 (Tag_RedEyes), yellow 2 (Tag_YellowEyes), blue 1,
green 1; hair brown, grey, black 1, white 0.01. TattooChance is 0 on every class and PaintChance 0
on every class but the Berserker (0.5). Infantry, Defender and Archer share every setup; the
Berserker has its own paint and tattoo, and the Spearman no clothing tint.
Armour
Designs and pieces
A design's ModelPicker is a list of _StructuresInventoryArmorModelPicker {SimpleModelPiece,
RequiredBodyType}. 0x61BC24(design, body type) returns the first entry whose RequiredBodyType is
the member's body type or 0 (any body). A design with no piece for that body type empties the slot.
Sets are named design families AD_<Set>_<Slot>, many with Bling variants (21 *Bling* designs).
A Bling piece is the same model as its plain twin with a different mesh-part table (see Shoulder
pads). The nine tribe sets cover all four bodies, except that Regal has no Olog legs.
A piece record carries Model, Tint (its tint layers), MeshIndexPairs, AppearanceTag,
BaseWeight and ArmorDataDynamicBundle (its geometry bundle). A design also names its material
bundle (DynamicBundle, a link to a DynamicResource/BundleDefinition, for example Black_Base),
an OutOfMemoryBackups design, used when the piece does not fit the gear budget (AD_Black_Shoulders
names AD_Sauron_Shoulders; see Resource streaming, "The gear budget") and follower and
enemy colour overrides.
Items carry the armour
Beyond the SimpleModel's own pieces, a captain wears armour only as items. The member's inventory
inv = FM+0x50 is laid out:
| Offset | Meaning |
|---|---|
| +0x00 + 8 x slot | item instances of the 10 weapon slots (by the look's ItemSlot; see Items) |
| +0x50 + 8 x slot | item instances of the 15 armour slots (by ArmorSlot) |
| +0xC8 / +0xD0 | the colour list: entries {u8 source, u32 palette slot, u32 ARGB} |
| +0xE0 / +0xE8 | the look-record list: Inventory/Items records with a body change or material overrides |
The give path 0x355474(inv, fm, item, source, appearance):
- refuses the item when the member's combat tree's
CharacterTypefails the item'sRequiredCharacterType/ExcludedCharacterType; - for each
FlairColorsentry (Inventory/Flair/Color{ColorType< 17,ColorRecord}) gets or creates the colour-list entry of that slot (0x1C10E54) and, if the source may replace the entry's (rule below), rolls it from the seed (0x1C11A74); - an item with an
ItemBuilderbecomes an instance: the build 0x649790 draws oneWeaponDatalook and then oneArmorDatadesign uniformly, sets+0x48the design and+0x30itsArmorSlot(15 when none), and the source goes to+0x100; 0x61B868 then places it in its slot and adds its item'sAppearanceTags; - an item without a builder is filed in the look-record list (once) when it has a
BodyModelChangeorPassiveMaterialOverrides; - every
PairedItemsentry is given the same way.
PassiveFXSequencePicker is not read on this path.
Which item wins a slot. 0x61C238(new source, old source, new item, old item) decides whether an item may replace the one in a slot (and a colour entry another):
| Old source | Replaced by |
|---|---|
| 0 (empty) | anything |
| 1 / 2 | a source of at least 1 / 2 |
| 3 | a source above 3 |
| 4 - 10 | a source of 4 or more whose item's FactionMemberItemReplaceValue is at least the old item's |
| 11 and above | nothing |
A new source of 11 never replaces, nor does the same item. The sources the build uses are 2 class
kit, 3 army outfit, 4 personality, 5 role, 6 tribe and 10 scar. FactionMemberItemReplaceValue is 0
on 5,003 of 5,618 items; the rest use 1 - 100 (10 on 247, 5 on 137, 2 on 87, 20 on 44) and one 9001.
Applying. 0x61BAD4(inv, appearance) writes each armour-slot instance through 0x61BBC4 (piece for
the body type, then SetPieceSlot with no visual), applies each BodyModelChange
(Inventory/Flair/BodyModelReplace, 3 records: a model picker by body type and a BodySlot, 0x619C1C)
from the slot items and the look-record list, and copies each colour-list entry into the palette.
The faction base layer
Each orc class kit's named lists give a base suit at source 2, the lowest priority: an enemy gets
Sauron_Chest, Sauron_Pants, Sauron_Shoulders, Sauron_Arms and IT_Faction_Enemy_Tint; a
follower Talion_Chest, Talion_Pants, Talion_Shoulders, Talion_Arms, FactionPlayerFollower
and IT_Faction_Friendly_Tint (an Olog Sauron_Pants and Sauron_Helmet, or Talion_Pants;
Archers and Spearmen add their quiver). The kit's ItemsFactionMember gives the body LOD
(IT_Orc_Body_LOD -> AD_Orc_Body_LOD).
The army outfit by level
0x61BA74(fm) decides how many armour pieces a member wears and fills them from an outfit. The count
comes from the army's Faction/Definition.ArmorSetup (Faction/ArmorSetup/Default): 0x61C940 walks
its Entries in order and returns the ArmorPieceCount of the first whose MinLevel is at most the
shown level (FM+0x4B8 + 1) and whose RequiredRowRank list is empty or holds the row's rank:
| Entry | MinLevel | Rank | Pieces |
|---|---|---|---|
| 0 | 1 | Overlord | 6 |
| 1 | 30 | any | 6 |
| 2 | 22 | any | 5 |
| 3 | 16 | any | 4 |
| 4 | 10 | any | 3 |
| 5 | 5 | any | 2 |
| 6 | 2 | any | 1 |
A level-1 member gets none. The outfit is the tribe's ArmorSetOverride, else the role's
ArmorSetOverride (2 of 914 roles), else the class kit's DefaultArmorSet (1 of 93 kits); each is a
Model/SimpleModelArmorSet (17 records) with an item and a weight per slot:
| Tribe | Outfit |
|---|---|
| Dark | Black_Base_Outfit |
| Feral | Beast_Base_Outfit (no arms item) |
| Machine | Industrial_Base_Outfit |
| Marauder | Regal_Base_Outfit |
| Outlaw | Scavenger_Base_Outfit |
| Slaughter | Slaughterhouse_Base_Outfit |
| Sorcerous (and its DLC poison variant) | Mystic_Base_Outfit |
| Terror | Torture_Base_Outfit |
| Warmonger, Bruz | Warmonger_Base_Outfit (no arms item) |
| Militant | Fanatic_Base_Outfit (every item empty) |
Easterling, Gondor, Sauron, ShadowWar, Tribeless, Undead and NazgulTwins have none. Every tribe outfit
weighs chest 5, pants 4, shoulders 3, arms 2, legs 2, helmet 1. The Feral and Warmonger tribes'
own Item lists give Beast_Arms / Warmonger_Arms directly.
0x61BE74 lists the outfit's six slot items as candidates where the slot is empty or the outfit may
replace what is there, judged at the outfit's origin (6 for a tribe outfit, 5 for a role's, 3 for a
kit's); slots held by an item it may not replace count toward the total. It then draws candidates by
weight, one per missing piece, and gives each through the give path at source 3, so a drawn piece
lands only on an empty slot or over the base layer. Besides the build, 0x61BA74 runs on a level change (SetLevel 0x35188C) and from 0x61B614,
which places a member in a row (it reads the row's Rank and EnableHenchmen), so captains gain
armour as they level and rise.
Title looks
A title (Faction/Roles/Definitions, 914) carries no look of its own: its Item list does. 725 roles
list items; 685 list at least one item with an ItemBuilder (armour or weapon), 374 an item with a
look payload, and 6 carry a ScarType. An item builder's ArmorData list holds 1 - 40 designs.
Of 5,618 items, 317 carry a look payload: PassiveMaterialOverrides 210 (decals, glow, hides),
FlairColors 123 (palettes, including about 55 full colourways IT_*_Tint), MaterialSetVariants
71, PassiveFXSequencePicker 21 (auras) and BodyModelChange 3. 199 of them have no builder and
live in the look-record list.
Role_PretendergivesTower_Head(BodyModelChangeTower_HeadReplacement->Orc_Buff_Tower_Head),Tower_Body(its design has a piece for body type 3 only: the four-blade back),Only_LeftShoulder,IT_empty_head_NoHair,IT_AllBlack_Tint, weapons and a quiver. Its predicates admit only a Buff-torso Defender.Role_OfTheSpidersgivesSpiders_Helmet(its ears hidden byorc_Hide_Head_Ears),Spiders_Arms,Spiders_Accessory(whose effect picker carries the aura), a Mystic chest and a body-animation marker item.- 70 of 914 roles empty armour slots with
IT_empty_*items (Role_Machineall five).
A title change through the role setter 0x1BC6698 dresses at once: 0x1BC63F8 writes FM+0x318 (the
old role to +0x320), forces the role's personality when it has one, applies its ScarType through
AddScar and fires its role event; 0x1BC6698 then gives each of the role's items at source 5 and
re-applies the look (0x61BAD4). A plain write of FM+0x318 gives no items until the next build.
Body model, painted skin and body look
Three different things change "the body":
- The body model is the mesh in piece slot 0 and its LOD in slot 13. The base model (
+0x20) does not change with it; 16 of the 70 body pieces are built fororc_anim_base. Body designs are the nine of armour slot 10 (AD_Body_Scar,AD_Machine_Body,AD_Glutton_Body,AD_Infested_Body,AD_Tinder_Body,AD_Flint_Body,AD_Dragonborn_Body,AD_BodyModel_Forced_Buff...). - A painted skin is a texture on the same mesh: a builderless item
<Name>_BodyPaintwhoseMaterialSetVariantsswap the body piece's material set.Model/SimpleModelMaterialSetVariant(58 records) holds six body families (Digger, BlackBlade, Gravewalker, Unashamed, Rogue, Slave), each a variant for Buff and Slim (excludingTag_OlogandTag_Stk), a_Stkone and a_LODone; Slave has no Stocky variant.Orc_Body_Diggerexists only as a variant, never as a mesh.Digger_BodyPaintalso carriesBodyTintWhitePaint_Slave,Orc_Warpaint_LightBrownand excludes Ologs;<Name>_HeadPaintrepaints only the generic heads. - A body look such as the curse veins is decals on the body's scar material (below).
Scars, limbs and shoulder pads
Scars
AddScar 0x7BB8C4(fm, Combat/Scar/Type):
- returns when
FM+0x370already holds the type; - refuses when the member is in the world and holds any of the type's
ExcludedItems, or when 0xFCB040 finds one in his inventory; - takes the setup from his combat tree's
ScarSetup(Combat/Scar/Setup: Orc 22 types, Olog 18; 24 types in all) and gives one random entry of the type's scar items (0x7BBABC) and its helmet item (0x7BBAF4), both at source 10; - if either was given, writes
FM+0x370, re-applies the look (0x61BAD4) and counts the use on the faction system (0x7BBA44).
RemoveScar 0x1BC409C removes one random item of the type's list, so it can miss on a type with
several. The build gives only the role's ScarType; nothing in it reads FM+0x370, and its item clear
removes the earlier scar items, so a rebuild drops a scar given by AddScar. The scars a death or a
return brings are in The Nemesis system.
By their items: Poison, Branded_Slave and Mauled (and the Olog Poison) bare the head (an
AD_empty_head* item in armour slot 0); Metal_Head and Sack_Head replace the head (armour slot 11)
and MetalJaw_Helmet uses slot 9; ExposedBrain, DaggerHead and AxeHead are helmets; burns, cuts, eye
wounds, Facepeele and Shamed are decals only; claws use the accessory slot and the curse the body.
Limbs
Six designs replace a limb: AD_L_Arm_Claw, AD_BigClawL_Accessory, AD_R_Leg_Claw,
AD_NazgulArm, AD_Blight_Arms (accessory slot) and AD_Zog_Legs (legs). The orc's own limb is
hidden by the item's PassiveMaterialOverrides, not by the piece: an Inventory/MaterialOverride
names a model piece (0 body, 8 shoulders, 10 arms, 12 legs, 13 body LOD) and a SeverMask (1 left
lower leg, 2 right lower leg, 4 lower body, 8 left forearm, 16 left upper arm, 32 right forearm, 64
right upper arm). The claw and peg-leg items hide only the body skin; the game ships armour hides that
no item uses (orc_Hide_Armor_Arml_L, piece 10 mask 8; orc_Hide_Armor_Legl_R, piece 12 mask 2).
Two overrides on the same piece and key do not combine (the last bound wins).
NazgulArm (orcs only) puts Orc_<body>_NazgulArm in the accessory slot: a pale veined arm with its
own 512 x 512 atlas and armour tint layers, and three hides (orc_Hide_Body_Armu_R mask 96,
orc_Hide_Armor_Arml_R 32, orc_Hide_Armor_Armu_R) that cut the orc's own right arm away. It comes
from Role_Accursed, Role_Hand and Role_StormBringer.
Shoulder pads and appearance tags
A shoulder piece's MeshIndexPairs (_StructuresSimpleModelMeshIndexPair {MinIndex, MaxIndex,
RequiredAppearanceTag[], ExcludedAppearanceTag[]}, 4,447 in the database) are ranges of the
model's mesh parts. 0x1B37F0 keeps the pairs whose required tags are all present in the appearance
and whose excluded tags are absent, and picks one uniformly from a copy of the visual's seed. The
tags are Tag_ShoulderBoth, Left_Shoulder and Right_Shoulder, given by items (Both_Shoulders,
the Only_* items, some chests and scars). A Bling piece maps 2-2 to 1-2, 3-3 to 3-4 and 2-3 to 1-4
(parts 1 and 4 are the plates). 80 of 132 orc and Olog shoulder pieces have no both-sides layout.
Tags are added by SetPieceSlot, by item equip (0x61B868, an item's AppearanceTags), by the class
kit at the start of a build and by colour entries, and removed per piece on a swap; 0x2EA18C adds a
count, 0x6E90F4 subtracts and removes an entry at zero. A build adds the kit's and the colours' tags
again without clearing, so counts grow with each build. The vector is saved
(smc_storage_appearancetags). Tags also select weapon models: _StructuresInventoryItemModel.OverrideModels
(0x3FF344, picked by 0x1BFB1FC when the wearer holds every required tag).
Colour on screen
Tint layers
A piece's Tint is a Model/SimpleModelTint (76 records) that maps the tint mask's R, G, B and A
channels to palette slots; index 16 means unused. The records are named after the slots they read
(74 of 75 named records agree; for example Skin_Eye_Warpaint_Hair = 0, 1, 4, 2 and
Metal_Leather_FactionCloth_Trim = 5, 6, 12, 9).
The writer 0x36B430 walks the visual's 23 pieces: for each piece with a Tint it finds or creates
the tint entry (0x4C3634) and, per channel below 16, 0x2EAF10 -> 0x2EAFF4 writes the layer: when the
palette entry's alpha is 0xFF, amount 1.0 and colour (R, G, B) / 255 (SetParamVector, handle slot
7); otherwise amount 0.0 and no colour record. On the head (slot 1) the tattoo layer is gated by
SimpleModel.TattooApplyToHead and war paint by PaintApplyToHead (true on 182 of 185
SimpleModels). A second writer (0x36B5D8 -> 0x4C3F58 -> 0x78CD3C) applies a design's follower or
enemy colour overrides, rolled in HSV, in an entry keyed by that design.
The tint owner [visual+0xC0] holds the character's SimpleModel at +0x08 and a vector of 0x18-byte
entries at +0x18 / +0x20: {SimpleModelTint, design (0 = base), handle}; an entry whose design is
the slot's design wins over the base entry. Pieces that share a SimpleModelTint share one store.
The handle class and its setters are in Rendering; the store (0x28 bytes) holds its records
{u32 type << 28 | hash, u32 size, data} at +0x00 / +0x08 (type 2 float3 k_c?TintColor, type 5
float k_f?TintAmount), a reference count at +0x1C and a change stamp at +0x20.
The render cache. A piece's render node rebuilds its parameters only when one of its stores' stamps is newer than its last build. Values written into a store in place change nothing on screen until the stamp moves ahead of the counter the render thread keeps in its thread-local block (about 7,900 increments per second).
The shader blends c = lerp(c, diffuse x tint, mask x amount) per channel in linear space, always
from the original diffuse. Pieces with a blank tint mask (flatblack_alpha.tex, for example the
metal-bandage helmet) cannot be tinted. The gear shader also lerps each mask layer's specular sample
toward k_v?SpecularTint (reflectance, specular occlusion, roughness) by mask x k_f?TintAmount x
k_fSpecularTintToggle x k_f?SpecularTintAmount; gear materials declare the vectors (0.6667, 1,
0.5) with amounts 0, and no captain's tint store carries them.
Eyes and glow
A captain's eyes are solid material slots 5 and 6 of his head piece's material object, one material
per eye (see Characters for the slots): eye_orc, _black, _bright, _serpent, _undead,
_cataracts, _cut, _metal and _crossed, eye_cursed and olog_eyes, all resident in a loaded
world; eye_orc_spider ships only in a DLC bundle. The eye shader tints the iris by the G layer (the
eye palette entry) and writes an emissive value
saturate((mask . k_vEmissiveTintChannelMask - range.x) / (range.y - range.x)) x k_fEmissiveScale:
orc heads ship range (0, 2) and scale 0.05 (0.35 bright, 0 cataracts), so a fully masked pixel glows
at half the scale; Olog heads have no eye glow. The head writes an emissive lookup row
(k_nEmissiveLUTIndex, orc heads 7; the brand texture writes k_nBrandLUTIndex); the
EyeGlowLUT_* records name rows Fire 0, Wraith 1, Poison 2, Nazgul 4, Cursed 5.
A title's eye look is an item (58 items, given by 64 titles): MaterialSetVariants swap only the
eyeball (19 variants, all on the head), PassiveMaterialOverrides paint the rest, and FlairColors
set skin and eye colours. An undead captain's glowing eyes are not the eye material but the
I_Am_Undead item's PassiveFXSequencePicker (UndeadFX -> necro_undead_ambient effects with
"Left Eye Star" / "Right Eye Star" particles on the eye bones), started by the item handler 0x1C0E0C
-> 0x79BFF4; see Visual effects.
Material overrides and decals
AddMaterialOverride 0x57AF70(visual, record, n) and RemoveMaterialOverride 0x57D530(visual, record,
n) put an Inventory/MaterialOverride (378 records) on a drawn character. Add does nothing unless the
visual's byte +0xB2 is set and it has a skeleton (+0x90). The list visual+0xC8 holds 0x38-byte
entries {+0x00 decal slot, +0x08 record, +0x10 count, +0x18 handle, +0x20 vector}; the same record
again only raises the count, and a remove at zero unbinds and frees the entry. A new entry's
parameters come from the record (_StructuresInventoryMaterialParam: a parameter picker such as
ScarLayer, ScarPosition, ScarRotation, ScarScale with 16 names each, or a plain name), placed
in the lowest decal slot that no entry of the same piece group sharing that picker uses, and bound at
index 0 of the piece's
material after the tint store, so the last store carrying a key wins. The head and body shaders read
decal slots 0-7.
Body decals need the scar material. Normal bodies draw orc_body001(v2).mat / olog_body001.mat
(body_noscar.fxo, no decal slots). The scar twins (orc_body_scar, orc_body_stk_scar,
olog_body_scar and their LODs, body_lod0/1.fxo) use the same textures and meshes plus eight decal
slots. A decal store holds k_fScarLayerN, k_fScarRotationN, k_fScarTintN (floats) and
k_vScarPositionN, k_vScarScaleN, k_fScarRotationNCosSin (type 11, float2); a decal of scale s
covers a UV square of side 1 / (4s).
The orc body atlas (shared by Slim, Buff and Stocky; engine texture coordinates):
| Region | u | v |
|---|---|---|
| right hand / right arm | 0.00 - 0.15 / 0.17 - 0.49 | 0.10 - 0.44 |
| torso (back / front) | 0.35 - 0.65 | 0.02 - 0.25 / 0.30 - 0.54 |
| left arm / left hand | 0.51 - 0.83 / 0.85 - 1.00 | 0.10 - 0.44 |
| thighs | right 0.03 - 0.33, left 0.67 - 0.97 | 0.41 - 0.66 |
| shins | right 0.03 - 0.33, left 0.67 - 0.97 | 0.71 - 0.97 |
Left and right islands do not overlap. orc_Cursed (the curse) pairs Body_Scar_BodyReplacement,
sets skin Orc_Skin_White, the curse face and black eyes, and four olog_curse_body vein decals on
the torso. Orc_Skin_White and Orc_Skin_Undead are HSV saturation 0, value 255: a white tint that
shows the pale diffuse itself.
Grunts
Grunts are not faction members and get no member build; the dresser 0x4F12E0(visual component, is
follower) dresses them from the class kit ([Combat/Tree].InventorySetup, read at every call). It skips the
player (the combat object's +0x50); for an unnamed character an enemy gets
Grunt_SauronFollowerItems and loses Grunt_PlayerFollowerItems (0x4F163C), a follower the
reverse, so domination re-dresses a grunt. A named character gets the Named_* lists minus every
item with a builder. Its callers include inventory initialisation 0x4F0938 and the follower setter
0x102F0C.
The shipped grunt armour is one item: Sauron_Armor (Talion_Armor for followers) ->
AD_Sauron_Armor (armour slot 4) -> Orc_<body>_Sauron_faction, the strap-and-loincloth outfit, or
Olog_Buff_Skirt_Grunt. The give path 0x1C01E0 -> 0x1C1224 -> 0x11565A4 -> 0x649790 draws one design
per item build exactly as for a captain, and the equip requests the bundles of what it draws.
When a changed look shows
Residency. A piece draws only when its geometry and material bundles are resident; both are
requested by the gear load request of an equipped item (see Resource streaming). A piece no design
lists has no material bundle to request: AD_Machine_Body lists only Orc_Buff_Machine_Body (body
type 0), so the Slim and Stocky Machine bodies, which have their own geometry bundles, have materials
(machine_body_m.bndl) that nothing requests. Easterling pieces' designs list body type 12 only and
name no material bundle; their shared easterling_outfit.mat rides in easterlings_base,
easterling_buff_heavy_arms_g and flair\grunt\desert_grunt_m.
The appearance object and the body. 0x61BAD4 and the build write only the appearance object
(SetPieceSlot with no visual). A drawn body changes through SetPieceSlot with its visual and
through the equip path's load request.
0x62EB14 (often labelled UpdateVisualModel) is an army refresh, not a character refresh: it takes
an army context and that army's summary container ([ctx+0x30]), rebuilds the container's summaries
(0x62BC44, refilling each from its member through 0x62BF54) and asks the world UI to refresh the
army's stage (0x62E30C). The 7 of its 12 call sites examined all pass an army context: the walk over
every army 0x1B5D66C, fort-upgrade removal 0x1B5E39C, 0x1C1B22C, 0x1C1DB10, the promotion events
0x1C1D828 (with a temporary container), the console rebuild, and 0x7A97DC, which first replaces the
army's summary container (0x62AFD4) and is called after the faction system initialises and from
sim-event generation. A member is not a valid first argument: the rebuild's first instructions
(mov rax,[rdx+0xD8] / mov [rcx+0x9F50],rax) store the context's +0xD8 in the container's
+0x9F50 before anything is tested, so a member and his 0x1C0-byte appearance object in place of a
context and a container put a pointer 0x9F50 bytes past the appearance object.
The Army screen
The Army screen draws each captain from his army's summary (0x1A8 bytes, one per member, in the
summary container), not from the member (see User interface for the stage objects). The refill
0x169168(summary, fm) copies the key {FM+0x178, FM+0x174} (+0x88), class +0x138, combat tree
+0x130, role +0x140, personality +0x148, tribe +0x150, rarity +0x178, level, flags, rebuilds
the trait and marker lists, copies the appearance into [summary+0x40] with 0x2EBA60 and the
inventory into [summary+0x30]. Both copies come from the mock member when FM+0x2E0 is set (0x75D2FC
/ 0x75D314), so such a captain shows the older look (see The Nemesis system).
The per-captain redraw is 0x19C395C(world UI [base+0x26FFB60], &key): the army's stage holder
(0x455EDC), the summary (0x1B5C0F8), the stage character (0x1A28FAC) and its rebuild
UI_CWorldUICharacter 0x627F40(object, summary, 0, 0). It is what the game calls after a Training
Order (0x1C51630). It draws from the summary as it is, so a summary that was not refilled redraws the
old look; the open screen also keeps its own summary copies per army (0x16FC1C).
Pieces across species and shipped quirks
- Easterling armour pieces use the orc rig's bone names (the heavy chest 71 of 74 bones; only the
collar bones are missing). Every humanoid piece skeleton carries the same bind pose (Talion's suits
against
orc_base.skel: median 0.2 cm over 82 shared bones, limb rotations 0.00 degrees), so a piece follows any humanoid rig; misfits are surface volume, not bones. Model.Scaleis not a piece property: its only copier 0x56E870 fills the creation parameters of objects spawned from aModelrecord.- Every vertex shader of
gear_lod0.fxo(3) andgear_alphatest_lod0.fxo(15) takesTEXCOORD1;gear_lod1.fxo(7) does not.Orc_Buff_FaceCape_Chestand the Druid legs and pants have no second UV set in their LOD0 mesh while using a LOD0 material, so they vanish in close-up. - 69 armour pieces belong to items no shipped record references (Druid pants and legs, Underclothes,
Leather shoulders, the Buff Defender set, Easterling heavy and plate pieces ...); 15 cut pieces ship
no mesh in any archive (the Buff Defender set, two Celebrimbor rings, a cinematic hair ...). A record count does not see world scripts:
Sauron_Gondor1andVines_Accessoryare given by mission scripts.
Open questions
- How a spawned body picks up a rebuilt appearance object, and whether the engine supports a body
type change on a loaded character; a live comparison of
[FM+0x2D8]with the visual's+0xB8before and after a level-up would settle the first. - How
Rogue_BodyTattoo(no material override, so the give path files nothing) applies the Rogue body, and which items carry the_LODpainted variants. - How the body-animation marker items (
Personality_UniqueBodyAnim_*_IT, no payload) reach the animation, and how theChildItemPickertint items (IT_Faction_Enemy_Tint) are resolved, since the give path reads neither.
Shadow of War (Steam), game version 1.21. An unofficial fan research project, not affiliated with or endorsed by Warner Bros. Games, Monolith Productions or Middle-earth Enterprises. Middle-earth: Shadow of War belongs to its owners.