Engine core: managers, globals and key objects

The engine's top-level objects and the static slots that reach them: the game manager and its subsystems, the client shell, the main scene and the AI world, the engine interface and its services, the update root and object server, the UI and world-UI owners, time and frame counters, and the tests that say whether a world is loaded.

The engine reaches its subsystems through a small number of pointers stored in the writable globals (the .idata section, 0x25AC000-0x2AA5000). Each is a single slot written once or twice at start-up and read from hundreds or thousands of functions. Heap objects behind them move every run; the slots do not.

The top-level objects

Reads and writes are counted over the whole executable (every instruction that addresses the slot).

Slot Object Reads / writes Identified by
[base+0x26FFAA8] GameManager (GM) 2,851 / 1 the gameplay subsystems below hang off it
[base+0x26FFA98] CGameClientShell (vt_0x1F65D70) 2,603 / 1 its slot 21 prints "ERROR in CGameClientShell::InitializeSubsystems"
[base+0x26FFAB0] MainScene (vt_0x1F68920) 313 / 1 its constructor 0x178608C stores this here; the "MainScene" string; vf10 is the scene tick
[base+0x26FEFD0] the engine interface (vt_0x1E6AEC0, 0xD8 bytes) 1,483 / 1 built by 0x117F8B0; getters for the engine's services
[base+0x26FEFD8] the application object (vt_0x1FFE760) 414 / 2 the main loop calls its slots 33 and 29 every iteration
[base+0x26FF050] the update root 88 / 2 written by the main-thread routine 0xC24DB0; +0x3E0 is the object update manager
[base+0x26FF058] the object server (vt_0x22C0B28) 262 / 2 creates world objects (see World); its vf5 reports "busy"
[base+0x26FFBE8] the spawn manager 55 / 1 null at the main menu; see below
[base+0x26FFB08] the UI manager (vt_0x1F7A878) 1,982 / 4 [+0x50] is the menu system, its layer vector at +0x500 .. +0x508
[base+0x26FFB60] the world-UI owner (vt_0x1F97950) 177 / 1 owns the Army screen's stage holders (+0x2B8); its Update (vf18) runs every frame
[base+0x26FFB80] the world-map menu layer (vt_0x1FA0B20) 42 / 2 dirty bytes +0x275 (re-apply every captain's look) and +0x276 (rebuild rows)
[base+0x26FF960] the audio manager 124 / 3 the Wwise integration's globals sit next to it (see Audio and video)
[base+0x2A34C68] the player list 13 / 1 an engine vector of CPlayerObj (below)
[base+0x26FFC48] the AI sighting matrix 61 / 1 equal to the AI world's +0x50 (see AI)
[base+0x26FFCC0] the target-select picker table 5 / 1 27 pickers scanned by every NPC's target selection
[base+0x2702598] the world clock (vt_0x22F2E98) 358 / 1 one of the engine's scaled clocks (below)
[base+0x2A86750] the resource I/O core 75 / 2 see Resource streaming
[base+0x2A86760] the Havok holder 67 / 1 +0x20 is the hknpWorld (see Physics)
[base+0x2A86808] hkFreeListMemorySystem 20 / 3 type information .?AVhkFreeListMemorySystem@@
[base+0x26FEF88] the default heap (the static object 0x277CB40) 664 reads see The executable
[base+0x26FEF80] a second pointer to the default heap 456 / 430 written by the lazy initialisation inlined into 69 allocation functions
[base+0x26FFC28] the fight-circle participant manager 8 / 1 its constructor 0x1AA2664 stores this; the readers use Combat/FightCircle/Participant (ring weights) and Combat/Cooldown
[base+0x26FFC58] the exit-combat-space manager 7 / 1 its constructor 0x1AA852C stores this; the readers use Combat/ExitCombatSpace (RepulsionRange, AttackDecayTime, MaximumTargetRange)
[base+0x2703710] the first object of the navigation-mesh list 8 / 3 the next object is at +0x80 and the count at 0x2703700; the shutdown 0x12FC900 walks the list and destroys each object
[base+0x26FFB78] movie playback state 5 / 2 written by 0x1A13AD8 and 0x1A1355C (which use Interface/Movies.CommandSet), read by the movie-complete and movie-cancel script handlers; null while no movie plays
[base+0x26FFB40] the last object of class vt_0x1DA9640 2 / 3 its destructor 0x1097F0 clears the slot when it holds this
u32 at 0x26FFB48 the serial counter of timed UI events 1 / 1 0x32E4DC initialises a timed UI event on the UI clock (used by the HUD's event queues, CUIHUDEvent<...>::AddEvent) and gives it the next number, stored at +0x10

Some engine objects are static objects in the image rather than pointers: the render thread at 0x267E338 (its first qword is the thread-class vtable vt_0x1FFD4D0), the file system at 0x2A54600 (reached by 14 lea instructions; see Resource streaming), the console registry at 0x2A58BC0 and the Direct3D block at 0x272D1C0 (see Rendering).

The GameManager

Offset Subsystem
+0x30 a list on which world-object loaders queue objects for later processing
+0x6C60 the quest system (ticked by the scene tick, 0x4A859C)
+0x6C68 the faction system (the Nemesis world: pools of captains, army contexts, the current region; see The Nemesis system)
+0x6CC8 fort-assault setup
+0x6CE8 the main script VM (CScriptVM; see Scripting)
+0x6CF0 the global script agent
+0x6D08 the script system
+0x6D28 gifts and world items
+0x6D30 loading progress (it reports ENTER_WORLD_HANG_DETECTED)
+0x6D90 a vector of objects ticked by 0x179F64 (each whose byte +0x252 is set)
+0x6D98 the weather manager (see Rendering)

CGameClientShell

Offset Field
+0x888 the player (CPlayerObj)
+0x898 the active camera (its vf6 0x1A6E3C returns the camera position)
+0x8A8 a client-state object (its +0x183E0 flag switches NPC updates to a special path)
+0xE50 the current load task: a new pointer here means a load is starting
+0xE60 the network mesh (see Saves, the store and the network layer)
+0xE90 the local users

Its vf1 0x1A864C is the per-frame client update, half of the main thread's time in play (see Threads and the frame).

MainScene and the AI world

The MainScene's vf10 0x1F6958 is the scene tick and its vf1 0x3365A4 the render submission. Its members are the engine's world-side systems:

Offset System
+0x778 the AI world: +0x28 the fight-circle manager, +0x30 the spawn manager (equal to [base+0x26FFBE8]), +0x38 the alarm system, +0x50 the sighting matrix
+0x788 ticked second in the scene tick (0x1F7240)
+0x798 game mode, projectiles, weapon-mode item givers, attached characters
+0x7A8 the slow-motion manager
+0x7B0 dynamic object groups
+0x7D0 resurrection and ambient areas
+0x7D8 render restart
+0x7E0 presentation bundles
+0x7E8 bundle unloads and world UI
+0x868 the character bundle manager

The order in which the scene tick runs them is in Threads and the frame.

The engine interface

The engine interface hands out the engine's services; none of them has a global of its own:

Slot Getter Field Service
vf3 0x11811D4 the video-clip request manager, which lives at [[base+0x26FF050]+0xB50] (vt_0x22C0DD0); the getter asserts "VideoClipRequestMgr not initialized!"
vf10 0x3AD870 +0x98 the sound manager (vt_0x1E6D5F8)
vf26 0x4C3310 +0x90 the render interface (vt_0x1FFD5E8)
vf27 0x32F9B0 +0x88 the movie manager WBMovieMgr (vt_0x230B308)

The interface also registers world shapes: an atmosphere volume's brushes reach the renderer through its vf15 (see Rendering).

The player

The player list [base+0x2A34C68] is an engine vector {+0x00 begin, +0x08 end, +0x10 u32 capacity, +0x14 u8 owned} of CPlayerObj (vt_0x1F96818). The CPlayerObj set-up pushes the player (push_back 0x19B8064), 0x19B9CBC erases it, and Engine::GetActivePlayer 0x360950 returns begin == end ? 0 : *begin. In single player the list has one entry, the same object as [CGameClientShell]+0x888.

Time and frames

Counter Where Behaviour
world time the world clock's state [[base+0x2702598]+0x08]: +0x18 u32 milliseconds, +0x20 f64 seconds (the same instant) advances only while the simulation runs; stops while the game is paused or unfocused
frame counter u32 at 0x2701FE8 incremented once per frame by 0x160AF4, its only access; it advanced 249 times in 2 s, exactly like the presented-frame counter, also while the world was paused
presented frames u64 at 0x272D268 incremented by every successful Present (see Threads and the frame)

The clocks. The engine keeps a family of scaled clocks, each a holder object with its own vtable (vt_0x22F2E90 to vt_0x22F2F10) whose +0x08 is the clock state (vt_0x22C2570): +0x08 the number of objects that use the clock as their time source, +0x10 the current step in milliseconds (u32) and +0x14 the same step in seconds (f32), +0x18 the time in u32 milliseconds and +0x20 in f64 seconds, +0x28 / +0x2C the rate's numerator and denominator, +0x30 the scale, +0x34 the effective rate of the last step (f32), +0x38 the smallest and +0x3C the largest step in milliseconds, +0x40 a stop byte, +0x44 extra milliseconds added to the next step once, and from +0x60 a list of listeners. The update (vf2 0x59A2F50) computes the step as rate x the real elapsed time plus the pending extra (which it then clears), forces it to 0 and skips the clamp while the stop byte is set or the rate's denominator exceeds 0xFFFF, otherwise clamps it to [+0x38, +0x3C], and passes it to every listener's vf2. Six clocks clamp to 1-100 ms, so a long hitch advances them by at most 100 ms; the unscaled and real-time clocks have no upper bound (+0x3C = 0xFFFFFFFF). With the game paused, the seven pausable clocks read stop byte 1 and step 0, while the real-time clock read stop byte 0, steps of 3-4 ms and a rate of 1.0. The slots are the world [base+0x2702598], the player [base+0x2702548], the combat companion [base+0x2702570], the world UI [base+0x2702558], presentations [base+0x2702578], an unscaled clock [base+0x2702550], the UI [base+0x2702588] and a real-time clock [base+0x2702590]. Three more clocks fill the gaps in that run, 11 slots in all from 0x2702548 to 0x2702598: [base+0x2702560], which one ClientFX manager outside the client shell runs on, [base+0x2702568] and [base+0x2702580]. Every clock but the real-time one stops while the game is paused: with the game paused, the real-time clock advanced 1,015 ms in one second and the other seven not at all. Slow motion scales the first five as channels (see Quests, story and scenes).

The count at +0x08 is a reference count that includes the clock's own holder: the presentations clock reads 1, and its only reference is the holder's +0x08. On each of the 11 live clock states the heap holds exactly two more pointers to the state than +0x08 says, and those two are the state's own +0x58 and +0x80, which point back at it. So the world clock's count, 113,151 in a loaded Lithlad, is 113,150 users and the holder, against 31 on the player clock and 477 on the UI clock in the same scene. A ClientFX manager takes two references to its clock.

A character's time source [char+0x88] is the state of one of these clocks: 114 of 115 characters in the spawn manager use the world clock and the player uses the player clock. The object update scheduler subtracts that clock's step from each object's countdown (see Threads and the frame), so pausing (step 0.0) and slow motion throttle NPC and player updates through the same values. AI services run on the world time instead: each service is due when the world time at +0x20 passes its own due time.

Is a world loaded?

Threads and asserts

Anything that allocates engine objects, dispatches to scripts or rebuilds a world object runs on the main thread; the render thread consumes commands the main thread posts (see Threads and the frame). Engine asserts are a write to a null address at 0x819113 with the message in rcx (EngineAssert 0x819104).

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.