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?
- The spawn manager
[base+0x26FFBE8]is null at the main menu and live once a world is loaded. The player object persists across menus, so its presence proves nothing. A world is in play when the spawn manager exists and holds spawned characters. - Spawn manager layout.
+0x190is the number of characters owned by ambient spawning, not the length of the character array. Entries are 0x40 bytes with the character at+0x20. The manager is reallocated during travel, so a pointer to it must be re-read on every use; the same holds for character controllers, which are replaced on respawn. - A load starting shows as a new pointer at
[CGameClientShell]+0xE50. - Pause. The simulation pauses while the game window is unfocused: the world time stops and the per-object time steps are 0, while the renderer keeps drawing at full rate.
- Loads rebuild state. Some loading screens deserialise GameDB instance data and faction members again, and a region change resets bundles and animation state, so values written into those objects before a load do not survive it.
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
- Which class vt_0x1DA9640 is (
[base+0x26FFB40], constructor 0x108BB0 through 0x1081AC) beyond its shape: a 0x7BC0-byte object with two virtual methods, built by 0x109B44 from a setup object that copies a dozen texture handles into it, each marked by a flag bit; the slot was null in a loaded Lithlad.
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.