The Nemesis system: factions, captains and the army

The faction system and its pools and 38 armies, the FactionMember object and its saved fields, the seat table of every army's rows, the nemesis flag word and its engine setters, faction events, turns and sim events, cheat death and the event history, the ambush queue, rarity and Training Orders, followers, bodyguards and mercenaries, and how story and DLC characters take seats.

Every named orc, olog and story character is a FactionMember owned by the faction system, seated in an army's rows, and changed by faction events that the engine applies to it. The hierarchy, the promotions, the returns from death and the ambushes are data: rows, events and reward lists in the Faction/* categories of the GameDB, driven by a small set of engine functions.

Traits, classes and personalities are in Captain traits, classes and personalities; story characters, betrayal scenes and region ownership in Quests, story and scenes; looks in Captain appearance; what a save keeps in Saves, the store and the network layer.

The faction system

The faction system is FS = [GM+0x6C68] ([[base+0x26FFAA8]+0x6C68]; see Engine core). The scene tick calls 0x1E2BC4 on it every frame (see Threads and the frame); through 0x1E2A58 that also runs the ambush update 0x6DDBA8.

Offset Meaning
+0x30 the ambush manager (below)
+0x68 the pools: one 0xC0-byte entry per Faction/Pool record
+0x70 u32 the pool count (9)
+0x80 / +0x88 army -> pool: 16-byte entries {Faction/Definition record, its pool object}, count 38 (in a loaded campaign: 10 armies use the DLC3 pool, 9 QuestReplay, 8 MainGame, 6 InvasionGame, 2 DLC4, 1 each Player, Demo and QuestReplay_Ratbag2)
+0x98 the army contexts: a map {+0x00 entries, +0x08 u32 count (38), +0x10 the Faction/Definition category}
+0xB0 / +0xB8 role use: 16-byte entries {Faction/Roles/Definitions record, u32 counter, f32 the role's WeightReductionPerUse}, count 914; the counter reached 740 in a late campaign whose turn counter read 712, so it is not the turn of last use
+0xC8 / +0xD0 names in use: 32-byte entries {Faction/NameTable record, vector of u32 name ids (begin, end), u32 capacity, u32}, count 4 (Orc, Olog, Forthog, Easterling); in a loaded campaign 80 of the 80 Orc ids and 29 of the 30 Olog ids were held by current members
+0xE0 the trait flag table (see Captain traits)
+0xE8 u32 the trait flag table's count: 493, one 16-byte entry per Faction/Traits/Picker record
+0xF8 / +0x100 32-byte entries {Faction/Traits/PickerLevelRemap record, vector of u32 pairs, u32 capacity, u32}, count 5 (TraitLevelOffset_0, -1, -2, +1, +2): each record's remap table, decoded
+0x128 / +0x130 scar use: 24-byte entries {Combat/Scar/Type record, the record again, f32 current weight, u32 times given}, count 24; the weight is the record's WeightPerUse list at the use count (Curse, given 44 times: 0.0001, the last of 1000, 10, 1, 0.01, 0.0001)

Each of these per-record tables opens with {entries, u32 count, u32 hash, the category}. | +0x1B0 | the current region (a Regions/Region record) | | +0x1B8 | the current region's army (a Faction/Definition record) | | +0x1F0 | events queued while another is being processed; +0x208 is the busy flag | | +0x218 | u16 the turn counter (below) | | +0x21C | the world's running total of resurrections: incremented with the member's FM+0x500 at every cheat-death mark, and read when the odds table is chosen (see "Cheat death and the return") |

Pools

Members live in pools, one per game mode, not per region: the MainGame pool holds every captain of every region. A pool entry's first qword is its Faction/Pool record; the pool object starts at entry+8 (vt_0x1FD8310) and holds its member vector at +0x18 / +0x20. FM+0x174 is the member's index in that vector (500 of 500 members). The entries follow the Faction/Pool record order (9 of 9: Player, QuestReplay, QuestReplay_Ratbag2, InvasionGame, Test, Demo, DLC3, DLC4, MainGame), so MainGame is entry 8; entry+0xB0 repeats the record and entry+0xB8 holds its MaxMembersToTrack (9 of 9).

Pool MaxMembersToTrack Serialize NemesisCanComeFromThisPool
Player 32 False False
QuestReplay 50 True False
QuestReplay_Ratbag2 75 True False
InvasionGame 250 True False
Test 32 False False
Demo 64 True False
DLC3 450 True False
DLC4 250 True False
MainGame 500 True True

The MainGame record also carries importance coefficients (ImpC_XPLevel 1.5, ImpC_RowRank, ImpC_Role, ImpC_ISTotal 1.0). A new member comes from 0x1BD2E20 (reached through 0x1C23C10), which builds the object with 0x19ED468. When the pool is full it recycles its least important member, and never one with a row (+0x268), a preset (+0x190), a current sim event (+0x4F0), a unique builder instance (+0x350) or flag bit 45. A late campaign's MainGame pool sits at its 500 cap; in one such save the recyclable members were 201 dead former followers and 48 unassigned grunts, and no living follower.

Armies

An army is a Faction/Definition record. 38 ship, and every one has a context in every game mode. The context map at FS+0x98 is indexed by the definition's record index: entry+0 is the record and the context is entry+8 (vt_0x1FDDF78). 0x18DE04(map, record) returns it and asserts "Bad record index" when the record belongs to another category.

Kind Armies Pool
Campaign Faction_MinasIthil, Faction_MtnPass, Faction_island, Faction_volcano, Faction_snowMtn, Faction_desert_Real MainGame
Unused twin Faction_desert (same row records as Faction_desert_Real, FillFactionOnInitialization False) MainGame
Quest replays 10 Faction_<region>_QuestReplay* QuestReplay (QuestReplay_Ratbag2 for _RB2)
Online invasions 6 InvasionFaction_* InvasionGame
Blade of Galadriel 5 Faction_<region>_DLC3, 5 Real_Faction_<region>_DLC3 DLC3
Desolation of Mordor Faction_desert_DLC4, Faction_desert_mercs_DLC4 DLC4
Other Faction_Demovolcano (Demo), Faction_Player (Player, one 32-seat row player_created), Faction_None

A region's army is its Regions/Region.Faction:

Region record Region Army
MinasIthil Minas Ithil Faction_MinasIthil
MtnPass Cirith Ungol Faction_MtnPass
island Nurnen Faction_island
volcano Gorgoroth Faction_volcano
snowMtn Seregost Faction_snowMtn
desert Lithlad Faction_desert_Real

Each region also names an InvasionFaction, and Gorgoroth a DEMOFaction (Faction_Demovolcano). The region desert_mercs names no army in its Faction field; its game-mode override maps it to the mercenary army in the Desolation of Mordor mode, and the region-to-army lookup 0x1E4418 returns null for it in any other mode. Because twins share row records, a row name does not identify the live army: the live army is the one a seated member's FM+0x178 names.

The definition decides how an army is built and run:

Faction/Definition field Meaning
Pool, RequiredSaveSlot the pool it draws from; the save slot (Faction/SaveSlot: MainGame, DLC3, DLC4) it belongs to; army initialisation skips the fill of an army whose slot is not the current one
Rows its rows, in order (an array: 6 in five regions, 5 in Minas Ithil, 3 in the mercenary army)
EventSetup its event switchboard (below)
PresetMembers, QuestOnlyPresetMembers story characters built at army initialisation, and those only quests build
InitFactionOnCreate, FillFactionOnInitialization whether the army is initialised and filled
MustHaveOverlordOnLoad, EmergencyOverlordBuilder the Overlord guarantee and the builder that provides one
InitialMemberIntelLevel, InitialMemberDefaultFollower, InitialMemberIsMercenary flags given to filled members (the mercenary army: 30, True, True)
InterrogationSetup, NameLockedCombatTree intel stages and the combat tree shown for an unknown member (Orc_Unknown)
AdvanceTODCurrentFaction, AdvanceTODStoryActive which armies process a turn (below)
WorldUIFaction the Army-screen stage the army is drawn on (see User interface)
PlayerBodyguardSummonDataSlot0-2 summon data for the player's bodyguards (filled only in Faction_desert_DLC4)
HireCostOption, HireCostVariable the hiring price scalar

The army context

Context offset Meaning
+0x20 the pool object the army draws from and registers members in
+0x30 the army's summary container: one summary per seat (see User interface)
+0x90 / +0x98 the army's sim events
+0xD8 an object used for a row's level (0x1C1BDA8) and to test the army's own presets (0x1C1C638)
+0xF8 / +0x100 the runtime rows (below)
+0x140 pending work, drained by 0x1C1C4E4 on a flushing turn
+0x1E0 staggers the first three events' lifetimes
+0x1ED, +0x218, +0x219 generate at the first tick; regenerate after an event ends
+0x1EE, +0x1F0 dirty bytes set when a seat is vacated
+0x21C the fill latch: the army fills once per initialisation

The hierarchy: rows and seats

An army's Rows are Faction/Rows records named <region>_Row_<rank>. The seat count of a row is its ColumnMax:

Row Rank Seats in five regions Minas Ithil
_Row_Overlord Overlord 1 1
_Row_Inner_Warchief Warchief_Inner 2 (no row)
_Row_Outer_Warchief Warchief_Outer 4 5
_Row_Captain_0 Captain 4 4
_Row_Captain_1 Captain 8 8
_Row_Captain_2 Captain 7 7
total 26 25

The quest-replay, invasion and Blade of Galadriel armies of each region have the same rows (the DLC armies through their own DLC3_<region>_Row_* records), the Desolation of Mordor desert army the same 26 seats, and the mercenary army three captain rows DLC4_desert_Row_Mercs_0/1/2 of 5 seats with HireCost 6000, 3000 and 1500. The 38 definitions name 208 rows in all; the six campaign armies hold 155 seats.

Ranks. Faction/RowRanks has four records:

Rank IsOverlord IsWarchief ImportanceScore CanBePlayerBodyguard
Overlord True False 0 False
Warchief_Inner False True 40 False
Warchief_Outer False True 40 False
Captain False False 0 True

Row records carry the rules of their seats:

Faction/Rows field Meaning
Rank, ColumnMax, ColumnLocks the rank, the seat count, per-column lock entries (two on the inner warchief rows)
GlobalVarPredicate a campaign gate on the row (Overlord rows: OverlordsUnlocked in Nurnen and Cirith Ungol, IntroComplete in Minas Ithil, none in Gorgoroth)
EnableHenchmen, HenchmenRows, HenchmenCount, HenchmenAddChance Overlord and warchief rows bring henchmen (bodyguards) from the region's three captain rows, add chance 0.35
ResurrectionEnabled whether a death in the row can be cheated
InitialLevels, InitBuilderChance, InitBuilderRecords levels and builders used when the row is filled
SimEventGenerationSetup, SimEventGeneration what missions members of the row generate (below)
Locations, LocationsLowPriority, LocationsLastResort where members of the row are placed in the world (55 / 27 / 5 locations on a Nurnen captain row)
RewardListOnEnterRow rewards applied on entering the row (every Overlord row: the Legendary upgrade lists, below; in Nurnen the warchief rows Default_TerrorBodyguardDefeat and the captain rows Default_BloodBrother)
RandomIntelWeight intel weight: 1.0 captains, 0.5 warchiefs, 0.25 the Overlord
PerceptionOverride, DifficultySetupOverride, DeathStimulus the Overlord and warchief rows' perception (Warlord), difficulty and the stimulus their death sends
HireCost the mercenary rows' price
CanBeRevengeTarget, ClearExistingSimEventsOnEnterRow, RemoveBossOnEnter further row rules

Runtime rows. The context's rows at ctx+0xF8 / +0x100 are 0x28 bytes, one per definition row:

Offset Meaning
+0x00 the Faction/Rows record
+0x08 / +0x10 the seats: int32 per column, the occupant's pool index, -1 = empty
+0x20 u32 seat count: the record's ColumnMax (208 of 208 live rows across the 38 armies, with the seats vector the same length)
+0x24 u32 column lock bits (tested by 0x24EBB8)

A member's own seat is FM+0x268 (the row record) plus FM+0x51B (the column; 0xFF = rowless). Several members share one row record because a row has several seats; each seat holds exactly one member. The save stores the row as its index in the army's Rows and the column as a byte.

Moving members.

Dead captains keep their seats. In a late campaign the six regional armies held no empty seat and one to three dead captains each. The sim-event seat predicate 0xFDC350 treats an empty seat, or a dead member without flag bit 4, as free ("Passed: Dead") and refuses a dead member with bit 4 ("Failure: Resurrectable"), then checks the preset, spy (bit 28) and follower (bit 27) states, a blocking captor event and a duel. The jobs that take a dead or empty seat (Quest/Jobs/Definition DeadOrEmptyPosition, _WC, _Overlord) require MustHaveCanResurrect False; 26 of the 849 shipped jobs set that field, all to False. A captain who will return therefore keeps his seat.

Filling. FillFaction 0x1C1E13C(ctx) is army initialisation's fill. Per row and per column whose seat holds -1 and whose lock bit is clear, it creates one new member: from the row's InitBuilderRecords by InitBuilderChance (0x1C1E280 -> 0x1C1CFAC), or by the generic build 0x1C1D100 (allocate 0x1C23C10 -> pool 0x1BD2E20, build 0x1BC0084, register in the pool 0x61B5E8, level 0x1C1BDA8 -> SetLevel, seat 0x61E13C with flag 1, the definition's intel, follower and mercenary flags, henchmen 0x7DEEE0). The builder path calls no SetLevel. The latch ctx+0x21C limits the fill to once per initialisation (0x185FE0).

The FactionMember

The FactionMember (vt_0x1FD5EA0) is 0x528 bytes. Both allocation sites ask for 0x528 bytes (0x19ED468: mov ecx,0x528 -> allocator 0x3DE238 -> constructor 0x1BB4C1C; 0xF99FF0 likewise) and the constructor's last store is +0x520. Everything at +0x530 and above belongs to other allocations: the appearance object is a separate object at [FM+0x2D8] (it sat directly after the member for 0 of 206 row captains), and the traits live in vectors (see Captain traits). The constructor allocates that object itself, right after the member (see Captain appearance), so whether it sits at FM+0x530 depends on the heap's history: in another running game it did for 55 of the first 60 seated captains.

Valid member test: [FM+0x178] is a GameDB record (526 of 526 members pass, 0 of 53 other pointers do). FM+0x174 is not a validity flag: the MainGame and DLC pools both hold a member at index 0.

Member keys. The engine refers to a member by the pair {faction record = FM+0x178, u32 pool index = FM+0x174}, not by pointer: faction events, ambush requests and the boss link all store keys.

Fields

The save keys are the names the FactionMember loader 0x1BC21B4 reads each field under; flags go through the flag setter 0x1156740(fm, bit, value).

Offset Type Save key Meaning
+0x18 node the member's link in its pool's lists
+0x48 ptr the spawned character; null when he is not in the world
+0x50 object Inventory the inventory component (see Items)
+0x150 ptr player_interaction_score the interaction score object
+0x168 u64 random_seed the seed his rolls start from
+0x170 u32 name_string_id_hash the name's string id (below)
+0x174 u32 faction_id index in the pool's member vector
+0x178 record faction_record the army (Faction/Definition); copied to +0x180
+0x188 record combat_tree_record the combat tree (Combat/Tree)
+0x190 record preset_record the Faction/PresetMembers record of a story character
+0x198, +0x1B0, +0x1E0 vectors trait_marker_list, trait_picker_list active traits, markers and trait slots (see Captain traits)
+0x1F8 vector levelchange_reward_list the level-change reward list
+0x240 list relationship_list relationships (blood brothers, hatred)
+0x258 ptr the sim-event cooldown table (below)
+0x268 record faction_row the row (Faction/Rows)
+0x270 record personality_record the personality (see Captain traits)
+0x278 record character_record the class model (Character/Models)
+0x280 record purpose_levelremap_record, trait_picker_level_remap a Faction/Traits/PickerLevelRemap record (TraitLevelOffset_-2 .. _+2)
+0x288 record revengetype_record Faction/RevengeType (Kill, Betrayal, Humiliation)
+0x290 record purpose_record his purpose
+0x298 key 16 bytes {an army record, u32 pool index}: the KILLER's member key, opening the last-death block; the constructor sets the index to -1. In one world, 207 of 207 captains killed by the player held -1; of 62 killed by orcs, 23 resolved to a living member of that army (mostly of the killing character type) and 39 to no current member (the killer since removed)
+0x2A8 record document key LastDeathCharacterType: the Combat/CharacterType that last killed him (Player_Character on 3 of 40, null on 37)
+0x2B0 record document key LastDeathAttack: the Combat/Attack that killed him
+0x2B8 / +0x2B9 u8 / u8 document keys ExitNavMesh, LastDeathPlayerKiller (1 on the same 3 of 40)
+0x2C0 record last_deathtype_record how he last died (Faction/Resurrection)
+0x2C8 key boss_faction_id 16 bytes: a member key {his own army, the boss's pool index}
+0x2D8 ptr SMCStorage the appearance object (see Captain appearance)
+0x2E0 ptr the mock member (below)
+0x300 / +0x308 vector faction_event_list the event history (below)
+0x318 record role_record the role and title (Faction/Roles/Definitions, 914)
+0x328 record tribe_record the tribe (Faction/Tribes/Definitions)
+0x330 record location_record his location
+0x338 record document key LocationArea: an AI/SpawnAmbient/Areas record (26 of 40 seated captains)
+0x348 record resurrect_scartype_record the scar a pending return brings
+0x350 record the builder's UniqueBuilderInstance (null for most members)
+0x370 record resurrect_applied_scartype_record the scar last applied
+0x378 record the first active gang-leader trait (a cache; see Captain traits)
+0x390 record the rarity (below)
+0x398 record a campaign variable set when he dies (story characters)
+0x3A0 record the Faction/FactionMemberBuilder that built him (null for story characters)
+0x468 u32 npc_next_state_id
+0x498 char[32] the name as text (below)
+0x4B8 i32 menace_level the level, 0-based
+0x4BC i32 document key LockedXPLevel; the preset build 0x1BC06C0 copies Faction/PresetMembers.LockedXPLevel here (0 on 39 of 40 seated captains)
+0x4C0 i32 trait_level the level the traits were resolved for (equal to +0x4B8 on 155 of 155)
+0x4C4 i32 player_kills how many times he killed the player
+0x4C8 u32 document key PlayerFleeCount
+0x4D0 object a 0x28-byte object built by 0x40F2A0 (vt_0x1DB4FE0, a list head at its +0x08)
+0x4F0 ptr the sim event he is assigned to
+0x4FC / +0x512 u32 / u16 purpose_string_id, purpose_timeout
+0x500 u32 resurrect_count returns from death
+0x508 u64 (one key per bit) the flag word (below)
+0x510 the intel mask, rebuilt from the intel level
+0x514 / +0x516 u16 encounter_current_index, encounter_previous_index
+0x51A u8 interrogation_level the intel level
+0x51B u8 faction_col the column in his row; 0xFF = rowless
+0x51C u8 set to 1 by a full trait rebuild
+0x51F u8 the moving mark
+0x520 u8 2 for a builder-built member, 3 for a story character

The document form. Besides the binary save, a member is written and read as a key-named document tree: the writer 0x1BC45A8 (its callers include the console commands FactionMember.Save and FactionMember.Save.Pos and presentation code) and the reader 0x1BC159C (used by the member factory 0xF99FF0 and presentation code). The "document key" names in the table come from this pair; it also names +0x278 CharacterModel, +0x2D8 SMCStorage, +0x500 ResurrectionCount and +0x520 InitializationType. The last-death block is written by 0x1BC4FCC and read by 0x1BC1F50 with FM+0x298 as the base: +0x10 LastDeathCharacterType, +0x18 LastDeathAttack, +0x20 ExitNavMesh, +0x21 LastDeathPlayerKiller.

Names

The name is a string id, FM+0x170. 0x1BC6040(fm, id) stores it, looks up the localized UTF-16 string, converts it with WideCharToMultiByte on the system ANSI code page (CodePage 0, Windows-1252 on a Western system) and copies at most 31 characters into the 32-byte buffer FM+0x498; an id of 0 clears the buffer. A builder-built orc gets his name id from the name table of his class map (_StructuresFactionClassDataMap.NameTable; Faction/NameTable has Orc, Olog, Forthog and Easterling), so +0x498 is a derived copy and the id is what the save keeps.

Levels

FM+0x4B8 is stored 0-based; the game shows it plus one, and caps the shown level at 85. SetLevel 0x35188C ends in 0x353C44, which sets flag bits 30 and 31 when the level reaches two thresholds read from the army; crossing the second with no tribe assigns one (0x1BC6718, reason "XPLevel"). Seating re-levels a member for the row (0x1C1BDA8); a story character without a row starts at NoRowXPLevel - 1.

Finding every captain

Pools are by game mode, so the region of a member is his army (FM+0x178) or his row's prefix, never his pool.

The nemesis flag word

FM+0x508 is a u64 of flags. The getter 0x114B55C is a bare bt [fm+0x508], bit; 0x114DFE8 tests bit 2 (dead). The loader's keys name most bits, and the FactionMember document writer 0x1BC45A8 (which writes Identity, Flags, XPLevel ... Inventory) names more: it writes each boolean key from one getter call, IsResurrected bit 0, IsDead 2, KilledByPlayer 3, CanResurrect 4, PresentedToPlayer 5, IdentityPresented 7, HasEverSpawned 9, IgnorePlayerLevelRestriction 10, HasVow 12, SeveredHead 17, IsMockDead 21, CanInterrogateAbout 22, RevengeTarget 24, WasEverPreset 25, Builder_CanNotSave 32, Builder_DisableSimEvents 33, DisableTraitChanges 38, DisableRelationships 39, CanNotDropLoot 40, CanNotChangeXPLevel 41, CanNotApplyContract 42 and CalcXPLevelOnAssignToRow 44, while PlayerFollower, Spy and Prisoner come from functions (0x114E020, 0x1149E50, 0x1BBCAEC) rather than a single bit.

Three bits have no saved key. Bit 37 marks a temporary member: 0x1BBF928 sets it when the builder's byte +0x180 is set, and 0x1BC04E4 sets it on an online arena opponent (__online_arena_opponent__, which also clears the follower bit). A quest's Cleanup_OnEnter (0x7652BC) then calls 0x1B5E0B4, which checks the bit, confirms the member still sits at its pool index, nulls that slot and destroys the member. Bit 35 is set in one place, 0x61CED0, which assigns a member to a sim event: it copies bit 0 of the intel mask FM+0x510 at that moment. In one campaign all 102 members with bit 35 held that mask bit, none of the 75 members without it had bit 35, and 350 members held the mask bit without it. Bit 16 is set by the un-death 0x630644 just before it clears the dead bit, and that function never clears it, but only 1 of the 86 members with a return on record (FM+0x500 above 0) carried it, so it does not mark a returned member. Use FM+0x500 for that.

Bit Name Meaning and writers
0 is_resurrected set by the un-death
2 is_dead SetDead 0x626E38
3 killed_by_player
4 can_resurrect a return from death is pending (below)
5 is_player_presented the player has met him
7 is_menu_presented shown in the hierarchy; set after creation
9 HasEverSpawned
10 IgnorePlayerLevelRestriction ignore the player-level cap (setter 0xF88AA0)
12 has_vow
14, 19, 34, 43 derived from his traits (see Captain traits)
38 DisableTraitChanges also derived from his traits
17 is_severed decapitated; set by the sever-on-death handler 0x6DE30C
21 is_mockdead the mock member is dead: the hierarchy shows him eliminated
22 can_interrogate_about
24 is_revengetarget
25 was_ever_preset
27 is_player_follower setter 0x7D52B0
28 is_spy
29 the player's bodyguard
30, 31 level thresholds (above)
32 Builder_CanNotSave not written to the local save
33 Builder_DisableSimEvents
39 DisableRelationships
40 CanNotDropLoot
41 cant_change_xp_level / CanNotChangeXPLevel
42 CanNotApplyContract
44 CalcXPLevelOnAssignToRow
45 never recycled by a full pool
48-50 the player's bodyguard summon slot 0/1/2, read by the summon 0x1010790
51 mercenary (setter 0xF88AD0, getter 0xF86B90)

The document writer 0x1BC45A8 and reader 0x1BC159C name more bits (getter 0x114B55C and setter 0x1156740 with the bit number next to each key): 9 HasEverSpawned (also set by 0x356370), 10 IgnorePlayerLevelRestriction, 32 Builder_CanNotSave, 33 Builder_DisableSimEvents, 36 Prisoner, 38 DisableTraitChanges, 39 DisableRelationships, 40 CanNotDropLoot, 41 CanNotChangeXPLevel, 42 CanNotApplyContract and 44 CalcXPLevelOnAssignToRow. FM+0x50C is no separate field: it is the word's upper half (the constructor stores +0x508 as one qword; 40 of 40 seated captains agree).

The engine's setters do more than the bit, so a direct write to the word skips their side effects:

The pool serializer 0x59460C writes every member unless 0x595890(fm) (= not bit 32) refuses him; 0x595CA0 is the only other reader of bit 32. Members held by the online garrison carry it.

Faction events

Everything that happens to a member is a Faction/Events/Events record (2,039 ship, each with the same 77 fields) applied by the engine:

  1. System::CreateAndProcessFactionEvent 0x1B5B610(FS, event, subject, other) builds a 0x60-byte descriptor holding the subject's key, the other's key (index -1 when there is none), the event and the other's row index (0x1E4D90) and column, then calls 0x40C0AC(FS, descriptor). The script binding 0x1B7F210 resolves its handles in the current region's pool first.
  2. 0x40C0AC processes the event synchronously; an event raised while another is in progress (FS+0x208) is queued at FS+0x1F0 and drained afterwards (post-processing 0x1E2DA0).
  3. 0x40C13C -> 0x62E670, the dispatcher, picks and records the event's replay (the Army-screen flashback: 0x7421A0 -> 0x741284, [SIMEVENT|REPLAY]), then the apply 0x630644 changes the member.

What the shipped events do (fields set, counted over the 2,039 records):

Field Events Effect
LevelsToAdd 1,736 (1,711 of them 1) levels; LevelsToAddFollower (44) for followers
RewardLists 558 reward lists to roll (below)
ReplayType, Replay 420, 153 the replay shown for the event
KeepPriority 153 how long the history keeps the event (below)
SetMockMemberDead 142 (113 True) mark the mock member dead or alive
ForceLevelUp 82
PickNewSimEvent, PickNewLocation 53, 14 re-plan the member
UnlockName, UnlockTraits 47, 19 intel
SetIsPrisoner, SetPlayerFollower, SetIsSpy 46, 35, 29 links to Combat/Predicates/Bool/True or False
Ambush 32 queue an ambush
ClearCopyFactionMemberData 24 drop the mock member
Resurrect 18 un-death for a member with bit 4
ScarType 18 a scar (Branded_Slave 14, Shamed 3)
EjectIfWarchief, EjectIfOverlord 12, 11 remove him from a warchief or Overlord seat
CreateFactionMemberProfile 11 make a grunt or creature a named member (ActiveKillPlayer_grunt, ActiveKillNamedAI)
SetRevengeTarget, RevengeTargetType 4, 9
ToggleIsPlayerFollower 4 the four arena wins
ChangePlayerBodyguard 1 BecomeBodyguard
PlayerResurrect 1 Resurrected_PlayerAbility_RaiseDead
CopyAndHideFactionMemberData 0

ActiveKillPlayer_grunt, the event of a grunt killing the player, sets CreateFactionMemberProfile, UnlockName, SetRevengeTarget (type Kill), SetMockMemberPresented, KeepPriority 100 and the reward lists KillPlayer_Rewards_grunt and _gruntSlaveBrand.

The apply and following. The apply calls the follower setter only behind SetPlayerFollower (a link, 0x630CB8) and ToggleIsPlayerFollower (a bool, 0x630EF5); an event without them leaves an enemy an enemy.

Rewards. For each reward list the engine draws count = 0x34F980(list) rewards with reward = 0x350500(fm, list) (eligibility 0x105AEC, weights 0x3505F4, a random pick; 0 = none) and applies each with ApplyReward 0x3511F4(fm, reward, other, 1) -> 0x351AF8, which re-resolves every trait slot (see Captain traits). Called directly, ApplyReward skips eligibility.

The switchboard

Which event a game situation fires is per army: Faction/Definition.EventSetup names a Faction/Events/Setup record (seven ship: one per region and Faction_Player_Setup). In Nurnen's:

Setup field Event
KilledByPlayerEvent ActiveKilledByPlayer
KillPlayerEvent, KillPlayerAlreadyNamed, KillPlayerAssistEvent ActiveKillPlayer_grunt, ActiveKillPlayer_AlreadyNamed, ActiveKillPlayerAssist
ResurrectedPresentationEvent Resurrected_Presented
EventSwapFactionPositionsPromotion / PromotionEmpty Transform_SwapPromotionOccupied / Transform_SwapPromotionEmpty
EventSwapFactionPositionsDemotion / DemotionEmpty, Lateral / LateralEmpty the matching Transform_Swap* events
EventSlotOverlord, EventSlotOverlord_NoChange Transform_SlotOverlord, _NoChange
OverlordEjectedEvent, WarchiefEjectedEvent OverlordEjected, WarchiefBrandedEjection
SetPlayerFollowerEvent, ClearPlayerFollowerEvent ActivePlayerFollowerSet, ActivePlayerFollowerClear
EventApplyIsPlayerFollower, EventRemoveIsPlayerFollower IsPlayerFollowerApply, IsPlayerFollowerRemove
TransferIntoFaction, TransferOutOfFaction Transform_HireMercToEmpty, TransferOutOfFaction
DataLevelChangeEvent, GainInterrogationIntelEvent, InterrogatedEvent DatalLevelChanged, ActiveGainInterrogationIntel, ActiveInterrogated
HierarchyPresentedEvent, HenchmenKilledEvent, BossKilledEvent, LoadMemberEvent HierarchyPresented, HenchmenKilled, BossKilled, LoadMember
TimePassedEvent TimePassed (one level)
FleeEvent, FleeRunStartEvent, DespawnWhileFleeingEvent ActiveFlee, FleeRunStart, ActiveDespawnWhileFleeing
TimedSimEvents HalfEmpty, Resurrection, FillUpCompletely_LongDelay, CompletelyEmpty
PromoteEventSetup Faction/Events/PromotionEventSetup/Default (HardMode and _MinasIthil variants exist)

A promotion, a demotion or a lateral move is therefore an event; the seat changes themselves go through the seat function with flag 0 (from the sim-event outcome 0x40C304) and the engine's swap.

Turns and sim events

The missions on the Army screen (power struggles, hunts, executions, fort assaults) are sim events, and they advance in turns, not in real time.

The cooldown table

Each member can hold a cooldown table [FM+0x258]: a 0x18-byte object {entries, u32 count, category} of {record, u32 expiry turn} entries, the category being Faction/Events/SimEventCooldown ([base+0x2701A18]; two records, PowerStruggle_CD and HuntingMission_CD). The gate 0x1BC0C2C(fm, SimEvent.RequiredCooldown) passes when there is no table or the entry has expired, and refuses when the table exists but has no entry (the lookup 0x1BBC410 returns null). The setter 0x1BB7578 creates and sizes a table (0x1BBF840) only when none exists. The FactionMember loader creates the table when the save flags one (0x1BC0F2C) but never sizes it, and reads each saved entry into a temporary (0x1BC0EC4). After a load, every member that had a cooldown is refused power struggles and hunts permanently: in a late-campaign pool of 500, no table was sized and 60 living seated captains carried an empty one. An emulated generation pass on Minas Ithil's army, with every captain free and no events, refused 204 of 221 attempts; every power-struggle and hunt candidate (340 of 340) failed at this gate, the rest at their campaign gates.

Cheat death and the return

Death and return run through the kill path and events:

  1. Mark. The kill path 0x6DB95C (0x1BB9F84 for event kills) stores the death's Faction/Resurrection record at FM+0x2C0 and, when the row has ResurrectionEnabled, rolls the odds (0x6DBD4C: presets refused, row, predicate, odds table). On success the evaluator 0x1BC5D44 picks a fitting scar (0x1B5AA00; none = "No Available Scars", nothing changes), increments FS+0x21C and FM+0x500, sets bit 4, stores the scar at FM+0x348 (unless it equals FM+0x370) and fires the record's DeathEvent through 0x40C0AC. Without ResurrectionEnabled the kill path resets the mock member (0x6DBCFC).
  2. Queue. The death event (WillResurrect_<death>, 23 of them, KeepPriority 30) carries reward lists that queue the ambush: CheatDeath_Ambush for an enemy, CheatDeath_Betrayal or CheatDeath_FollowerStillLoyal for a follower. CheatDeath_Ambush retries forever (RetryMax -1, RetryDelay 0.25 minutes), never for an Overlord, and fires only in its own region; one such request reached 1,121 retries. The return is not a job: no shipped job requires MustHaveCanResurrect True.
  3. Complete. Only the apply 0x630644 un-deaths: when the member has bit 4 and the event has Resurrect, or the event has PlayerResurrect, it clears bits 4 and 6, sets 0 and 16, clears 2 and 17, then runs 0xE7DF70(fm). Resurrected_Presented (the ambush's success event) adds SetMockMemberDead False, ClearCopyFactionMemberData, PickNewSimEvent, one level plus a player-based level change, and EjectIfWarchief / EjectIfOverlord.

Other readers of bit 4: the seat predicate 0xFDC350, the cancel pass 0xF9BA70 (from 0xF665F0 and 0xFA0D30: a dead member with bit 4 that fails 0xFF3D30 loses bits 4, 0 and 16), the sever abort 0x6DE30C ("Aborting Resurrection, Faction member was severed after death", only while the corpse is in the world), SetDead, the writer 0x1BC5044 and the FactionMember document writer 0x1BC45A8 (whose keys include CanResurrect).

The data. Faction/Resurrection has 34 death types. Each names an OddsTable (Faction/Resurrection/Tables: Base, Bleedout, BodyCleaved_Uruk, Death_from_Fall, HeadRemoved_Olog, over 14 Faction/Resurrection/Chance records), a ScarType, a SecondScarChance and SecondScarType, a DeathEvent and loot tags. The odds roll (0x6DC65C, called from 0x6DBD4C) walks the table's ChanceRecords and keeps a Faction/Resurrection/Chance record only when the world total FS+0x21C lies within its MinTotalResurrected .. MaxTotalResurrected and the member's own count FM+0x500 within MinThisGuyResurrected .. MaxThisGuyResurrected (0x6DC784; a negative bound is no bound). The shipped records use the world total as a campaign stage and the member's count as a repeat counter:

Chance record World total Member's count
Resurrection_1st, Resurrection_1st_FallOnly 0 any
Resurrection_2ndFew, Resurrection_2ndFew_FallOnly 1 - 4 any
Resurrection_3rdFew, Resurrection_3rdFew_Bleedout 5 or more any
Resurrection_1stFew_OlogHeadRemoved / _2ndFew_OlogHeadRemoved up to 2 / 3 or more any
ResurrectSameGuy_Again / _Again2 / _Again3 any 1 - 2 / 2 - 3 / 4 or more
ResurrectSameGuy_Again_OlogHeadRemoved / _Again2_OlogHeadRemoved any 1 - 2 / 2 or more
Resurrection_BodyCleave any any

From the fifth return of a campaign on, the world total is above every upper bound (it read 375 in one late campaign), so only the records without one stay eligible: the 3rdFew pair, _2ndFew_OlogHeadRemoved, Resurrection_BodyCleave and the same-member records. Resurrection_Torment: table Base, scar Curse, a second scar (Sack_Head or Metal_Head) at 0.5, event WillResurrect_Torment. Resurrection_HeadRemoved has no odds table and no death event: a decapitated orc never returns (the olog has its own table).

The mock member. FM+0x2E0 is a 0x20-byte copy {appearance, inventory, role, flag} that the Army screen draws instead of the real member while it exists (0x75D2FC / 0x75D314 read it). Rewards with CopyAndHideFactionMemberData build it (22 of 1,239 rewards: role transforms, pinnacle scars), as does an event's SetMockMemberDead True; ClearCopyFactionMemberData, SetMockMemberDead False and the kill path without resurrection drop it. Bit 21 says the mock member is dead. Every death in a resurrection row keeps the copy whether or not the member will return; among living members only one returned but not yet presented carries it, together with bit 21 (SimResurrected sets Resurrect without touching the mock member). In one late save the 11 seated dead members all had bit 21 and 9 of them a copy; of 197 seated living members, one (returned, not yet presented) had a copy and bit 21.

Raising the dead. Resurrected_PlayerAbility_RaiseDead, the only event with PlayerResurrect, un-deaths any member, scars him Resurrected_PlayerAbility_RaiseDead, adds a level, aborts his ambushes and rolls PlayerSkill_Resurrected_Captain: one of four rewards giving tribe TribeDef_Undead, the undead items, personality Damaged901_Undead_pss_Moaner, deranged markers, a permanent fire vulnerability, poison immunity and Unbreakable, after removing every fire and poison trait. It does not touch the follower bit.

The event history

FM+0x300 / +0x308 is the member's history: 16-byte entries {Faction/Events/Events record, u16 the turn}, in order (488 of 488). 0x631648 trims it whenever it exceeds 32 entries through 0x1BC3EB8, which erases the entry with the lowest KeepPriority, the oldest on a tie; the save writes 32 slots (0x594788). KeepPriority is read only by that eviction. Shipped priorities: the WillResurrect_* death events and Resurrected_Presented 30, ActiveKilledByPlayer 10, ActiveKillPlayer_grunt 100, and 50 for frequent events such as ActiveInitialPlayerThreat, ActiveKillNamedAI, FleeRunStart and every fear or hate trigger, so a full history drops its deaths before them: 196 of 515 histories were at the cap, and 18 of 86 members who had cheated death had lost the record of it, all 18 at the cap. A spawned character's audio object receives a copy of the list (see Audio and video).

Ambushes

The ambush manager is [FS+0x30]. TriggerAmbush 0x6DDCB8(manager, definition, forced member) only queues a 0x40-byte request in the vector at +0x338:

Request offset Meaning
+0x18 the region's army it was queued in
+0x20 the Faction/Ambush/Definition
+0x28 the forced ambusher's key ({0, -1} = none)
+0x38 u32 due time in milliseconds on the world clock
+0x3C retries

A second request for a queued definition is refused unless its forced member is dead (0x114DFE8); one for the active definition is refused. The update 0x6DDBA8 runs only while no ambush is active ([manager] holds the active one) and evaluates each due request with 0x6DD92C: a forced member already in the world is discarded; the gates are the global enable [base+0x267D598], manager+0x368 (the running quest allows ambushes), +0x369 (the player is not in a FactionAmbushDisableBrush) and the post-quest timer +0x36C; a request whose army is not the current region's is retried (see Quests, story and scenes). Then 0x1C5E358: the chance (0x1C5F0E8: AmbushChance, the global-variable predicate, the player's excluded attacks, tags and buffs, minimum followers, the Overlord-follower predicate), the ambusher 0x1C5D458 (the forced one unless he is spawned or fails the job, else a job pick 0x1B5AC8C; 16 definitions with NeverPickAmbusherWithJob or no job can only fire with a provided ambusher), validation 0x1C5F070 (not spawned, rank not in ExcludedRowRanks) and the result 0x1C5D624. A failed pass re-queues the request after RetryDelay minutes. A rowless member's rank is null, so an empty ExcludedRowRanks refuses every rowless ambusher. The active ambush object (vt_0x1FFACB0) keeps +0x38 the ambusher, +0x90 the definition, +0xA5 completed and +0xA6 done; its slot 2 0x1C7E1FC only sets +0xA6, after which the manager's tick drops the request and deletes the object.

Rarity and Training Orders

FM+0x390 is a Faction/Traits/MarkerRarity record: OrcRarity_Common, OrcRarity_Epic, OrcRarity_Legendary and a test record OrcRariry_Unique (display name test_string). Each gives the hierarchy's selection effect (Interface/ElementFXList/Rarity_*), a priority (0, 1, 2) and a tint (Epic 0xFFED5132, Legendary 0xFF4509C4). Rarity is also a trait: Picker_OrcRarity_* -> the OrcRarity_* trait -> the item Buff_OrcRarity_*. A Legendary carries two epic traits (ET_*) and an ET_CanRemove_* marker, an Epic one.

A Training Order is an event. Contract_LegendaryTraining adds one level and rolls the reward lists Contract_LegendaryTraining_CommonOrc, _EpicOrc, Default_EpicTraits_PickRemovable and _MarkerSwap; the events Upgrade_OrcRarity_ToEpic and _ToLegendary are the other upgrades. The marker swap LT_MarkerSwap is eligible only for a member who already has an epic trait (IncludedTraitPicker PickerListRaw_AllEpicTraits), so applying it directly through ApplyReward makes a Legendary with no epic traits; rolling the lists through 0x350500 gives the traits first. Entering an Overlord row applies the same kind of upgrade: its RewardListOnEnterRow rolls OverlordSetup_LegendaryUpgrade_CommonOrc (twice), _EpicOrc, Default_EpicTraits_PickRemovable, Contract_LegendaryTraining_MarkerSwap and Overlord_ElementalVent_Resistances.

Tribes, roles and builders

Followers, bodyguards and mercenaries

Story and DLC characters in the hierarchy

A story character is a Faction/PresetMembers record (203; 67 belong to the DLCs). The saves of the base campaign hold the DLC cast too: the mercenary army, the ten Blade of Galadriel twin armies and their presets (the Nazgul sisters DLC3_Nazgul_Twin_1, eight DLC3ToMain_* specialists, DLC_FakeDarkTalion, DLC4_Desert_Overlord). The base game's Nazgul are presets as well (six GP_Nazgul_*). The build sequence is in Quests, story and scenes; the seating law is the creator's:

The creator seats any preset by its FactionRow. 0x1C1D378(ctx, preset) allocates (0x1C23C10), builds (0x1BC06C0: +0x190 = the preset, +0x3A0 = 0, +0x520 = 3, flag 25) and registers the member (0x61B5E8). With no row he is rowless at NoRowXPLevel - 1 (0x1C21448). With a row he is levelled for it (0x1C1BDA8), the column is the preset's FactionColumn (0x1C206AC); if that seat is taken (0x4F9014) or the column is negative, the row (0x61AC00) gives a free column (0x1C1BEB4), and the seat call 0x61E13C(flag 1) runs only when a free column exists: in a full row the character is built but unseated. Then each preset event is fired through 0x40C0AC. Its callers are army initialisation 0xFDAC54 (the definition's PresetMembers), the henchmen builder 0x1BB7898 (PresetHenchmen) and the quest path 0x62666C. Seated this way, a preset takes the row's level and his own tribe (a Blade of Galadriel preset in a Seregost warchief row: level 84, TribeDef_NazgulTwins).

DLC_FakeDarkTalion (AI_Talion, IsDarkTalion, LockedXPLevel 60) has the build event Debug_RecruitFollower, which makes him a follower: seated as an Overlord he hands the region to the player. An occupied Overlord seat is emptied synchronously by an event with EjectIfOverlord through 0x1B5B610; the engine re-places the ejected Overlord in a captain row shortly afterwards.

Unknown captains and intel

FM+0x51A is the intel level (0-3 in play), and FM+0x510 the intel mask that 0x62136C rebuilds from it with the army's InterrogationSetup (Default, three DataStages): level 0 gives 0x2, level 1 0x9B, levels 2 and 3 0xFB (3, 30 and 531 of 564 members). Mask bit 0 means the name is known; UnlockName 0x621208 sets it. The Army screen draws a generic model (Character/Models/HierarchyUnknown) for a member whose name is not known and who has never killed the player (see User interface). A level-0 start is ordinary: the builder build, 0x1BC00C8, 0x1BC02AC, the preset build (twice) and System::HierarchyResetPreSetMemberData 0x1C2373C force it, and the war simulation (0xFCD340, 0xFD0660) forces it whenever its level is lower. The hierarchy treats bit 21 (mock dead) as eliminated: the henchmen link effects 0x18316FC / 0x183191C skip such a member.

Open questions

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.