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.
- Seat 0x61E13C(ctx, fm, row, column, flag): flag 1 = a new member (the creator 0x1C1D378), 0 = a
move (the sim outcome 0x40C304). It refuses a move into a warchief row whose side differs from the
Overlord's when
FM+0x4F8is set (0x1E4C08 / 0x114E020); it vacates first, re-levels the member for the row (0x1C1BDA8 -> SetLevel 0x35188C), checks that the column is below the seat count, unlocked and empty, then writes the pool index into the seat,FM+0x51BandFM+0x268(0x61B614). - Vacate 0x61B274(ctx, fm): -1 in every seat holding his index,
FM+0x51B= 0xFF,FM+0x268= 0, and the dirty bytesctx+0x1EE / +0x1F0. 0x61D354(fm, bool) marks a member as moving (FM+0x51F). - Swap, the engine's own at 0x40C767 (inside the event-processing code): mark both, vacate B,
unlink B's pool node (
FM+0x18), seat A into B's seat and B into A's old one (both flag 0), unmark. - Transfer between armies is
FactionSystem::TransferMember0xF6AC90(FS, fm, {army, row, column}): it refuses unless both armies share a pool and refuses a move inside one army; it unlinks the member (0x1C25500), rewritesFM+0x178(and+0x180when empty) and fires the destination'sEventSetup.TransferIntoFaction(every base army shipsTransform_HireMercToEmpty). Any two MainGame armies share a pool.
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
- From the seats. For each army context, each runtime row and each column, a seat value other
than -1 is a pool index into the army's pool object (
ctx+0x20). This finds exactly the seated members, dead or alive, with their row and column. - From the pools. Walk every pool's member vector and keep the valid members (
[FM+0x178]is a record). A member with a row (FM+0x268, named<region>_Row_<rank>) is a captain, warchief or Overlord; dead ones have flag bit 2. Story characters carryFM+0x190and normally no row. Spawned members haveFM+0x48.
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:
- SetDead 0x626E38(fm, dead) sets bit 2 and, when
FM+0x398names a campaign variable, sets that variable (0x59A1D8): a story character's death unlocks the next scene this way. Its un-death branch (0x627197) clears bits 2, 12 and 17 but not 21. - The follower setter 0x7D52B0(fm, bool) changes the member; the character's
CCharacter::SetIsPlayerFollower0x102F0C(char, bool) also sets the character's follower byte (+0x23B8) and its combat stats before calling it. Region ownership is the Overlord's bit 27 (see Quests, story and scenes). - The intel setter 0x353EC4(fm, level, fireEvent, force) refuses a level that is not higher unless
forced, clamps to the setup's
DataStages, writesFM+0x51A, rebuilds the maskFM+0x510(0x62136C), notifies the player (0x7BE290, which reaches the global script agent[GM+0x6CF0], so it belongs on the main thread), updates trait visibility on a body, fires the army'sDataLevelChangeEventwhen asked, and re-resolves his traits (UpdateTraits reason 6). - Bit 32's clear 0x18E218 is a bare
btrwith no event.
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:
System::CreateAndProcessFactionEvent0x1B5B610(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.- 0x40C0AC processes the event synchronously; an event raised while another is in progress
(
FS+0x208) is queued atFS+0x1F0and drained afterwards (post-processing 0x1E2DA0). - 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.
- AdvanceTurn 0x1B5EE88(FS, unused, u8 flushPending) increments the turn
FS+0x218, on a flush runs 0x1C1C4E4 on every army (drainingctx+0x140), then runs 0xFDEC20(ctx, save slot), turn processing and generation (FactionSimEventGenerate0x597A9C), for every army that passes 0x1B5B058. That test refuses an army withAdvanceTODCurrentFactionunless it is the current region's army (FS+0x1B8), and otherwise requires the army'sAdvanceTODStoryActiveto match a state read through[GM+0x6D30](0x114E008); the campaign armies ship with the first False and the second True. AdvanceTurn's callers are the quest post-active state 0x19F8C34, the world reset 0x1789F0C (the player's death), the single-hierarchy view 0x434CF0 (no flush) and 0x1B5ED08. The world clock stops while the Army screen is open. - Generation. For each member, his row's
SimEventGenerationSetuplists entries that are tried in order (0x5924EC; each entry may require a follower, a non-follower or either). An entry'sFaction/Events/SimEventGenerationTyperecords (68 ship) are picked by weight (a reservoir pick 0x1B3B90 in 0x5926EC; a failure drops the type and retries) and 0x5935DC creates the event. A type's weight isWeights[min(active events of that type in the army, count - 1)], or itsWeightOverride's entries when set (difficulty-scaled throughSimEventGeneration_*constants); weight 0 is the cap.PowerStruggle_Act2lists 13 sim events (Chase, Duel, Execution2, GraugHeist, the raids, the trials ...) withWeights10, 7, 4, 0 and the global-variable predicateAct2. Candidate lists are capped at 64 (0x597E44); members with a current event (FM+0x4F0), dead members without bit 4, and members with bit 21 are skipped; one event per member. - The event object (vt_0x1DBC8A8):
+0x40the army,+0x48theFaction/Events/SimEvent(431 ship),+0x50 / +0x58its quest (aQuest/Questsrecord such asVO_Chase_02),+0x60the generation type,+0x94a serial number and+0x98the turn it expires (created turn plus the type's lifespan). An army's events are atctx+0x90 / +0x98.
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:
- Mark. The kill path 0x6DB95C (0x1BB9F84 for event kills) stores the death's
Faction/Resurrectionrecord atFM+0x2C0and, when the row hasResurrectionEnabled, 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), incrementsFS+0x21CandFM+0x500, sets bit 4, stores the scar atFM+0x348(unless it equalsFM+0x370) and fires the record'sDeathEventthrough 0x40C0AC. WithoutResurrectionEnabledthe kill path resets the mock member (0x6DBCFC). - Queue. The death event (
WillResurrect_<death>, 23 of them,KeepPriority30) carries reward lists that queue the ambush:CheatDeath_Ambushfor an enemy,CheatDeath_BetrayalorCheatDeath_FollowerStillLoyalfor a follower.CheatDeath_Ambushretries forever (RetryMax-1,RetryDelay0.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 requiresMustHaveCanResurrectTrue. - Complete. Only the apply 0x630644 un-deaths: when the member has bit 4 and the event has
Resurrect, or the event hasPlayerResurrect, 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) addsSetMockMemberDeadFalse,ClearCopyFactionMemberData,PickNewSimEvent, one level plus a player-based level change, andEjectIfWarchief/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
- Tribes are
Faction/Tribes/Definitions(19): Dark, Feral, Machine, Marauder, Militant, Outlaw, Slaughter, Sorcerous, Terror, Warmonger, Undead, Easterling, Tribeless, the special tribes Bruz, Gondor, Sauron, ShadowWar and NazgulTwins, andDLC_TribeDef_Sorcerous_Poison. A member gets one when he is built (0x1BC6718, reason "Builder") or crosses the level threshold without one. - Roles (
FM+0x318, 914Faction/Roles/Definitions) carry the title; the role setter 0x1BC63F8 writes+0x318and+0x320. - Builders. A
Faction/FactionMemberBuilder(608 ship) is a recipe:CombatTrees,Roles,Tribe/IncludedTribes,ArmorRowRank,MinLevel(+MinLevelDelta,PlayerBasedXPLevel),CalcXPLevelOnAssignToRow,DataLevel, trait markers, desired and excluded trait pickers,RandomTraitPickerLists, personalities,Rewards/RewardLists,SimEventPickers,PlayerFollower,CanSave,RemoveAfterQuest,EnableTraitChanges,EnableRelationships,UniqueBuilderInstance.Captain(MinLevel5) andEmergencyOverlord(MinLevel30 +/- 5, enters throughBuilder_EnterHierarchy) are the base ones; families such asFD_InvCap_*,DLC5_Y<n>_<region>_C<n>_*,SW_Y*andGauntlet_*serve other game modes. A build request is prepared by 0x1C63920, which stores the builder at request+0x168, itsUniqueBuilderInstanceat+0x140and its flags; the builder build 0x1BBF928 then copies the builder toFM+0x3A0and the instance toFM+0x350, sets+0x520= 2, and writes the seed, a name from the class map, a personality (0x1BDA7AC), forces the intel level to 0 (0x353EC4 withforce), assigns a tribe, the follower and level-cap flags, then levels him and resolves his traits.
Followers, bodyguards and mercenaries
- Followers. Bit 27; set and cleared by events (
SetPlayerFollower,ToggleIsPlayerFollower) and by the follower setter 0x7D52B0, whichCreateEmergencyPlayerFolloweruses. A betrayal clears it. - Bodyguards. Only the Captain rank has
CanBePlayerBodyguard. Bit 29 marks the player's bodyguard (the eventBecomeBodyguardis the one withChangePlayerBodyguard), and bits 48-50 give the summon slot read by 0x1010790 together with the army'sPlayerBodyguardSummonDataSlot0-2. Overlords and warchiefs have their own bodyguards: their rows' henchmen, made by 0x7DEEE0 when the army fills or a preset with henchmen is built. - Spies and prisoners are event flags (
SetIsSpy, bit 28;SetIsPrisoner). - Mercenaries. A hire is a transfer, not a new member. The mercenary screen's action 0xDBC910 takes
the member from
Faction_desert_mercs_DLC4, prices him (0xFA3AF0: the row'sHireCostthrough the army'sHireCostVariable/HireCostOption, scalar 1.0 without discount skills), pays (0x189353C) in the currency item of the player's skill setup (PC_Mirianfor Talion), then callsTransferMember0xF6AC90. A zero price passes the spend check 0x1BCEC24. The mercenary army fills its members as followers and mercenaries (bits 27 and 51). It binds the DLC4 pool and belongs to the DLC4 save slot, so the base campaign never fills it.
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
- What
FM+0x4F8means. The seat function tests it; it held 1 on 2 of 555 members in one campaign, both in the InvasionGame pool, and 0 on 527 of 527 in another. - Why flag bit 16, which the un-death sets, is missing from most returned members.
- What reads flag bit 35. It has no resolved getter call, and 60 of the getter's 125 calls pass the bit indirectly.
- Whether the role counter at
FS+0xB0is the number of times the role was given (it would then lower the role's weight byWeightReductionPerUseper use, as the scar table does by its use count); the code that reads it is not traced. - Whether the pool's
ImpC_*coefficients are the importance score used when a full pool recycles a member. - Whether the world's resurrection total
FS+0x21Cis saved with the game, and under which key (the faction manager's document keys includeResurrected). - When exactly the cancel pass 0xF9BA70 runs relative to a save and a load. Its caller 0xFA0D30 is
reached from two places: the faction manager's part of the game document (0x1B5D254, keys
TimeStamp,Resurrected,FactionPool,FactionRecord,FactionRole,AmbushMgr,GauntletMgr,MonumentMgr), which the document routine 0x1C5B4C4 runs together with the quest manager, the game variables and the inventory, and the online-fortress setup's finalisation (0x1B40CAC -> 0x1B5D368, stageSETUP_FINALIZE_VALIDATE_DATA_INTEGRITY). The direction of the document routine (write, read or both) is not traced; the second caller 0xF665F0 has no direct caller. - Whether a queued ambush request's retry count survives a save and reload.
- Which loading screens rebuild the army contexts and members: values changed in the mercenary army's context and records survived two region changes.
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.