Characters, the player and the camera

The character object and its key fields, the player, the camera pipeline and what the screen actually reads, near-camera dissolve, the character controller with its collision layers and trigger volumes, Wraith World and the wraith companion model, weapons and holders, runtime material slots, render IDs, mounts, and Talion's material controller.

The character object

Every character - the player, NPCs, creatures - is a CCharacter. AI characters are the NPC class (vt_0x1FB3640, 0x3180 bytes; see AI); the player is a CPlayerObj (vt_0x1F96818).

Offset Meaning
+0x48 (FactionMember) a captain's spawned character is FM+0x48
+0xA0 the body pose
+0xAC position, 3 floats in cm (Y up)
+0xB8 rotation, a unit quaternion (x, y, z, w); forward = q * (0, 0, 1)
+0xC8 scale
+0xE2 bit 0: visible
+0x110 the skeleton object; [[char+0x110]+0x1C0] is its model animation instance (see Animation)
+0x120 the behaviour-interface base subobject
+0x22E0 the face ([[[char+0x22E0]+0x60]+0x60]+0xA0 = the face pose)
+0x2340 the visual
+0x2348 the visual component, which holds the inventory
+0x23B8 follower byte
+0x2410 the alignment (Character/Alignments)
+0x26A8 the combat object (see Combat)
+0x2BF8 u32 index into the sighting matrix (see AI)
+0x2C21 set while a queued teleport is pending
+0x294C forced dissolve mode; +0x298C / +0x29A0 target and smoothed dissolve

The player is [base+0x2797808], the first entry of the player list [base+0x2A34C68]; the client shell [base+0x26FFA98] holds it at +0x888. Player-only fields: +0x2F10 AI-spectator invisibility, +0x2F88 the movement chain, +0x2F90 the camera / dissolve object, +0x2FA0 the combat component, +0x2FA8 vision mode, +0x2FC0 the scene component, +0x3420 the screen camera copy, +0x2578 the mount being ridden. A bone's world position comes from the pose and the bone hashes: a skeleton object's runtime skeleton [[[skel+0x10]+0x18]+0x10] holds +0x40 the bone count and +0x78 the bones' name hashes in pose order (Talion 128 bones, the face rig 241).

The camera

owner = [[[base+0x26FFAA8]+0x6CB0]+0x6728 + i*8]   (vt_0x1FD99F8; the local player's entry has +0xDEA8 set)
owner+0xDEA8 -> the camera modifier data (md)
owner+0x5F0  -> the modifier list (sentinel; +0x5F8 first node; node = modifier + 0x10, next = node + 8;
                modifier +0x08 owner, +0x20 its GameDB record, +0x28 state, 2 = finished or skipped)
md+0x66C position, +0x678 rotation quaternion, +0x690 / +0x694 horizontal / vertical FOV (radians)
md+0x650 / +0x688 the previous frame's transform / lens

A frame (0x2FC6B8): 0x51BF3C(md, dt) prepares from the previous camera and the focus; 0x51BCA8 runs each modifier's vf7 on md, validating after each; the finalizer 0x51BE7C stores the result into the output object md+0x768 (+0x3A4 transform, +0x3C0 start transform, +0x3F4 / +0x3F8 lens). The screen reads that stored copy, not the live md: 0x2FEA2C -> 0x51AC00 -> 0x51AC30 -> 0x4E0CFC copies it to player+0x3420, which drives the view, camera transitions and the lens. The live md is the camera's state for the next frame. The gameplay camera Camera/Behavior::Camera_Primary has 12 modifiers (Adjustables, Lens, SetRoot, SmootherBegin, Distance, OrbitControl, AvoidObstacles, Pitch, Yaw, Roll, Smoother, Collide); only the collision modifier (vt_0x1DB5FE0) calls SetCameraPos 0x51C394. Presentation cameras are the *_BS / *_BR modifier families.

The debug camera manager [base+0x2A86860] (vt_0x22F4A90, a state machine; built at client start by 0x17CD2EC, read by the screen copy 0x4E0CFC) takes a mode from DebugCamera.FreeCamControls (handler 0x17CFF54: "Cinematic", "ClipMode" or "Marketing" stored as 0 / 1 / 2 at +0x118). DebugCamera.Track (0x17D0074) follows a named object.

Near-camera dissolve. Camera/CharacterDissolve (34 records: hide and fade radii and lerps) is set by camera behaviours (setter 0x1567E8, controller +0x240) and by presentation elements (1,380 of 3,548 set Dissolve.Settings, controller +0x248). 0x6C4E60 builds two capsules from the camera along its forward axis; the classifier 0x30A368 -> 0x309128 -> 0x308BE0 returns show, fade or hide from the capsules against each body's capsules (a camera inside a body forces hide), unless the character's forced mode overrides it (3 volume decides, 0 always show, 2 always hide; set by presentations' HideActors / ExcludeActors and HideAllCharacters 0x19B43B4). The result drives the material constant k_fDissolve (0x3093F4); at 1.0 the visible bit clears. Every result reads one table on [player+0x2F90] (vt_0x1F67370): +0x258 show {0.0, 0.2}, +0x260 hide {1.0, 0.1}, +0x268 fade {0.76, 0.2}, copied once when the player is initialised.

The character controller

ctrl = [[[player+0x2F88]+0x08]+0x28] (the movement object vt_0x22F7738 sits on the way; one controller per character, 94 live in one scene). The controller is vt_0x1DB4DE0: +0x38 owner, +0x40 the hknpCharacterProxy (vt_0x1DBDA20: +0x3C filter info, +0x80 position in metres, +0xB0 velocity in m/s), +0x81 support state (2 = standing), +0xE4 collision filter, +0xEC gravity on. Slots: 5 SetPosition (cm), 6 SetVelocity, 8 Step (-> 0x399F18 integrate), 11 SetGravityEnabled 0x3F1900, 12 IsGravityEnabled, 13 refresh support, 20 SetCollisionFilter 0x3F14B4 (writes the controller's and the proxy's filter), 24 CopyFrom. The controller object is replaced during play (across a death or respawn), so it is reached through the chain on every use. The character controller's gravity (Client/PlayerMovement: -2000 at the movement object's +0x54) is separate from the rigid-body world's.

The proxy. The hknpCharacterProxy holds its shape at +0x28 (an hknpCapsuleShape on 17 of 17 live proxies) and the physics world at +0x40. +0x50 .. +0x8F is its transform: three rotation columns, then the position at +0x80 (w = 0), which equals the character's +0xAC / 100 (0.0 cm apart on the player). +0x90 / +0xA0 are the minimum and maximum corners of a box around the shape, relative to the position (0.73 x 1.89 m up to 0.99 x 2.56 m for orcs and humans, 3.5 x 6.16 m for the largest creature in one scene). The integrator 0x399F18 (controller slot 8) turns the velocity into the step's displacement, +0xC0 = +0xB0 x dt (dt from the step information +0x08), casts the capsule from +0x80 to +0x80 + +0xC0, and keeps the velocity it started from in +0xD0, so a value written to +0xB0 is what the next step moves by. +0x120 is the up vector (0, 1, 0). The settings that follow are the same on all 17 proxies, so they are not per character:

Offset Value Use in the integrator
+0x130 0.05 added to +0x134 for the cast tolerance; also multiplied by 0.5 in a later step
+0x134 0.1 added to +0x130 for the cast tolerance
+0x138 10.0
+0x13C 50 (u32) added, with 10, to the manifold size +0x20 to size the plane buffer
+0x140 10.0 passed to the solver as the w of its input vector
+0x144 / +0x148 100.0 / 100.0
+0x14C 1.0
+0x150 10 (u32) the bound of the cast loop

Collision layers. The world's collision filter is the object at [hknpWorld+0x480] (vt_0x22C2530, an engine subclass of hknpGroupCollisionFilter): two bodies collide when table[layerA] & (1 << layerB) (table at filter +0x24, symmetric). The player's proxy is on layer 7 (row 0x0030003D); every AI proxy is on layer 1 (row 0x00000001, the world only). Hit capsules use layer 6. Every character also carries a sensor body on layer 28 (filter 0x1C) exactly at its position. Trigger rows: 27 = 0x10000001 (layers 0 and 28), 28 = 0x48000001 (0, 27, 30), 30 = 0x10000001 (0, 28). The world's body array is [hknpWorld+0x28] (15,360 entries of 0xB0: +0x30 position in metres, +0x60 shape, +0x6C filter info with the layer in bits 0-4, +0x70 id); about 4,300 bodies are live in a region.

Trigger volumes. Script triggers are Havok trigger bodies: the trigger class table at 0x2A61E48 (TriggerSpatialBase constructor 0x1A52C88, TriggerSpatialShape 0x65575C / vt_0x1DC1228, TriggerSpatialDims 0x65599C / vt_0x1DC1660). TriggerSpatialDims vf45 0x3DAD68 creates the trigger through the physics system on layer 27 (31 for corpse triggers); TriggerSpatialBase vf21 0x3DA8AC filters by script tags, the player, excluded character types, mount and rider. Layer 27 meets only layers 0 and 28, so triggers react to characters' sensor bodies. The levels place out-of-environment kill and teleport volumes this way (for example OOEKillVolumes / OOEKV_* under Minas Ithil and the fortress walls' *.OOETSS / *.OOE_SO script objects, which teleport the player back): a character under the world dies by trigger, not by height. The kill itself is CCharacter::Die 0x19B3EE0 (100,000 damage, the float at 0x202ECBC; ApplyDamageInternal 0x2E4BE8). The engine's own world extents (+-5 km / +-3 km at 0x26C6588) only assert.

Height predicates. PredicateHeightAttackerIncluded (evaluator 0xEDD5F0) measures the height above ground with a ray cast from the character's position down to 100,000 cm (0x77A6D0) and tests it against a Combat/Predicates/Height range (On_Ground 0 - 50 cm, Jumping 40 - 5,000 cm). Every ground ranged attack carries On_Ground; 1,320 compiled attacks use it, 318 of them AI. An attack's AnimationData.EnableCollision / EnableGravity (combat +0x683 / +0x686) override the movement component's collision scale (0x41BEB4) and gravity (0x41C558) while it runs. The engine also has a native spectator movement state (NativeState.SpectatingState, SpectatorSpeedMultiplier1/2 10 and 0.1).

The player clock [[base+0x2702548]+8]+0x18 stops in the pause menu and while the game is unfocused; [base+0x2702590] keeps running.

Wraith World and the wraith companion

Wraith World is a weapon mode: Combat/PlayerWeaponModes/VisionMode -> Client/VisionMode::VM_Wraith; 0x6A6CE4 -> 0x6A7E90(vision = [player+0x2FA8], mode): +0x28 the mode record, +0x44 active, an amount that eases from +0x48 to the target +0x4C over the mode's 1 s ramp (ShaderVisionMode, read by 0x115EF98). The game changes nothing on the player's own pieces: Talion's solid shaders cut his body away with the same reveal map the wraith draws with (tRevealMapArray, k_fRevealValue). Orc and human shaders carry only k_fDissolve.

The wraith form is the player's companion model (Combat/CompanionModel): a copy of the player (CombatCompanionModelInstance vt_0x1DBD198; manager vt_0x1DCC8F8 at [[player+0x26A8]+0x80]: +0x38 body instance, +0x60 head instance, +0x68 companion record) built from a SimpleModel. Wraith moves add echoes that clone it (Combat/Echo::QuickThrow = three PC_Echo with CopyPlayerCompanion, material set Echo). Monolith's wraith pieces carry a default set of translucent wraith materials (wraith_game.fxo, fed the character's own textures) plus depth proxies (wraith_proxy.fxo) and an Echo set (wraith_riddick.fxo).

Material sets are compiled. 0x1B6190C builds a cache at start-up at [GM+0x6DD0]+0x18: one entry per Model record of 0x48-byte compiled sets. Editing a record later changes nothing there, and asking for a set index past an entry's count returns null.

Visuals, pieces and weapons

A visual (vt_0x1DA9610) has a spec at [visual+0xB8] (23 piece slots at +0x28) and a skeleton at [visual+0x90]. SetPieceSlot 0x4C62EC(spec, slot, piece, armour design, visual) swaps one slot live (the engine's live armour equip). A SimpleModel builder (0x4C5194) sets body, head, hair, accessories, flair and helmet from its entry; piece slots 8-12 come only from items. Pieces follow the host's pose through 0xF5A290 (pieces at skel+0x108 into the host's pose list); visibility 0x1BEE8C (skeleton, flag) is the engine's "hide the base model" (the material controller's HideBaseModel transform calls exactly that); 0x58BCF0 sets the material-set type.

Weapons are separate objects (vt_0x1DAA9D0: wielder +0x20, material controller +0x60, an inline mesh at +0xB0) drawn by per-slot holders [vc + 0x38 + slot * 0x28] (vt_0x1DB1F68: +0x140 the item, +0x148 / +0x150 the built models, +0x1F1 shown). A weapon skeleton has three bones (root, handle, tip). 0x3FD228 builds only an empty holder; an equipped, drawn weapon is rebuilt by the engine's refresh 0x45ABE4 per holder: 0x3FD21C (hide: destroys every model) then 0x3FD204 (show: builds from the item as it is now); the equip setup (vf35 0x3FCF70) does the same pair. The weapon's look chain: Inventory/EquippedWeaponData.Model -> _StructuresInventoryItemModel.Models -> _StructuresInventoryModel.ModelRecord -> a Model -> its default material set. Equipped weapon slots are vectors at vc+0x4D0 + i * 0x18 (0 sword, 1 dagger, 2 bow); an item's +0xA9 is "equipped", +0x40 its equipped-weapon data and +0x2C its holder index. The weapon TYPE comes from the item's Inventory/Items.WeaponType, copied into the built weapon object (+0x188, by 0x3FCF70): the active weapon is [char+0x2348] -> slot [vc+0x728] -> [vc + idx * 0x28 + 0x38] -> +0x188.

Runtime material slots. A piece's material object (piece+0x48, vt_0x1FFE3E8) has one table of 48 slots of 0x18 bytes from +0x1E8: slots 0-15 solid, 16-31 translucent (+0x368), 32-47 depth proxy (+0x4E8); a slot's first field is the .mat resource. The setter is the object's vtable slot 47, 0x17AF0C(object, kind, index, resource): kind 0 solid, 1 translucent, 2 depth proxy; it references the resource and posts to the render command ring, so it belongs on the main thread. SetMaterialSet 0x4DC938(skeleton, type) re-applies a set's solid entries.

Render IDs (SetRenderID 0x1BF144(visual, id), through the skeleton's vf4 0x1BF3AC onto body, head, pieces and attachments): the player 255, his wraith copy 254, a player-aligned mount 212, followers 210, enemies 220. In-world characters stay in material-set type Wraith permanently (the type's override is the pale vision glow); no shipped Model carries a Wraith-typed set (set types in use: default 2,644, GhostWeapon 133, Echo 78, HierarchyUnknown 65, PlayerCompanion 17, Death 11).

Mounts. The ridden mount is [player+0x2578], its rider [mount+0x25A0], its visual [mount+0x2340]. A mount has no material controller (its combat object's +0x390 is 0).

Talion's material controller

ctrl = [[player+0x26A8]+0x390] (no vtable). It animates the shader parameters of Talion and of his wraith double; no other character has one (0 of 43 spawned characters).

Offset Meaning
+0x00 / +0x08 its link in the global list of animating controllers (mgr+0x1370, walked by 0x58B44C from the world tick 0x1F6EC4)
+0x30 owner
+0x60 the Combat/FX/MaterialController/Setup record (Setup::Player)
+0x80 an attack-linked state
+0x88 / +0x90 handle references to Talion's parameter block (self) and the double's (child) (vt_0x1DC47B8)
+0x98 / +0xB0 the parameter entries (0x40 each)
+0x100 the active state nodes (node: +0x00 state record, +0x10 expiry)

A parameter entry: +0x00 the parameter name's hash, +0x04 the priority of its driving state, +0x08 an easing object (only while easing), +0x10 / +0x14 start and end time, +0x18 the transform driving it, +0x20 default, +0x28 current, +0x30 from, +0x38 to. Value objects (float vt_0x1DC8930, int vt_0x1F9FF30, ARGB vt_0x1F9FE70; value at +0x08) push through their slot 2 to the handle's setter (SetParamFloat 0x3094FC, int 0x57AC84, SetParamVector 0x47E4540), each posting one render command.

The 168 material states are parameter states; none hides a model or swaps cloth. WraithRadiance, Vanish, WraithPing, Domination, Tower and GameChanger set Talion's k_fRevealValue 0 and the double's k_fRevealArm 1 and k_fOpacity 1 (the body cut away, the double shown); TalionDeathSwitch shows both; HandOfGod and Stealth draw a fainter double; _Curse sets k_fCurse 1; Player_Eltariel, TalionWraith and DarkTalion are colour sets.

The wraith shader (wraith_game.fxo, skeletal pixel shader): the material constant buffer cb3 follows the .mat parameter order (cb3[9] fixed-reveal width and active, reveal width and arm; cb3[10] fixed-reveal arm, reveal map index, dissolve, fresnel minimum; cb3[11] fresnel maximum, alpha floor, opacity, opacity minimum; cb3[12].x bloom scale). Reveal = saturate((sample + (W + 1) x Arm - 1) / W) x (1 - Dissolve) with W the reveal width; alpha is roughly fresnel x reveal x k_vObjectColor.w x opacity x diffuse alpha. k_vObjectColor is a global shader-constant object (0x2A39FA0, value at +0x40) filled per draw (0x254B80) from a packed colour at draw item +0x4C.

Birds and other flying creatures

Crows, bats, seagulls and the desert raptors are not characters. Each is an AvianObject (vt_0x1DBBD30, 0x518 bytes, derived from the generic game object, without a character controller, behaviour graph or inventory) spawned by a placed world object, an AvianMarker (vt_0x1FA4CD0). A marker owns at most one bird.

Definitions

Avian/Definition holds 14 species records: Crow, Crow_Eat, bat, bat_Day, bat_DaySleep, bat_AnyTime, Seagul, Seagull_Eat, raptor_Desert, raptor_Desert_Day, raptor_Desert_Night, raptor_Desert_Night_Clear, testing and testingBenchmark. A definition names the model (Model.Cha_Crow, Cha_Bat, Cha_Seagull, Cha_RaptorDesert), a scale range (crows 1.0-1.5, the others 0.9-1.1), a weather / time-of-day predicate (Weather/Predicates/Forecast.AnyTimeNoRain, NightNoRain, DayNoWeather ...), a combat tree (Hunting_Crow, Hunting_Bat, Hunting_Seagull, Hunting_Avian), a render-ID setup, seven animation slots each with a weighted variant list (perched, eating, take-off, bank left, bank right, flap, landing), perch and flight durations (average and variance), take-off height and distance, climb, descent, turn and lateral rates, a death effect, a respawn time and the group-sound settings. A crow perches 9 +/- 6 s and flies 60 +/- 11 s; bats fly 900 s; every species respawns after 33-120 s.

The manager

CAvianMgr is a singleton at [base+0x2A86878] (constructor 0x19C7D8C, destructor 0x19C7E00):

Offset Meaning
+0x00 the markers ({begin, end, u32 capacity, u8 owned})
+0x18 the live birds
+0x30 group-sound entries (no vtable; +0x00 = the definition)
+0x48 enabled
+0x4C the sweep cursor

System.SetEnabledAvians (0x1BAAB9C) writes +0x48; disabling also removes every marker's bird (0x575F98). Each frame on the main thread the client shell's update (0x1A864C) calls 0x441A10: the marker sweep 0x4418A8, then the group sounds 0x441A3C. The sweep is round-robin: it processes max(1, markers / 10) markers per frame from the cursor (the divisor is the static 0x267355C), so every marker is visited about every ten frames.

Markers

A marker reads its world properties when loaded (0x1A41DF4; defaults in brackets, distances in cm):

Offset Property
+0x120 position
+0x140 AvianDef, the definition record
+0x148 StartEnabled
+0x14C / +0x150 ActivationRadius [2000] and its square
+0x154 / +0x158 DeactivateRadius [2500; raised to 1.1 x ActivationRadius when not larger] and its square
+0x15C ActivationPercent [100]
+0x160 / +0x161 / +0x162 Unique, AlwaysFlying, Eating
+0x164 / +0x168 / +0x16C the script event ids AvianSpawnScript, AvianKilledScript, AvianOutOfRangeScript (P919 of the names)
+0x170 / +0x174 FearDistance [300] and its square
+0x178 / +0x17C / +0x180 FlightBottom [100], FlightTop [200], FlightRadius [100]
+0x188 the definition's render-ID setup
+0x198 / +0x19C an orbit centre is set / its position, copied from the object named by OrbitMarker (0x1A41C34)
+0x1A8 / +0x1AC FlyOff [false], FlyOffRange [4000]
+0x1D0 the bird
+0x1D8 the respawn time (ms)
+0x1DD / +0x1DE activated / the spawn roll passed

Per marker the sweep calls 0x43AE38 with the player's position. A disabled marker, or one whose definition's weather predicate fails against [GameManager+0x6D98], loses its bird. Distances are horizontal (X and Z) and measured to the player only. On entering the activation radius the marker rolls once: random(0, 99.9) < ActivationPercent; on success it spawns (0x1A41FFC: the definition's model, a scale drawn from the scale range and stored at the bird's +0xC8, class AvianObject, created through the object server). While activated, a marker with no bird respawns once the engine clock passes +0x1D8. Beyond the deactivation radius the bird is removed and the out-of-range script event fires. When a bird is destroyed the marker sets +0x1D8 = now + RespawnTime x 1000 (0x575ED4; 5 s when the field is absent); a respawn time of 0 or less clears the roll instead, so nothing returns until the player leaves and comes back.

In a live desert region 301 markers (raptor_Desert_Day 157, raptor_Desert 63, raptor_Desert_Night 57, raptor_Desert_Night_Clear 24) used activation radii of 120-130 m, a deactivation radius of 140 m, fear distances of 30-40 m and activation chances of 25, 38 and 40 percent; 250 were AlwaysFlying. Of 28 markers whose radius covered the player, 21 were activated and 6 had passed the roll, giving 6 birds, each linked both ways (marker+0x1D0 and bird+0x390), flying 7-26 m from its marker at 0.5-6 m above it, with scales 0.92-1.07.

The bird

Offset Meaning
+0xAC / +0xB8 / +0xC8 position, rotation, scale
+0x120 the hittable interface (vt_0x1DBBBD8)
+0x390 / +0x398 the marker / the definition
+0x40C the player is inside the marker's fear distance
+0x410 flight state
+0x4D0 a combat object of the definition's Hunting_* character type
+0x500 / +0x508 a pending attack and its tree node
+0x510 dead

The update (vf18, 0xEF820) advances the state machine at +0x410:

State Function Behaviour
0 perched 0x1A5B6B8 perched (or eating) animations; leaves after the perch time or when the player comes within the fear distance of the marker
1 take-off 0x1A5B8A0 take-off animation and climb from the take-off height and distance
2 flying 0xEFC48, steering 0xEFD10 flies for the flight time around the marker or its orbit centre above FlightBottom; turns pick the bank animations, straight flight the flap animations (threshold pi/16); a FlyOff marker's bird is destroyed
3 descending 0x1A5B3E8 landing animation down to the landing height; fear sends it back to 2
4 landing 0x1A5B508 touchdown, then 0 (or 2 on fear)
5 attacking 0x1A5B390 plays a pending attack

AvianMarker.Script_StartAttack (0x1A41FBC -> 0x1A5B160) picks an attack from the bird's combat tree for a target and enters state 5. The four bird trees (Hunting_Crow, _Avian, _Bat, _Seagull) have no attack nodes in a running game's merged database (132 of 281 combat trees do), so the call finds no attack.

Killing a bird. A hit goes through the hittable interface's slot 8 (0x1A5AA58), which runs the same damage evaluation as for characters (0x2E3B6C) and kills only when the damage is greater than 0. Death (0x1A5B01C) sets +0x510, fires the marker's AvianKilledScript event, plays the definition's death effect, posts combat event 0xB and destroys the object; the hit routine also calls the hit-counter increment 0x2DC668. The ranged targeting predicate WraithRanged_Targeting lists all six Hunting_* character types (avian, bat, crow, rat, seagull, spider).

Group sounds. 0x441A3C counts the birds of each definition within GroupSoundDistance, perched and in flight separately, and picks the none / low / high loop from the LowCount / HighCount thresholds.

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.