Saves, the store and the network layer

The save file format and its encryption, the typed section grammar and what each slot section holds, the engine's own save switch and the new-game flow, the client side of the old in-game store, and the dormant peer-to-peer network stack with the asynchronous online modes.

The save file

A Steam save is Steam\userdata\<account>\356190\remote\<account hex>_<slot>_ShadowOfWar.sav; the file-name prefix is the Steam account number in hexadecimal.

header  'SOM3' | u32 salt | u32 padded length (= file size - 16, a multiple of 16) | u32 real length
body    AES-256-CBC, PKCS#7 padding
key     256 bits, derived per file from a constant built into the executable, the header's salt
        and the owning account's number (SOM2 files leave the account out)
iv      a constant built into the executable

The reader 0x119A2C8 accepts SOM2 and SOM3 (sub eax,'SOM2'; test eax,0xFEFFFFFF); the writer is 0x52B4BC. Both use OpenSSL's EVP layer linked into the executable (EVP_aes_256_cbc, the decrypt init / update / final calls and the padding switch). The padding byte equals padded length minus real length, and the plaintext's entropy drops from 8.00 to 5.01 bits per byte. This reference describes the format; it does not publish the key material.

The typed grammar

The plaintext opens with SAVE version 2: a header (a u32 profile-section count at +0x18, 5), the profile sections, a u32 slot count (3), then per slot {u32 6, u32 length, u32 0, payload}. A section is tag, u32 version, u32 size, u32 0, u32 n (= size - 4), n bytes of typed values:

Tag Value
00 a 32-bit name hash (4 bytes)
02 u8
04 u32
05 two u32 (a GUID half, a pair of counters)
08 i32
0A f32
0C bool (1 byte)
0E vec3 (12 bytes; positions in cm)
0F byte string (u8 length + bytes: GUID text, a product id)
10 u32, the byte size of what follows

With these ten tags, 27 of 27 sections of a story slot read to their exact end. GameDB records are saved as name hashes, never as strings (a 2.3 MB save holds 421 ASCII tokens, all settings); the hash does not carry a category, so the section's context decides which category it names.

Layout. 86 sections: 5 profile sections (GOXC PERF PICB GPDC GMSC) and the same 27 in each of the 3 slots. Per slot, in file order: SAVE (the summary, version 31), a u32 giving the byte length of the rest of the slot, IVGD (v3), ENT2 IVSH PPDC PDIC PAIC IEUC IVMG (v43), IVPD (v4), TTRL LTMR, about 1.28 MB with no typed section header, then FAST FOTP ASMC DIFF CBTC GCPT INVA REEL QSTD (v23) PDCC LPDC AUDI IIUC UITC STOR (v159). Most sections open with 04 3, 00 hash(<game mode>).

Section Contents
SAVE (slot) the summary: 10 values (region, a hash, a percentage, play time in minutes, ...)
IVGD the 573 game variables, three times: 00 variable hash, 02 VariableType, value. The first copy holds the played values; the other two hold the record defaults
PPDC the per-mode level and XP table ([GM+0x6D20]): {00 mode, 04 level0, 04 XP} (level0 + 1 is the level shown)
IVMG the player's inventory (below)
IVPD 584 {00 item, 04 value} pairs for the skill items (PS_*, S_*)
PDIC contains world positions (vec3)
GMSC (profile) per game mode {mode, int, sub-object, score record A, score record B, bool}; a score record is {mode, hash, 5 ints, 4 floats, bool} with the score as its second float

IVMG, the inventory, in order: a header (04 3, 00 mode, 0C, 10 size, 05 (...), 00 PlayerSkin00, 04 <one above the highest item id>, 04 729, 0C 1); the owned stacks {00 item, 04 count, 04, 04 n, n x 04, 0C} (714 on a finished save; the Mirian currency is a stack too); 14 bools; then the gear records {04 id, 04 level, 02 0, 00 Inventory/Items, 00 EquippedWeaponData, 00 0, 0C, 0C, 04 n, n x 00 Affix/Definition, 04 0, 05 GUID low, 05 GUID high, 0F source product, 02 0, 0C has-nemesis} followed, when set, by a nemesis block (00 NemesisGear, 00 Description, 00 Role, 04 seed, 04 seed, 0C, then optionally an upgrade tier, art, a Combat/Challenge/Eval challenge with its progress and a weighted Faction/Loot/Tag list) and two closing bools; then 25 per-slot entries {0C, [04 item id]} (one per inventory slot); then reward items and 8 gem sockets {00 SocketType, 0C, 00 SocketType, 00 gem}, and DLC flags.

The front-screen card. The card printer 0xDF9370 reads the slot info [CGameClientShell+0xEB8] + 0x1C8 + GameModes.SaveSlot * 0x298: it prints ceilf(+0x24 minutes) as hours and minutes and floorf(+0x20) as the completion percentage. Both come from the slot's save summary: its value 3 is the completion in percent and its value 4 the play time in minutes (a summary holding 100.0 and 14,655 showed on the card as "100 %" and "244h 15m"); for a game mode with GameModes.UseHighScore (the fourth story DLC's mode) it prints the score from the GMSC entry found by 0xFAD1B0([base+0x2A36278], mode) (the first entry with +0x80 equal to the mode and +0x50 non-zero, score at +0x68).

Saving and starting a new game

The save switch. Both world-save paths read GameModes.CanSave of the current game mode ([base+0x2A36260]; SinglePlayer in the campaign) and skip the save when it is clear: the save task (vt_0x1F6CC40 vf3 0x52C70C -> 0x52C664, which also refuses on the client shell's state at +0xE38, flags at +0x189 / +0x194 and +0xA20; accessor 0x52C5EC) and the world save 0x5B7B58 ("Attempting to save the game without a world to restore", test at 0x5B7BA8). CanSave is packed bool 13 of the record's first cell. It ships clear on QuestReplay (story mission replays) and Benchmark: the engine is built to run a world that never saves. SaveCurrentWorld (bit 3) is a second gate inside 0x5B7B58.

The mode-select screen (GameModeSelect; layer class vt_0x1F92AF0, record Interface/Menu/MenuLayer/GameModeSelect/ModeSelect) lives in the menu system's layer vector ([[base+0x26FFB08]+0x50], +0x500 .. +0x508). The highlighted row is [layer+0x1348] (node +0x28 the record, +0x230 the GameModes record, +0x238 the world). The profile slot info is [[CGameClientShell+0xEB8] + [CGameClientShell+0xED0] * 8] (+0x38 = 3 when ready); a mode's slot is +0x1C8 + SaveSlot * 0x298, whose +0x08 is non-null when the mode has a save.

New Game+ was cut by nulling one link. Interface/Shared/Shared still carries the whole New Game+ card: NewGamePlusPresentationItem (Inventory/Items/Player_PresentNewGamePlusDialog), a bundle path that ships in no archive, an image, an icon, a command set (Profile/CommandSets/NewGamePlusConfirm), the title "Congratulations!" and a description key with no string - but NewGamePlusPresentationEvent is null, and the player-UI tick 0x5218C8 builds nothing without it. No restart logic stands behind the card. The console command UI.NewGamePlusDialog (registered by 0xB2220) only sets a byte at +0x98 of the player's +0x2F98 object, which nothing else in the analysed code writes; Quest/Quests.ShowNewGameDialogOnComplete has a field binding but no reader. The game's own delete warning lists what a new game keeps: all boosts, unused training orders, undeployed follower orcs and unopened chests ("available in Act 2").

The player profile and settings

The engine keeps the local player profiles in a vector at CGameClientShell+0xE90 (+0x28 begin, +0x30 end; one profile on PC) with the active index at +0x40 (u8, 0xFF = none). 0x30B254 returns the active profile when its state +0x38 is 3 (loaded). The profile object (vt_0x1F6CC40, constructor 0x17C32F8) also owns the save task (its slot 3 is 0x52C70C, see Saving and starting a new game) and the slot summaries the front-screen card prints.

Offset Content
+0x1C0 u8 bitmask of the slots present
+0x1C8 three slot summaries of 0x298 bytes
+0xAB0 the option store (the GOXC chunk)
+0xB10 pointer to the input binding object
+0xB28 the per-mode score chunk (GMSC)
+0xB30 the command-set manager

Where the settings are stored

Settings live in the save file itself: the five profile sections at the start of every save (see The save file) are the profile's chunk objects, each returning its tag from slot 5 and its name from slot 9.

Tag Chunk Version Content
GOXC GlobalOptionChunk 5 the two difficulty settings, then 118 {00 option name, 02 type, value}; a record-typed option is 04 hash(category name), 00 hash(record name); then 19 {00 DLC/Config record, 0C flag}
PERF PerformanceChunk 6 25 {0F console-variable name, 0A value}: ClientFXMgr.DetailLevel, WorldDetail, FSAA, LODTextures, ScreenWidth, ScreenHeight ...
PICB ControlRebindings 8 the player's rebinds: {00 button, 04 hash(ActionID), 02 slot, 0C, 0C, 00 model-override type}
GPDC GlobalProfileDataChunk 5 profile statistics, one entry per Interface/GameProfileData record (350 of 350): a count and {04 key, 04 value} for the 269 records of DataType 0, then the 3 of DataType 1 with 0A float values, then the 78 of DataType 2 with 0C flags, then three zero integers; the key is the CRC-32 of the record's name
GMSC GameModeSessionChunk 5 per-mode scores (see The save file)

The statistic keys use the standard CRC-32 (0x505400(buffer, length), table 0x27C54D0, initial value 0xFFFFFFFF; six of its 13 callers read Interface/GameProfileData), case-sensitive: all 350 keys of one save reproduce. It is a third name hash beside the GameDB's (see GameDB) and the animation keywords' multiply-by-31 hash (see Animation). Each statistic record also carries IsGlobalData, ResetOnNewGame, StorageType, UseOnlineStorage, Association, CollectableList and the platform statistic names (SteamAgoraName, XONEStatId).

The value of an option changed in the menus is the same in the live store and in the save file.

Options

Profile/GlobalOptions defines 118 options (EGlobalOptionType: 65 bool, 24 float, 23 integer, 6 record) with their defaults; the prefixes group them: CP_ controls (25), GP_ gameplay (25), VP_ video (14), AP_ audio (6), Skin_ skin unlock and notification flags (19), SS_ save slots, Lang_ languages. Lang_Text and Lang_Audio hold the locale as a four-character code (enUS), and CP_Binding_PC names a Profile/Inputs/PC/Bindings record.

The store (vt_0x1F6F548):

Offset Type Content
+0x08 vector difficulty settings (reset from Difficulty/Lists.DefaultDifficultySettings)
+0x18 table {data, u32 count, category} of 0x20-byte entries {+0x00 record, +0x08 value, +0x10 u32 type}, indexed by record index; the value is a bool, f32 or i32 in its low bytes, or a record pointer
+0x30 u8 dirty
+0x38 table DLC/Config flags, 16-byte entries {record, u8}

Options are addressed by name: 0x2784D0 hashes the name and binary-searches the category's sorted name index ([base+0x27007A8]), and the getters read the record's entry: GetBool 0x2785A0 (falling back to the record's DefaultBool), GetRecord 0x438468, SetBool 0x7CF4C8, and a float getter 0x5238A4 (used for CP_AnalogDeadzone). Slot 1 (0x17D5060) resets every option to its default. When the profile loads (0x17C65FC) the engine polls four gamepad ports through the input system [base+0x26FEFC8] (slot 13), clears CP_UseGamepad when none answers, and applies the binding named by CP_Binding_PC.

Commands, buttons and bindings

Profile/Commands (382 records) defines one command per action: ActionID (a string enum; at run time commands are keyed by the enum index), CommandType (ECommandType, named in the executable's enum table: COM_MOVE 0 on 119 commands, COM_INV 1 on none, COM_VIEW 2 on 9 - yaw, pitch, their accelerations, the debug camera's four and CenterView -, COM_MISC 3 on 8 - the message-box confirm and deny commands and DebugMenuActivate1-4 -, COM_WEAP 4 on CombatSecondary01 and 02, and kNumCommandTypes 5, the count used as "none", on 244), Event (289 of 382), IgnoreWaitForOff, and two button slots per platform: PCKeyboard, PCGamepad, XBoxOne and ps4. The keyboard binds 226 commands (60 in both slots), the PC gamepad 275, the console pads 270 each. Profile/BindingSets (17) group commands in a CommandList plus CommandCharacterModelOverrides (alternative commands for the Eltariel and Baranor model types); Debug and DebugSyncAction are marked DebugOnly. Profile/Inputs/PC/Bindings holds nine keyboard bindings, one per locale, each listing the same 13 sets; Profile/PCKeyboards adds the AZERTY, QWERTY and QWERTZ layouts with five key overrides.

Profile/Inputs/PC/Buttons.Device is one code space for every PC device (155 of 155 records):

Codes Device
8-223 keyboard keys: the Win32 virtual-key code (KeyBack 8 ... KeyOEM8 223; 117 keys)
1000-1008 mouse: buttons 1-5, the X and Y axes, wheel up and down
2000-2019 gamepad: 2000 + the console pad's own code (Profile/Inputs/Durango/Buttons, 0-19)

0x438498(binding object, binding record, gamepad) builds the active bindings through 0x5B6D20: for each set of the record (skipping DebugOnly sets), first each model override and then each command, every non-empty slot of PCKeyboard (or PCGamepad) becomes a 0x18-byte entry:

Offset Type Content
+0x00 ptr the button record
+0x08 ptr the model-override type, or null
+0x10 u32 the ActionID enum index
+0x14 u8 slot (0, 1; 2 for a copied binding)
+0x15 u8 Event
+0x16 u8 IgnoreWaitForOff

0x5B6C00 then applies CopyBindings (copies of one command's entries under another command, slot 2). The binding object holds the player's rebinds at +0x00 (from PICB), the active list at +0x18 and a copy of the record-built list at +0x30; 0x438518 overlays the rebinds, replacing the button of the entry with the same ActionID, slot and override type, or appending, and only for keyboard and mouse (it returns at once while CP_UseGamepad is set). With KeyboardEnglish the active list holds 363 entries: the 348 the records predict (280 in slot 0, 68 in slot 1, 128 of them model overrides), 13 copied slot-2 entries and 2 rebinds. The binding is rebuilt by the handlers of CP_Binding_PC, CP_UseGamepad and CP_Southpaw.

Command sets

A command set enables or disables commands while some state lasts. Profile/CommandSets has 231: EnableCommands, DisableCommands, DisableAllOtherCommands with DisableAllOtherExclusions, WaitForOff and AllowMenuToGameplay. The profile's manager (vt_0x1F6B510, constructor 0x17B77F4) compiles all 231 once (0x17B7E08 -> 0x17B8580) and keeps a stack:

Offset Content
+0x08 priority table {data, u32 count, category}, 16-byte entries {record, u32 priority}
+0x40 / +0x48 the compiled sets
+0x58 the input-system context handle
+0x7B u8 dirty
+0xD0 / +0xD8 the active stack: set records in ascending priority
+0xE8 / +0xF0 the compiled-set index of each stack entry
+0x100 / +0x108 sets pushed with the flag argument

A compiled set holds the enabled ActionIDs (+0x00), the disabled ActionIDs (+0x18), the DisableAllOtherCommands byte (+0x30), the exclusions (+0x38), the WaitForOff byte (+0x50) and the record (+0x58). The priority (0x17B745C) is the record count minus the set's position in Profile/CommandSetPriority.Shared (217 entries, 213 distinct; the first is the highest, a later duplicate overwrites); an unlisted set has priority 0.

Push 0x5B5E40(manager, set, flag) (43 callers) refuses a set already on the stack, inserts it before the first entry of higher priority (or appends it) and marks the stack dirty; pop 0x5B4AEC(manager, set) (55 callers) removes it; 0x17B7D38 clears the stack. The active profile's push and pop are 0x7DFB8C and 0x7DFCF8. Scripts reach them through PlayerInput.ApplyCommandSet / ReleaseCommandSet and Quest::ApplyCommandSet / ReleaseCommandSet; menus, presentations, photo mode, struggles and interrogation push their own sets. The debug menu pushes the set named in Debug/Shared.DebugMenuCommandSet (DebugMenu: menu navigation plus two debug shortcuts, with DisableAllOtherCommands and WaitForOff) on open (0x18CC238, 0x7B5980) and pops it on close. A stack read in free roam held StateSupported (priority 15) below PresentationIdentifySystemic (126).

The in-game store

The pre-2018 Market is the pause menu's GARRISON: Root_Store -> Interface/Menu/MenuLayer/Store (the label resolves to "GARRISON"; an older patch archive still says "MARKET"). Its sections are Store_Garrison (Followers), Store_Stash (Unopened Chests), Store_Contracts (Training Orders), and Store_Vendor and Store_Boosts, which build only from visible catalog products.

The network layer

The engine keeps an older peer-to-peer multiplayer layer. The shipped game builds it and ticks it every frame; nothing hosts or joins.

The online modes are asynchronous. GameModes lists ranked and friend Overlord arenas (online fight pits), Vendetta and ranked / friend Conquest. Opponent data comes from the online service; the fight is simulated locally. An online fight pit is the quest <Region>_ArenaOnline (six regions; Quest/Types::OnlineArena; setup Quest/Jobs/SetupSimple::ArenaOnline: job 0 the player's Overlord, job 1 ArenaOnlineOther = UseOnlineArenaOpponent, builder ArenaOnline_Other). The ranked menu 0xE24AE0 sends a matchmaking request; its reply parser 0xF74430 reads {Id, Info, Overlord} or an error string. The opponent goes through 0xF548C0, the same orc importer as Vendetta and Conquest (0x1B41894), is level-checked by 0xF58EF0 and stored as __online_arena_opponent__. Launching (0xF72D30, quest enter 0xEA7700) sends the current region's Overlord, serialised by 0x1BC5044(fm, map), the engine's orc exporter (also used for publishing, Vendetta and Conquest; an exported orc carries name, title, trait level, rarity, appearance piece hashes, inventory, combat tree and about 70 keys in all).

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.