World: regions, layers, placements and world objects

How a region's world is assembled from streamed parts, what a world part holds (the object registry and its fourteen blocks, static placements, prefabs, terrain and vegetation), how placed objects look in memory, how the engine creates, binds and moves them, and the world data behind traversal (context-action lines), weather-gated effects and collision.

A region's world is a set of world parts, each streamed in as a section and turned into a world context full of placed objects. This chapter describes the parts on disk, the streamer that loads them, the objects they create and the world data that the rest of the engine queries.

Regions and world parts

The game has eight regions. Each has one main part (worlds\retail\<folder>\<folder>, the only part with terrain) and a set of layers under worlds\retail\<folder>\sectionsandlayers\: story and side content, fortress pieces, lighting and its per-Overlord variants. 1,303 layers ship (each identified by the paths its own container lists, never by its archive entry name):

Folder Region Layers
volcano Gorgoroth 243
snowmtn Seregost 239
island Nurnen 228
mtnpass Cirith Ungol 228
desert Lithlad 221
minasithil Minas Ithil 138
blackgates the Black Gate 4
baraddur Barad-dur 2

Seven regions have a region archive and a lighting archive (<folder>.arch06, <folder>_lighting.arch06), and the patch archives add and replace parts. Lithlad has neither: its main part and all 221 of its layers are in the patch archives (the containers in Patch_DLC4, with standalone .objres and .scripts copies in later patches).

A world part is an EMBB container (see Archives and file formats). Every layer carries its own prefab (.inst), and 1,302 of 1,303 carry the same eight further parts, named after the layer: .objres (the object registry below), .terrain, .hkai (navigation mesh), .wldclnt (static placements), .veggrid, .scripts (its script; see Scripting), .uis and .uiwd; then the prefabs, meshes, materials, textures and effects it uses (176 layers also carry a world mesh, .wldmsh). A layer's .terrain is an empty 12-byte section; only main parts carry terrain.

Across all 1,311 parts (1,303 layers and 8 main parts) the object registries place 1,247,824 objects of 156 classes. The most common: AINodeMenacePlace 113,874, AINavMeshObstacle 67,714, SpecialFX 67,460, DynamicObjectGroupState 67,361, ScriptRelay 56,290, StaticLightOctreeBias 54,700, FireVolume 50,718, AISpawnerAmbientNormal 49,769, LightCube 47,771, Group 43,956, VolumeController 35,992.

Patched registries win. The engine does not load the registry embedded in a part when a patch archive ships a standalone .objres for that part (2,753 standalone registries ship). Lithlad's main registry in a running game is the copy in Patch_DLC5 (161,821 context-action lines), not the one embedded in the Patch_DLC4 container (161,828) or the copies in Patch_09 / Patch_10 (161,836). Layer scripts follow the same rule (see Scripting).

How a region's world is assembled

The section streamer

WorldBundleMgr (GameSystems\Core\WorldBundleMgr.cpp) is [GM+0x6D90]. It keeps a registry vector of world bundle groups at +0x00 / +0x08 (145 groups in Lithlad, each pointing back to the manager at +0x240). The scene tick walks the groups (see Threads and the frame) and services only those whose want-load byte +0x252 is set.

A group (0x258 bytes, constructor 0x1D297EC) is the head of a circular list of entries: +0x00 the first entry, +0x08 the last, closing back on the group in both directions. +0x251 set disables the duplicate check, +0x252 is the want-load gate. An entry (0x168 bytes, vt_0x1DB6558) is one requested section:

Offset Meaning
+0x34 u32 id = StringHash(path) (127 of 127)
+0x38 list links {next, prev}
+0x48 the CResourceBundle of the section (vt_0x1E6E678, path equal to the entry's: 127 of 127)
+0x50 the part's object-registry request (set on 126 of 127)
+0x58 the bundle load request
+0x63 the path, inline (...\<part>.bndl)

AddSection 0x4B41CC(group, path, &id) calls 0x4B420C, which rejects an empty path, appends .bndl, returns the existing entry when the path is already requested in that group (0x4B1FE4) and otherwise links a new entry at the front of the list. RemoveSection 0x4B1BCC(group, id) releases every entry with that id (0x4B5C34). The entry exists as soon as it is requested, long before its content has streamed. Requests are de-duplicated per group only, so the same section requested through two groups is loaded twice.

A plain bundle load (see Resource streaming) makes a part's resources resident without placing anything; a section requested through a group is what creates its world context and objects.

What a region loads

MainScene ([base+0x26FFAB0]) owns eight group slots. In Lithlad during play they held:

Slot Gates Sections
+0x720 none (the null group)
+0x728 flag +0x250 set worlds\retail\desert\desert.bndl, the main part
+0x730 flag set empty
+0x738 worlds\retail\shared\wraith\lightingwraith.bndl and prefabs\ui\worlduimenuws.bndl
+0x740 want-load 0 the quest group: sectionsandlayers\fortassault\fortassault.bndl
+0x748 flag set the fortress: drakecollision, fortwallgameplay, fortwallprops, and sectionsandlayers\dynamicfort\ keeplarge, flairoutlawkeepl, flairoutlawworld, gatessimple, pourtrapsfire, fort_wd
+0x750 duplicate check off empty
+0x758 flag set empty

The lighting layer is requested by the weather manager in its own group ([[GM+0x6D98]+0x20]): the current weather pattern's Section field names it (see Rendering, "Weather"). In Lithlad the pattern was DS_OL_Outlaw_Clear, whose Section is worlds\retail\desert\sectionsandlayers\ol_outlaw.bndl, and that group held exactly that section. The ol_* layers are per-Overlord variants: Lithlad ships nine (ol_dark, ol_feral, ol_machine, ol_marauder, ol_mystic, ol_outlaw, ol_slaughter, ol_terror, ol_warmonger) beside its plain night01..03 layers; Nurnen, Cirith Ungol and Seregost nine each (Nurnen's and Cirith Ungol's include a shared ol_share01); Gorgoroth 27, each also in _bd and _bduc forms. Lithlad's ol_outlaw holds what a lighting layer holds (1,104 SpecialFX, 959 LightCube, 612 Sound, 175 WorldModel ... and an AtmosphereVolume), much like its night03 (1,492 SpecialFX, 959 LightCube, 612 Sound, 32 AtmosphereVolumes).

Of the 68 groups with entries, 63 hold only the menus' 3D scenes: 86 sections under prefabs\ui\mordormap\ (the world map's region pieces such as minasmorgul.bndl, and each fortress's keep and gate pieces by tribe, fortworldsections\keeps\blackguard\bgrdflairped05.bndl), 12 under prefabs\ui\store\, 6 battle-report scenes and 8 region-faction scenes (wuim_*_ws.bndl). 127 sections were loaded in all, 13 of them from worlds\.

Conditional sections. At load MainScene+0x760 .. +0x768 receives 16-byte pairs {section record, u32 id} copied from the region's WorldInfo.ConditionalWorldSections. The evaluator 0x1DFAC8 (called from 0x1DF500) reads each section's _StructuresWorldInfoConditionalSection.GameDataVariable: true with id 0 requests the section through [MainScene+0x758] (0x4B41CC); false with a nonzero id releases it (0x4B1BCC). Lithlad has none; Minas Ithil during the Shadow Wars has 12 (its pre-fall sections released, the post-fall ones loaded).

Region travel. The script call System:LoadWorld(DBWorldInfo.<world>) on the main thread starts a region load within 0.1-0.2 s (LoadWorldCustom, which no shipped script uses, shares its implementation 0x1780824). The current region record [[GM+0x6C68]+0x1B0] changes at the start of the load, before the destination's objects exist.

World contexts

The objects of a streamed part live in a world context (vt_0x1DB5D58). The object update manager's object sets (see Threads and the frame) are these contexts: 129 of 129 sets in Lithlad carry that vtable, and every placed object points back to its own at +0x58 (105,946 of 105,946).

A context's object-name table is [ctx+0x68]: 0x800 buckets of 0x18 bytes, bucket = hash & 0x7FF, list head at +0x08; the node is embedded in the object at +0x28 (+0x08 next, +0x10 hash, +0x18 name). 20 of the 129 contexts had a table, holding 105,946 objects; the largest is the main part's.

Matching each table against the shipped registries by object name:

Part Objects in the file In the live table Same name Same rotation Position within 1 cm
Lithlad main 95,205 90,863 90,546 90,510 70,240
fortwallprops 679 635 635 635 635
ol_outlaw 5,545 5,545 5,545 5,455 5,398

Of the main part's names, 4,659 are not in the live table, among them every StartupCommand (*.SC) and some MapCanvas objects (*.MCNV_H); 317 names exist only live (Avian0 .. Avian11, CTP_D_103.CTP ...). The position differences are AI placements moved vertically: 20,211 of the 20,306 moved objects in the main part are AI nodes, ambient spawners and combat teleport points, and in every one of those classes the median horizontal change is 0.0 cm while the median vertical change is -47 to -285 cm (AINodeMenacePlace 6,802 of 6,869 moved, median -181 cm; AISpawnerAmbientNormal 4,343 of 4,920, -150 cm; AINodeRetreat 432 of 432, -285 cm; CombatTeleportPoint 751 of 849, -47 cm). The other 95 moved objects belong to other classes.

The object registry (.objres)

A part's .objres is an OBJR chunk registry:

'OBJR' | u32 13 | u32 hash | u32 block count
block count x { u32 name hash | u32 size | payload }

The chain ends exactly at the end of the section in 1,310 of 1,310 parts. Parts with context-action data carry 14 blocks (724 parts), the others 12 (no CAWR / CADB). Each block hash is StringHash of the name its handler registers with the resource manager; the names sit together in the image at 0x1E6B4E8-0x1E6B6B0, 14 of 14:

Tag Hash Engine name Handler (vf3) Content
KEYF 0x9B011056 ObjRes.Keyframer vt_0x23017A0, 0x4BB804 key-framed motion paths
SFXS 0xBE10C375 ObjRes.SpecialFx vt_0x2301300, 0x4BB9E4 static effect placements
SWFX 0x71F34310 ObjRes.WeatherFx vt_0x2301468, 0x7C692C weather-gated effect points
ONLS 0xACECAC8A ObjRes.ObjNameList vt_0x2301808, 0x4BBB80 group member lists
NLFE 0x9F53FC59 ObjRes.NativeLife vt_0x22F3A08, 0x7C34B0 ambient-life emitters
PFDG 0x22A69B09 ObjRes.PrefabDebug vt_0x22F3B78, 0x4B8554 the prefab tree for the debug viewer
(none) 0xC3685E5B ObjRes.Clutter vt_0x22F3890, 0x4B680C instanced clutter meshes with geometry
SCRB 0xD69D2456 ObjRes.ScriptBind vt_0x230EF30, 0x4B6B58 which script function runs for which object and event
VOLM 0x8A27BAE6 ObjRes.VolumeDB loader 0x7B83B8 convex volume brushes (see Rendering)
CAWR 0xA7ED4C13 ObjRes.ContextLineDB vt_0x2301110, 0x4B6368 context-line owner index
CADB 0x8B7E18E7 cntx vt_0x1FF6B98, 0x501478 context-action lines
GADB 0x29F76277 ObjRes.ObjProps the object property database
(none) 0xB9E81CF3 ObjRes.WorldPhysics which prefab each dynamic object draws, and collision shapes
OBJL 0xAE72B7E7 ObjRes.ObjList vt_0x22C0338, 0x502B8C the object list

Across the 1,310 registries the blocks total 736 MB: ObjRes.WorldPhysics 416 MB, CADB 123 MB, OBJL 84 MB, VOLM 40 MB, GADB 23 MB, PFDG 18 MB, ONLS 14 MB, Clutter 11 MB, SCRB 3.1 MB, NLFE 2.2 MB; the rest are under 0.4 MB. Atmosphere volumes and their VOLM brushes are described in Rendering; the context-action blocks and SWFX have their own sections below.

The object list (OBJL)

'OBJL' | u32 5 | u32 sets | u32 N | u32 class bytes | u32 name bytes | u32 group bytes
0x1C   class names, object names, group names (NUL-terminated string tables)
then   per set: u32 set hash | u32 count | count x 50-byte record (unaligned)
Offset Type Meaning
+0x00 u32 class name (offset into the class table)
+0x04 u32 object name (offset into the name table)
+0x08 u16 the prefab instance that placed the object (offset into the group table; 0xFFFF = none)
+0x0A u32 index of the object's record in the property database
+0x0E f32[3] position, world space (cm)
+0x1A f32[4] rotation quaternion (x, y, z, w)
+0x2A u32 0
+0x2E u32 the authoring id of the object's template (see below; not kept at load)

The table ends exactly at the block end and every offset lands on a string start in 1,310 of 1,310 registries (re-read on five layers and Lithlad's main part, 104,489 objects; the 9,284 layer objects all have unit quaternions).

The loader (vt_0x22C0338 vf3, 0x502B8C) expands each 50-byte record into an 0x50-byte run-time record: +0x08 the name, +0x10 the group offset, +0x14 StringHash(name), +0x18 the property record index, +0x1C the position, +0x28 the quaternion, +0x38 the word at file +0x2A, +0x40 the class name. It builds, per object set, a table of {name hash, u16 index} pairs used to find an object by name. It reads the id at file +0x2E into a local and discards it: nothing at run time keys on it. The id is the authoring identifier of the object's template. Every copy of an object placed through the same prefab carries the same id (one AINodeMenacePlace prefab node: 9,275 copies over 74 parts share 0x2651E0E7; one hanging-light trigger: 8,749 copies over 109 parts), while objects authored directly in a part have their own: over the whole game, 1,247,824 objects carry 167,063 distinct ids, 151,160 of them used by a single object. The id never equals the hash of the object's name (0 of 1,247,824) and stays within one class (two exceptions: 0 on every WorldInitializer and WorldCollision object, and one pair of unrelated objects, as expected from chance collisions among 167,063 random 32-bit values). Most lists hold one set, the world set (hash 0x3BDE767D). Object names carry their prefab instance and their role: Barre_46.DOGS_Fire, MesaE_08.VC_NoHang, HideS_138.FP, pGCarags.spwCaptain. In all ten parts read, the list opens with a WorldInitializer object and then WorldCollision objects at the origin: PhysicsBSP (and LargeAIIgnore in some layers); Lithlad's main part has four (PhysicsBSP, PlayerBlocker, CameraBlocker, LargeAIIgnore).

The object property database (GADB)

ObjRes.ObjProps is a complete GameDB database (version 17) whose root type is ObjectPropertyDB, with the category ObjectDB and records named ObjectRec (see GameDB). An object's OBJL record index selects its record, and every property the object reads at creation comes from it. Records are de-duplicated: objects with identical properties share one (Minas Ithil night01: 3,160 objects, 1,886 records; Lithlad main: 95,205 objects, 21,430 records). In a running game an object keeps its record at +0x50.

Group members (ONLS) and script bindings (SCRB)

Both key objects by StringHash(object name).

What a dynamic object draws (ObjRes.WorldPhysics)

The static placements (below) list only static prefabs. Everything the engine can burn, break, open or animate is an OBJL object, and its geometry and collision are found through this block:

u32 n_objects | u32 n_records
n_objects x u32 hash(object name)   (unsorted)
n_objects x u16 record index
records:  {u32 1, u16 0, f32[6]}                                   no path: world collision
          {u32 flag, u16 length, path, u32 key, f32 cull, f32 half[3], f32 centre[3]}   a prefab (.inst)
then the records' Havok shapes (tagfile TAG0, SDKV20160200, hknpCompoundShape)

Lithlad's main part lists 18,943 objects over 585 records. In Cirith Ungol's main part 12,638 of 12,639 hashes are OBJL object names, all of geometry or physics classes (FireProp 2,491 of 2,491, DynamicObjectGroupBrush 1,382 of 1,382, WorldModel 590 of 590, AINavMeshObstacle 6,397, the world-collision objects). A record's bounds are its prefab's decoded bounds (the orc furnace base: half-extents 18.1 x 8.9 x 22.1 m), and records are per (prefab, class), so a prop and its navigation obstacle use different records of the same prefab.

Motion paths, effects, ambient life and clutter

Static geometry

Placements (WDSC, .wldclnt)

The whole section is read by one handler (vt_0x230A800 vf3 0x6B3510 -> 0x6B3914, then 0x6B3664), in this order; the walk ends exactly at the section end in 1,310 of 1,310 parts:

0x00  'WDSC' | u32 9
0x08  u32 CELLS | u32 total boxes | u32 INSTANCES | u32 total groups | u32 MESH ENTRIES
      | u32 total materials | u32 total polygons | u32 total polygon vertices | u32 STRING BYTES
0x2C  strings: the part's .wldmsh, its materials, every prefab .inst it places
      MESH ENTRIES x {u32 n, u32 mesh path, u32 value, n x u32 material path}   (1 when the part has
                                                                               its own world mesh)
      INSTANCES x 76 bytes (unaligned)
      CELLS x cell
      the trailing block

The header's totals size one allocation for everything the cells hold (Lithlad: 1 cell, 16 boxes, 2 groups, 6 polygons with 24 vertices). An instance:

Offset Type Meaning
+0x00 f32[3] / f32[3] world bounds min / max
+0x18 f32[3] pivot position (world, cm; the base centre of the prefab)
+0x24 f32[4] rotation quaternion (x, y, z, w)
+0x34 f32 scale
+0x38 u32 flags (only "non-zero" is kept)
+0x3C u32 kind (two bits kept)
+0x40 u32 the tag of the prefab LOD group to draw
+0x44 f32 cull distance (cm)
+0x48 u32 prefab path (offset from 0x2C)

The reader 0x1CCB150 turns each into a 0x60-byte run-time record: bounds at +0x00 / +0x10, the pivot at +0x40, the quaternion at +0x4C, the scale at +0x5C, the squared cull distance at +0x3C (a default when the file value is not positive), and at +0x38 the kind in bits 0-1 and "flags non-zero" in bit 2. The +0x40 word selects which LOD group of the prefab the instance draws: the reader passes StringHash(prefab path) and this tag to 0x58A928, which keeps, per prefab path, a small table of the tags placed with it (three inline, then blocks of 14) and returns the tag's index, stored at +0x3A. The tags are the ones the prefab's LOD records open with (see Prefabs): in Lithlad and Minas Ithil 18,937 of 18,937 instances whose prefab ships in the part's own container name one of their prefab's groups; 163 of them use a prefab with several groups and 45 pick one other than the first. Composite prefabs use the same tag on their child records. Lithlad's 12,877 instances use 15 distinct tags, one per prefab for 241 of 242 prefabs.

Lithlad's main part places 12,877 static instances of 242 prefabs (quaternions unit 12,877 of 12,877, scale 0.12-2.03, flags 1 on 12,789; cull distances mostly 60 m, 90 m, 350 m and 4 km). The kind is a property of the prefab (constant for 242 of 242): 0 on 11,659, 3 on 1,210 (cliff-base rocks, torches, fort stairs, doors, pickaxes) and 1 on 8 (a zipline). Layers place few or none (0 to 17 in five read). These are the static buildings, walls and rocks; prefabs that can change (destructible, animated, interactive) are objects instead.

Cells (read by 0x6B3CD0). Each main part has one cell; 351 layers have one, 65 two, 2 more, and 884 none:

u32 boxes | u32 polygons | u32 groups
boxes    x 32 bytes {f32 min[3], f32 max[3], u8, u8, u8, u8, u16, u16}
polygons x {u8 n, n x f32[3]}                     (four vertices in every main part)
groups   x {u32 key, u32 n, n x u32 instance index}

For each group the engine builds two lists of instance bounds, each entry the instance's bounds with its squared cull distance in the fourth component: one of every instance in the group, and one that leaves out the instances of kind 3. A cell's groups cover its instances: Lithlad's cell lists 12,714 instances under key 0 and 163 under 0xD7D4D586 (12,877 in all).

The trailing block (read by 0x6B3664) opens with 13 words, four of which must be 0: A count | first cell of a range | 0 | B count | ? | 0 | total s | total hull references | hull count | ? | 0 | total A-to-A references | 0 (the loader keeps a pointer to the cells from "first cell" on, and the totals size its allocation); then:

hulls x {f32, u32, u32 vertices, u32 n1, u32 n2, vertices x f32[3], n1 x u16, n2 x u16}
A     x {60 bytes, ending with u32 count c1, u32 count c2, i32 cell (-1 none), u8 count s;
         then s x 16 bytes, c1 x u32 hull references, c2 x u32 references to other A records}
B     x {u32 n, u32, f32, i32, i32, n x u32 references to A records}

The engine computes each hull's bounds from its vertices. Every main part has one or two A records and one B record; Barad-dur and the Black Gate have no hulls, the others 86 to 207. 1,217 of the 1,302 layers carry only the empty 52-byte header, and 85 carry records.

Prefabs (PRFB, .inst)

A prefab's baked geometry; static world geometry is never .mesh (prefab .mesh files are the destructible and animated pieces). 'PRFB' | u32 8 | u32 hash | ... with header words for draws [4], materials [5], child prefabs [6] and vertex formats [9], then:

A layer carries the prefabs it places as PRFB sections of its own container (Minas Ithil's sabotagecages_00 carries guard_caragors.inst and three footprint-tracking prefabs).

World meshes, texture arrays, terrain and vegetation

World objects in memory

Layout

Read on every object of the 20 named contexts in Lithlad (105,946 objects):

Offset Type Meaning
+0x00 ptr vtable: exactly one per class (142 classes, 142 vtables)
+0x28 node the name-table node: +0x08 next, +0x10 hash (StringHash(name), 105,946 of 105,946), +0x18 name
+0x40 char* the registered name (equal to the node's, 105,946 of 105,946)
+0x50 ptr the object's ObjectRec in its part's property database (a GameDB record on 105,383; null on 563)
+0x58 ptr its world context (105,946 of 105,946)
+0x60 node the update-list link (see Threads and the frame)
+0x78 ptr its runtime class entry
+0x80 ptr render component (set on 8,457)
+0x88 ptr time source (see Engine core)
+0x98 ptr physics body (pieces vector at body +0x08 .. +0x10)
+0xAC f32[3] position (cm)
+0xB8 f32[4] rotation quaternion (x, y, z, w): unit on 105,946 of 105,946
+0xC8 f32 scale
+0xD0 f32 update countdown
+0xD4, +0xD8 u32 copied from the create parameters +0x30, +0x34
+0xDE u8 1 = the name was copied, 0 = it points into the part's string table
+0xE2 u8 flags: bit 0 visible, 0x0C solid (FireProp 0x0F on 4,077 of 4,129)

Classes. The runtime class entry is 0x38 bytes in a heap array: +0x00 the static class descriptor, +0x18 StringHash(class name), +0x20 the class name, +0x30 align << 32 | size (WorldInitializer: 0x800000100, alignment 8 and size 0x100). The static descriptor (in .idata) holds +0x00 the name, +0x08 the base class's descriptor, +0x10 flags, +0x18 the constructor, +0x20 the destructor, +0x2C the instance size and +0x30 the alignment. A shape-validated sweep of the image finds 433 descriptors with 433 distinct names (431 in .idata, 2 in .xtext; 121 constructors lie outside .arch). 320 classes descend from BaseClass, 256 of them from GameBase (0x120 bytes, constructor 0x7DCC78), and 64 from BrushBase. The image also lists the static descriptors in one place: the leaf function 0x19CBDFC stores 435 descriptor pointers, one after another, into the array 0x2A61A30 .. 0x2A627C0 (stride 8).

Class vtable Size Constructor Base
WorldInitializer 0x1DB5E28 0x100 0x481F9C BaseClass
WorldCollision 0x1DCD4A8 0xE8 0x7C6208 BaseClass
VolumeController 0x1DB0340 0x148 0x47DD1C0 GameBase
StreamingStamp 0x1FDE810 0x1A8 0x1C2CD14 GameBase
DynamicObjectGroup 0x1DBD300 0x238 0x5BD384 GameBase
DynamicObjectGroupBrush 0x1DBD570 0x1E8 0x4875F50 GameBase
DynamicObjectGroupState 0x1DBCAE8 0x3C8 0x5AF74C GameBase
FireProp 0x1DCA878 0x240 0x7834D4 ParticleSurface (< WorldModelBase)
WorldModel 0x1DC7368 0x150 0x1A2AE74 WorldModelBase
TeleportPoint 0x1DC9C38 0x148 0x767FC8 GameBase
AINavMeshObstacle 0x1DCB420 0x128 0x49326F0 GameBase
AINodeMenacePlace 0x1DB0478 0x2E8 0x2BFB9C
AISpawnerAmbientNormal 0x1DC5800 0x1C8 0x48CE4A0 AISpawnerAmbientBase
LightCube 0x1FE1148 0x168 0x6FBF2B0 LightBase
Sound 0x1DBBE78 0x180 0x4861DF0 GameBase
SpecialFX / SpecialFXSeq 0x1DC13D8 / 0x1DC1530 0x178 / 0x280 0x655868 / 0x6558FC GameBase
CombatExplosion 0x1DCA058 0x178 0x775FA8 GameBase
Group 0x1DC9D50 0x140 0x769064 GameBase
ScriptRelay / ScriptObject 0x1DA9218 / 0x1DCAB18 0x170 / 0x128 0xFCED4 / 0x784DD4 GameBase
RemoteScript 0x1DBFDA8 0x140 0x4889000 GameBase
TriggerSpatialDims / TriggerSpatialShape 0x1DC1660 / 0x1DC1228 0x1D8 / 0x1E0 0x65599C / 0x65575C TriggerSpatialBase
KeyFramer 0x1FAEB70 0x318 0x6DB7CA0 GameBase
AtmosphereVolume 0x1FDEFB8 0x160 0x6F9B700

The most numerous live classes in Lithlad were AINavMeshObstacle 11,588, AINodeMenacePlace 7,249, FireVolume 7,189, DynamicObjectGroupState 6,251, ScriptRelay 5,993, AISpawnerAmbientNormal 5,002, Group 4,628, FireProp 4,129 and VolumeController 3,882.

Position. +0xAC is the logical position for world objects; prefabs are authored with their pivot at the base centre. Characters share the transform offsets (see Characters).

Creating, binding, moving and removing objects

All of this runs on the main thread.

The object server

The object server [base+0x26FF058] (vt_0x22C0B28) is the world-object factory:

Slot Function Role
vf4 0x7AAB6C -> 0x43DBC8 the time-sliced object-set streamer
vf5 0x1F6EA0 "busy" (the update walk is skipped while it reports busy)
vf8 0x43D630 CreateObject(params): requires a class at params+0x40, copies the parameters (0x43D680), calls the creator, then the new object's vf21 (+0xA8, post-create)
vf14 0x502034 find an object by name in a context

The creator 0x1CD454 (also called by the part loader 0x11B4EC0 for every object a part streams in) requires an object set at params+0x1A0, allocates the object through the class holder at params+0x40, runs its ReadProps (vtable +0x60, which pulls every property from the ObjectRec), builds the transform from the parameters into +0xAC, registers the name, links the object into its context, builds its components (vtable +0x68) and activates it (+0x80, +0x70, +0x48). A part's loader creates every object first and post-creates each afterwards.

CObjectCreateParams (defaults 0x43DAA0):

Offset Meaning (default)
+0x01 flags, copied to the object's +0xE2 (0)
+0x04 position (0, 0, 0)
+0x10 scale (1.0)
+0x14 rotation quaternion (0, 0, 0, 1)
+0x30, +0x34 copied to the object's +0xD4, +0xD8 (0, 0xFFFF)
+0x40 the class holder (copied to +0x78)
+0x168 inline name
+0x188 name pointer (0 = use +0x168)
+0x198 the ObjectRec (copied to +0x50)
+0x1A0 the object set, that is the world context (copied to +0x58)

Names

The name registrar 0x4DBF08 either copies the name (and sets +0xDE to 1) or keeps a pointer into the part's strings (+0xDE = 0), hashes it into the node at +0x28 and calls the context's vf10 0x4DBF78, which picks the bucket (hash & 0x7FF, 0x4DBF84) and appends the node at the tail (0x115ED3C, which asserts "Error: Adding a link that is already inside of a list" only when the node itself is already linked). Duplicate names are legal: nothing checks the name. The lookup 0x4DC078 compares the hash and then the name case-insensitively, walking from the tail, so the newest object of a name answers.

Render and collision are bound by name. The component build (vtable +0x68 -> 0x333430) asks the context's vf5 0x3334F0, which looks the registered name (+0x40) up in the context's resource table (ctx+0x98). An object created under a name its part does not list gets no render component and no physics; created under an existing object's name it receives that object's model and collision. Because the newest object answers a name, the members of a prefab created after the original members bind to each other during post-create.

Binding from script

A script's Create does not construct anything: the objects already exist in the part before any script runs, and scripts bind to them by name. The <Class>.Create(scriptId, name, b3, relative) stubs of 117 of 125 script classes share 0x5013A8 -> 0x501B64, which composes the name (a #. prefix or relative prepends the prefab instance's name) and calls the object server's vf14 with the scriptId's own context; the lookup is not given the class. A missing name or a class mismatch returns Class.None; ScriptObject.Create is a function of its own, 0x501CAC. A scriptId is {PrefabName, PropertyName, Instance}; Instance is a weak handle (prefab instance at payload +0x48) that becomes null when the prefab's part streams out.

Moving

GameBase:CopyXForm(A, B) 0x1823B70 is SetPosition(A, B+0xAC) 0x1454EC followed by SetRotation(A, B+0xB8) 0xEF690; render state and spatial structures follow the setters, and it works across contexts. Collision follows ownership: the setter path 0x274C3C forwards to the physics body at +0x98, so only pieces the object owns move. 3,382 of 3,491 FireProps own no pieces (their collision is baked into the part's static PhysicsBSP), so a moved bench or barrel stack keeps its old collision; DynamicObjectGroupBrushes (607 of 634) and cage brushes (vt_0x1FA3C00, 84 of 84) own theirs and stay solid where they go. A destructible prop is a prefab of sibling objects (brush, explosion, effects, nav obstacle, volume controller): moving one member leaves the others behind.

Removing

Objects live and die with their part's context. When the part streams again its objects are created again from the file, so a moved object is back at its authored transform and an object created into that context is gone. Nothing about placed objects is saved. A class's destructor is at +0x20 of its static descriptor.

Dynamic object groups

Cages, nests, barrels, gates and traps are dynamic object groups: a DynamicObjectGroup object (the root) with DynamicObjectGroupBrush members (the breakable or interactive geometry), DynamicObjectGroupState members named after their states (Barre_46.DOGS_Fire, WaspN_03.DOGS_Shoot) and the prefab's effects. The brushes bind the group's composite prefab (caragorcage, waspnest, ghulcloset: no draws of their own) and sit at fixed offsets from the root (35 of 35, 53 of 53 and 226 of 226 in those three); the drawn art is the prefab's children at the root's transform.

The data is in four GameDB categories:

Category Records Fields
DynamicObjectGroup/Setup 103 States, VisibilityMaxDistance, Tags, ValidateStateTransitions, ValidateBrushDamage
DynamicObjectGroup/StateTransition 101 StateToEnter, ActiveQuestList, IncludedParentStateTypeList, ExcludedParentStateTypeList, IncludedBrushDestroyedAttackCategory, ExcludedBrushDestroyedAttackCategory, ChildState, ChildrenInStateMin
DynamicObjectGroup/StateType 77 none: bare state tags (Alive, Destroyed, Hidden ...)
DynamicObjectGroup/PlacementID 16 none: fortress slots (GateBrow, WallTop, TowerFace, Keep ...)

A transition is a guarded rule: the attack categories and parent states it accepts, and the state tag it enters. The state tags carry no data; what a state shows is the part's DynamicObjectGroupState objects for it and the scripts bound to them.

At run time a group's current state is DOG+0x188 (the state object; its +0x180 is the StateType record), and DOG+0x190 holds its 32 state links. The group's post-create 0x4871190 collects the links but does not enter a state; a group streamed in with its part receives its initial state afterwards. DynamicObjectGroup:Script_SetState is 0x5ADF0C(dog, StateType record): when the ready flag +0x22C is set the request is queued at +0x160, otherwise it is applied at once (0x5AEABC). A brush's current state object is brush+0x148; while it is null, hits on the brush are routed nowhere. A brush's ReadProps does not fill the visibility flags (a FireProp's does), so a brush created without them is invisible.

Placed lights and sounds

Light and sound objects read their colours and radii from their property records at creation and keep them in the object:

Class vtable Live fields
LightCube, LightSpot, LightDirectional, DecalLightDirectional 0x1FE1148, 0x1FE0998, 0x1FE0E98, 0x1FE0D40 +0x138 LightColor, +0x13C SpecularColor, +0x140 LightRadius, +0x144 IntensityScale
Sound 0x1DBBE78 +0x130 OuterRadius

In Lithlad the live values equal the decoded property records: LightColor, SpecularColor and IntensityScale 950 of 950, LightRadius 903 of 903, Sound OuterRadius 1,326 of 1,326 (each control read 0). 2,810 lights were matched by name; 45 sat more than 1 cm from their decoded position. 3 of those sit exactly where the patch .objres places them. The other 42 are lantern lights (LightHangOrc*_l.LC_Out01, light_NN.LC_Out01) displaced 2-22 cm horizontally (vertical median 0.4 cm, none downward), and 16 of them moved between two reads a minute apart.

Context actions: traversal markup

Where Talion can hang, pull up, vault, rail-run or leap from a mount is world data: context-action lines owned by VolumeController, WorldInitializer and StreamingStamp objects, built into a world grid on the ContextActionBuilderThread (see Threads and the frame) and queried by the movement behaviour graphs. ContextActionDB (ContextActionDB.cpp, initialised by 0x1B43A90, vt_0x23010A8) is [GM+0x6DA0]; it registers the cntx resource type.

On disk: CADB and CAWR

CADB (read by 0x7CB194 for the header and vt_0x1FF6B98::vf3 0x501478 for the body; exact in 724 of 724 parts, all version 3):

'CADB' | u32 version | u32 nA | u32 nL | u32 nP | u32 nV | u32 nO          (0x1C bytes)
nA x u32   action palette: hash of a Character/Models/ContextActions record
nL x 0x34  line   {f32 p0[3], f32 p1[3], f32 normal[3], u128 mask}   mask bit i = palette action i
nP x 0x10  plane  {f32 normal[3], f32 d}
nV x 0x20  volume {f32 min[3], f32 max[3], u32 first plane, u32 plane count}
nO x 0x34  owner  {u32 key, f32 min[3], f32 max[3], u32 first line, u32 line count,
                   u32 list-1 first volume, u32 count, u32 list-2 first volume, u32 count}

Across 724 parts: 7,883 of 7,886 palette entries name one of the 124 Character/Models/ContextActions records (fields Group, DebugColor, AdditionalReferencedActions; so a global mask fits 128 bits); 2,209,930 lines, every one owned exactly once and inside its owner's box; 314,834 planes (all unit); 50,668 volumes; 32,104 owners, whose key is the hash of an OBJL object name (32,104 of 32,104: VolumeController 31,803, WorldInitializer 288, StreamingStamp 13), sorted. Lines are not level in general (1,551,979 slope) and 68,951 normals have a vertical component. The busiest actions by line count: Edge_Hang 997,374, Edge_Pullup 388,445, Edge_NoHang 350,260, Block_Movement 326,177, Block_Movement_RailRun 219,953, Edge_Hang_Dangle 216,272, Hurdle 94,327, Edge_MountJumpTo 93,946, VaultOver_Mount 91,633, Edge_NoTrollJump 83,358, Rail_Running 56,365.

CAWR (ObjRes.ContextLineDB) is the owner index: 'CAWR' | u32 1 | u32 n | n x {u32 key, u32 asset, u32 key} (exact in 724 of 724). key is the hash of the object name; the middle word is 0 on disk and is filled at load with the hash of the cntx resource that holds the owner (2,777 of 2,777 live).

Loading

A loaded cntx object is 0x80 bytes (0x7CD444): lines at +0x20 (stride 0x38: the mask at +0x00, the vectors from +0x10), planes +0x30, volumes +0x40, owner keys +0x50, owners +0x60, the asset's hash at +0x1C, and a node at +0x08 in the database's list DB+0x68 (vf5 0x79A948 asserts "Duplicate asset being loaded."). vf4 0x41E564 rewrites every line mask from palette bits to global bits: the database's action table (DB+0x10 count, 124; DB+0x18 entries of 0x28 bytes {u32 record hash, u128 own bit, u128 closure}, sorted, "Improperly sorted database records!") closes each action over its AdditionalReferencedActions to a fixed point, and a line stores the OR of its actions' closures. A query for one action therefore also finds every line whose action lists it.

Live, for the four loaded parts with a shipped source, 163,048 of 163,048 lines equal the file in geometry and in the predicted global mask (the unconverted file mask equals the live one in only 806 of 161,821), as do 2,364 of 2,364 planes, 344 of 344 volumes and 2,091 of 2,091 owners.

Registration

Register 0x2B9E9C(DB, &handle, object, cut group) is called by three classes: VolumeController (vf21 0x2B9758), WorldInitializer (vf21 0x7A53B8) and StreamingStamp (vf16 0x1C2CF04). It resolves the object in two steps: CAWR (list DB+0x98) binary-searches the hash of the object's name (0x2BA01C), then the owner is found in the cntx object named by the middle word (list DB+0x68) by its key (0x2BB7B8). Live, 3,889 objects carried the VolumeController vtable, 3,748 of them named *.VC* (.VC, .VC_NoHang, .VC_Pullup, .VC_Side, .VC_Alive, .VC_Destroyed) and the rest other markup names; 126 were WorldInitializers and one a StreamingStamp (SS_00).

A registration row (DB+0x50, 0x38 bytes) is {u32 A, u32 B, ptr handle, u8 active +0x10, f32 pos[3] +0x14, f32 quat[4] +0x20, u32 cntx hash +0x30, u32 owner key +0x34}. A is a serial from DB+0x48. B is the cut group: the hash of the object's ContextCuttingGroup property, one of the 11 field-less Character/Models/ContextActionCutGroup records (PropKit, WallKit, WallFlair, GateKit, GateFlair, KeepWallFlair, KeepWallKit, TowerFlair, TowerKit, WildFlair, WildTower); only VolumeControllers pass one. VolumeController::SetEnabled 0x5B21A8 (a script function; the StartEnabled property seeds it) calls 0x2BA65C, which writes the row's active byte and sets DB+0x4C, the rebuild flag.

Building the grid: KILL and CUT volumes

QueueRebuild 0x2BB1C8 files each row by its transform (full rotation, translation only, identity). The build 0x2BA9F8 first instances every owner's two volume lists (planes transformed by 0x2BB01C, {A, B} copied, covered cells flagged), then inserts every line through 0x257C70:

Measured in a running game (two reads with the world re-streamed between them; 186,023 and 184,993 lines): no line lies more than 1 cm inside a volume that applies to it, for 106 kill and 1,051 cut volumes. Control: the same volumes moved 300 cm along x would contain 31 kill and 1,890 cut lines.

The world grid

[DB+0xB8]:

Offset Meaning
+0x00 / +0x0C f32 min[3] / max[3]
+0x18 f32 inverse cell size[3]
+0x24 u8 cell counts[3]
+0x28 f32 minimum piece length squared (625)
+0x30 the inner tables
+0x38 u32 line count

The cell target is 2000 x 8000 x 2000 cm (0x267A448 .. 0x267A450). Lithlad's grid spans 1,120 x 244 x 1,220 m in 56 x 4 x 61 = 13,664 cells of 2000 x 6105 x 2000 cm (the extent divided by the target, rounded up, on all three axes) and holds 184,993 lines. The inner tables: +0x10 cells (0x28 bytes: u32 index, u8 x, y, z, u8 has kill +7, u8 has cut +8, a run vector at +0x10), +0x20 owner instances (0x20: u32 A, u8 active +4, run vector +8), +0x30 kill volumes, +0x40 cut volumes (0x38: box, u32 plane count +0x18, planes +0x20, u32 A +0x28, u32 B +0x2C, u8 cell range[6] +0x30), +0x50 world planes (16 bytes {n[3], d}, inside when n . p - d <= 0). A world line is 0x40 bytes: u8 flags (bit 0 = owner active), u32 home cell | bit 31, u128 global mask +8, f32 p0 +0x18, p1 +0x24, normal +0x30.

The query

ContextActionDB::Query 0xFEAFB0(DB, AABB box, u128 any, u128 all, u128 none, u128 not_all, results ) locks DB+0x20 and runs 0xFEAE40 over every cell the box covers (0xFEB3B0) and every run in it (0xFEB070). It returns each active line once (de-duplicated by its home cell) for which mask & any != 0, mask & all == all, mask & none == 0 and mask & not_all != not_all (a zero mask skips its test) and whose segment meets the box (slab test 0x5AA774). Results are {u32 n, n x 0x38 {u128 mask, p0, p1, normal}}, at most 256. Running the engine's own 0xFEAE40 in an emulator against live memory, on boxes around the player and on random lines with five or six mask sets, gave the same answer as this model in 96 of 96 and 80 of 80 queries; returning inactive lines would have changed the answer in 18 of 19 and 9 of 9.

The callers build the masks from the action table's own bits (0x41F954): any from a node's GameDBContextAction and GameDBContextAction_OR, all from _AND, none from _NOT_OR, not_all from _NOT_AND. The seven callers of 0xFEAFB0 are behaviour-graph nodes (see Animation): Predicate.DetectedEdge (DetectEdgeData 0x1C6EF90: fields Direction, VerticalOffset, Radius, Height and the five action lists; box = position + (0, VerticalOffset, 0) +- (Radius, Height / 2, Radius); defaults -104, 100 and 10 cm), Predicate.FoundRailing / Modifier.FindRailing (RailingData 0x1C700D4), Modifier.MoveWithBrush (vt_0x2303CD8::vf11 0x1C46DE0), Predicate.FoundContextActionBasic (0x41F39C), Character/Models.LockPhysicsContextActionRecords (0x41E70C -> 0x41FBF0), 0x1C6FA58 (from 0x1BE703C, R_Foot) and 0x3ADC94.

Weather-gated static effects (SWFX)

ObjRes.WeatherFx places effects that play only under a given forecast. It is the empty 16-byte header in 1,300 of 1,310 parts; the ten others are seven region main parts (not Barad-dur), Minas Ithil's t1prefallprops and t1postfallprops, and Gorgoroth's fx_environment (10 types, 51 points).

'SWFX' | u32 1 | u32 n_types | u32 n_points
n_types  x {u32 record hash, u32 first point, u32 END point}     ranges tile [0, e0), [e0, e1) ...
n_points x f32 position[3]
n_points x f32 quat[4]

The ranges tile the points in 10 of 10 blocks (79 of 79 types; quaternions unit 8,110 of 8,110). The type hashes are records of Weather/FX/Static (the category cached at 0x2700050; 32 records, 28 of 28 distinct hashes resolve): Ledge_1m / 3m / 5m / 10m / Corner, Curtain01_20m, Curtain02_40m, Curtain_CeilingFill01 / 02, Lightning_Far0N, Lightning_Near0N, Lightning_VOL_Far0N, Lightning_VOL_Near0N, Desert_AtmoDust_Lrg01, ISLD_Pollen_5m / 10m. A record has a BubbleRadius (cm: Ledge_1m 2,000, Curtain01_20m 15,000, Desert_AtmoDust_Lrg01 45,000, Lightning_VOL_Near 125,000, Lightning_Far 300,000) and Data = _StructuresWeather.StaticFXData {Predicate -> _StructuresWeather.StaticFXPredicate {Forecast -> Weather/Predicates/Forecast/<RainLight | RainMedium | RainHeavy | ...>}, Effect (fx\weather\rain\edgedrips\rainlight_edgedrip01_1m.fxp, fx\weather\lightning\static_lightning_rainheavy_far01.fxp)}: play this effect here, under that forecast, within the bubble radius. Lithlad's main part has six types and 1,646 points (Ledge_1m 590, Ledge_3m 323, Ledge_5m 242, Ledge_10m 6, Ledge_Corner 421, Desert_AtmoDust_Lrg01 64).

The rain drips sit on the climbing ledges. In Lithlad the nearest live context-action line is within 25 cm of 538 of 590 Ledge_1m points (median 5.1 cm), 296 of 323 Ledge_3m, 233 of 242 Ledge_5m, 6 of 6 Ledge_10m and 310 of 421 Ledge_Corner; with every point moved 3 m sideways the counts fall to 34, 23, 15, 0 and 24. The dust points (64) lie a median 9.7 m from any line.

Collision and navigation

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.