User interface: Scaleform, menus and the debug menu
How the game builds and draws its interface: the UI manager, the generic view host and the UI render list, the Scaleform movies and the node API that drives them, menus and the HUD as GameDB data, the Army screen's 3D stage and camera, the title, save-select and loading screens, and Monolith's developer debug menu with the cheat backend and console behind it.
The interface is a set of Scaleform GFx movies that the engine drives entirely from code and data: the movies hold artwork and timelines, while the GameDB holds menu layers, element placement, HUD states and cameras. This chapter follows the stack from the UI manager down to a single movie clip, then covers the Army screen's 3D stage, the title and loading screens and the developer debug menu that shipped without its controller.
The UI stack
The UI manager
The UI manager is [base+0x26FFB08] (vt_0x1F7A878, a class with three virtual methods; see Engine
core). 0x1889674 stores the object in that slot and creates its members; CInterfaceMgr::Init
(0x17804C4) later calls 0x188BAE4, which builds the views (below).
| Offset | Meaning |
|---|---|
| +0x30 | the HUD; its +0x108 is the combat widget holding the ammo count (+0x408) and the reticle state (byte +0x414, written by 0x114C588) |
| +0x50 | the menu system: the layer vector at +0x500 .. +0x508 (see Engine core), the Interface/Menu/MenuManager record at +0x518, the store header at +0x800 (see Saves, the store and the network layer) |
| +0x88 | the object that owns the debug-menu open byte +0x7AE (below) |
| +0xB0 | the player's visual component, used by the Skills screen (see Items) |
| +0xF0 / +0xF8 | the top menu layer's record and its layer state |
| +0x220 | the UI viewport as an int32 rectangle {left, top, right, bottom}; view initialisation takes its width and height from it |
| +0x248 / +0x250 | the pending message-box completion {function, context}; the box's close callback 0x17DA030 calls function(result, context) |
| +0x360 | the views, one per Interface/Shared.UIViews entry; the first three are cached at +0x378, +0x380 and +0x388 |
| +0x430 | a GameModes record (the HUD builder reads its UseMercenaryWidget) |
The top menu layer is what menu predicates test: the evaluator 0x276E74 compares each record of a
predicate's IncludedTopMenuLayers / ExcludedTopMenuLayers with [UIManager+0xF0] and each record of
IncludedTopMenuLayerStates / ExcludedTopMenuLayerStates with [UIManager+0xF8].
There is no UI thread in a running game. The UI manager's constructor also creates a thread object
named UIThread (0x1889460 -> 0x193856C; vt_0x22F94B8 with the shared thread methods, vf8 the name,
vf9 the stub 0x19385BC, vf10 a 0x4000-byte stack), but it is never started: the shared thread start 0x529500 has 30
call sites, and each starts another object (the named threads of Threads and the frame, a Bink worker, a
queue worker and two event workers on static objects). The object sits at UIManager+0x08. The UI's own console commands live in the source file
CUIThread.cpp. The UI's per-frame work shows on the main thread instead: the HUD's CUIMgr::Render
(0x1AB28C) runs inside the main scene's render submission, and captured movie frames reach the render
thread through the main thread's command ring (below).
Views: the generic host and the factory
A view is a CUIBaseView (vt_0x1F7CD48, 0x60 bytes). The factory 0x1889250(&view, 0, &record,
&index) allocates one and calls the constructor 0x18CAA38(view, record, index). The host is generic: it
builds any record of Interface/GFX/UI.
| View offset | Meaning |
|---|---|
| +0x48 | its layer (below) |
| +0x50 | u8, set to 1 at construction |
| +0x54 | u32 view index |
| +0x58 | the Interface/GFX/UI record |
Interface/GFX/UI has five records; a view record names a flash layer and the bundles it needs:
| View | ActiveFlashLayer |
RequiredBundle |
Built |
|---|---|---|---|
NavMarkers |
_Structuresuiflash primary 1 (interface\flash\navmarkers.gfx) |
interface\bundles\gfx\ingame.bndl, ingame_uifx, ingame_navmarkers, pc_alternatbuttonstyles |
yes (index 0) |
Player |
primary 2 (interface\flash\sp.gfx) |
the same four, plus five additional bundles (worlduidata, ingame_tex, store_icon ...) |
yes (index 1) |
MessageBox |
primary 4 (interface\flash\messageboxes.gfx) |
interface\bundles\gfx\ingame.bndl |
yes (index 2) |
PreLoad |
primary 3 (interface\flash\preload.gfx, the only layer with BackgroundAlpha 1) |
none | no |
Debug |
primary 0 (interface\flash\widgets\debugmenu.gfx) |
interface\bundles\gfx\debug.bndl |
no |
0x188BAF0 walks Interface/Shared.UIViews, which ships as [NavMarkers, Player, MessageBox], and builds
one view per entry into UIManager+0x360. PreLoad and Debug are never built.
Layer setup 0x18CACF4(layer, record, index) reads ActiveFlashLayer and refuses a layer whose
_Structuresuiflash.File is empty; it stores the index at layer+0x30 and the flash record at
layer+0x48. The view's methods then do the rest:
- vf1 0x18CB7DC, initialise. 0x18CBBA4 waits until the required and additional bundles are
resident and the movie file (
_Structuresuiflash.File, opened through the engine interface's vf13 service) is loaded into a movie definition atlayer+0x40. 0x18CAD78 then creates the movie: the view index becomes a movie mode (0 -> 1, 1 -> 2, 2 -> 4, anything else -> 2), the definition's vf4 0x11BBE3C(mode) (vt_0x22C2B00) builds a 0x3938-byte movie wrapper (vt_0x1E6EE80, constructor 0x11BC268) stored atlayer+0x38, and the record'sEdgeAA,MaxCurvePixelError,StrokeQualityandBackgroundAlphaare applied to it. The method is called until the wrapper exists. - vf2 0x2A92A4, advance. Takes the frame step from the layer's timer (
[layer+0x58]+0x14) and calls the wrapper's vf4.
The engine advances only the views it owns in UIManager+0x360; a view built outside that list draws
nothing until its vf2 is called.
The movie wrapper and the UI render list
The wrapper's +0x18 is the GFx movie and +0x38 its render ID. Its vf4 0x47D78F0 is one UI frame:
movie->vf24Advance(step, 2, 0), thenmovie->vf25Capture(1), which returns the captured frame.- 0x115226C posts a 0x28-byte command on the main thread's render command ring 0x272E8D8 (see Threads
and the frame):
{+0x00 thunk 0x65086C, +0x08 the UI render list, +0x10 method 0x650888, +0x18 u32 render ID, +0x20 frame}. On the render thread 0x650888 locks the list and stores the frame into the entry with that ID. - 0x251E00(ring, 0), then 0x2AA3A8 runs the wrapper's callback queue at
+0x3920.
The UI render list is a static object at 0x272EB20:
| Offset | Meaning |
|---|---|
| +0x10 / +0x18 | vector of 0x18-byte entries, kept sorted by mode |
| +0x28 | lock |
| Entry offset | Meaning |
|---|---|
| +0x00 | the render target (vt_0x22C6720) |
| +0x08 | u32 mode bit |
| +0x0C | u32 render ID |
| +0x10 | the last captured frame |
0x1C8DAF4(list, handle, mode) registers a movie: it takes the next ID from the counter at 0x27966A0, inserts an entry before the first one with a higher mode and returns the ID. Every movie has its own entry, so two movies of the same mode do not collide. In a loaded game the list holds NavMarkers (mode 1), the Player HUD (mode 2), MessageBox (mode 4) and two more movies with modes 8 and 0x10. The last captured frame stays on screen until the next capture. Scaleform draws the list through its own D3D11 renderer (see Rendering).
Message boxes
Interface/MessageBox holds 173 box definitions. A request is queued on the message-box manager at
[CGameClientShell+0x890] (the request layout is in Saves, the store and the network layer); the caller's
completion function goes into UIManager+0x248 / +0x250 and receives the button result.
Scaleform movies
Where they ship
The base game's movies are sections of three bundles in UI_Assets.arch06 (2,269 containers scanned,
19 movies found):
interface\bundles\ingame.embbholds 17: the component library, the HUD (sphud.gfx), the Army screen (hierarchy.gfx), upgrades, skills, swords, progression, minimap, zone map, message boxes, struggle sequence, reveal-and-death, world puzzle, the main menu, the online sign-in movie (wbid.gfx),navmarkers.gfxandsp.gfx;interface\bundles\debug.embbholdsinterface\flash\widgets\debugmenu.gfx;interface\bundles\font_latin.embbholds the font movieinterface\fonts\fonts_latin.gfx(9,649,751 bytes, a 550 x 400 stage).
A running game holds 18 resident movies (see The executable), which matches the 17 of ingame plus the
font movie. The patch archives also carry 56 loose copies of eight of them (24 distinct versions). The
archive search order (see Archives and file formats) serves these copies:
| Movie | Library symbol | Base copy: bytes / SWF version / exports | Served copy |
|---|---|---|---|
componentlibrary.gfx |
CompLibrary |
183,424 / 8 / 197 | Patch_DLC4: 204,515 / 8 / 246 |
sphud.gfx |
(none) | 177,433 / 14 / 133 | Patch_DLC4: 191,900 / 14 / 141 |
hierarchy.gfx |
HIELIB |
47,651 / 17 / 31 | Patch_09: 60,114 / 15 / 34 |
upgrades.gfx |
UPGRADESLIB |
96,435 / 17 / 39 | Patch_DLC5: 112,457 / 15 / 48 |
minimap.gfx |
MM-LIB |
25,070 / 17 / 21 | Patch_DLC4: 25,505 / 17 / 22 |
zonemap.gfx |
MAPLIB |
10,747 / 17 / 15 | Patch_DLC4: 13,765 / 17 / 15 |
messageboxes.gfx |
(none) | 11,734 / 8 / 7 | Patch_09: 17,906 / 8 / 10 |
wbid.gfx |
WBIDLib |
17,176 / 8 / 16 | Patch_09: 17,040 / 8 / 16 |
skills.gfx, swords.gfx, progression.gfx, strugglesequence.gfx, mainmenu.gfx |
SKILLSLIB, SWORDSLIB, PROGRESSLIB, StruggleLib, MainMenuLib |
base only | |
debugmenu.gfx |
(none) | 27,142 / 9 / 33 (12 DBG- symbols, 21 textures) |
base only |
All 18 interface movies, and the eight served patch copies, are uncompressed (GFX), have a 1280 x 720
stage (25,600 x 14,400 twips) and a single root frame, and run at 60 frames per second (30 for the
minimap, the message boxes, navmarkers.gfx and sp.gfx). Fonts are imported from
interface\fonts\gfxfontlib.swf ($HydraHUD); the message boxes import the component library, and the
zone map imports MM-LIB from the minimap. No movie embeds a picture: all 231 images of the 18 base
movies are Scaleform external images (DefineExternalImage2) named by their .tex path and resolved by
the engine's texture system. Symbol names carry the movie's prefix: LIB- (component library), SPH-
(HUD), HIE- (Army screen), MAP- (zone map), UPG- (upgrades), MM- (minimap), WBID-, DBG-
(debug menu).
What the movies contain
The movies are ActionScript 2 skins with almost no code. A widget's timeline has the frame labels
out, off, in and on; the off and on frames run stop(); if (this._FrameEventHandler)
this._FrameEventHandler(_root.EFrameEvents_Off) (or _On), and the root defines EFrameEvents_None,
_On, _Off, _FX and _Sound (0 to 4). Code selects a widget with gotoAndPlay("in") and deselects
it with gotoAndPlay("out"); the widget animates itself and reports the end of the animation through the
handler. Layout, lists, counts and text all come from code and the GameDB.
Driving a movie from code: the node API
The engine wraps every Scaleform value it touches in a 0xF8-byte node (vt_0x1DAB888, constructor
0x1C309C): +0x28 the value's object interface, +0x30 the value type ((type & 0x8F) == 0xA is a
display object), +0x38 the object. Callers receive nodes through output handles that the callee
constructs in place (0x1C2F80, the node at handle+0x10); a handle holds pointers to itself, so it
cannot be moved once constructed, and its destructor 0x1C68FC releases the node.
| Call | Address | Arguments |
|---|---|---|
_root |
wrapper vf42 0x4679C4 | (wrapper, out) |
| GetVariable | 0x467A20 | ([wrapper+0x18], value, "path"): resolves _root, not clips attached at run time |
| GetMember | node vf83 0x1C2EE4 | (node, out, "name") |
| GetMember by path | node vf84 0x11C063C | (node, out, "a.b.c") |
| createEmptyMovieClip | node vf80 0x7B1EE4 | (node, out, "name"): creates the clip; returns an empty handle when the name exists |
| attachMovie | node vf76 0x474434 | (node, out, "symbol", "instance"), both char*; refuses an existing instance name (0x47482C) |
| removeMovieClip | node vf86 0xEBDAC | (node) |
| Invoke without arguments | node vf85 0x11C10B4 | (node, "method"); the general invoke is 0x4676C0 |
| play / stop | node vf121 0xEBDE4 / vf122 0xEBE0C | (node) |
| gotoAndPlay / gotoAndStop by label | vf110 0x46F30C / vf112 0x471BE8 | (node, "label") |
| gotoAndPlay / gotoAndStop by frame | vf111 0x7DE75C / vf113 0x16D9B8 | (node, frame) |
| setText | vf103 0x471BFC (char*), vf102 0x471C84 (wchar_t*) |
(node, text) |
| setHTMLText | vf106 0x11C156C | (node, char*) |
| set the 2D matrix | 0x128592C | ([node+0x28], [node+0x38], matrix): rows {a, b, 0, tx}, {c, d, 0, ty}, translation in pixels (multiplied by 20.0 into twips) |
Interface elements are found by name through Interface/Elements (0x32A774): an element record names
its library symbol (SymbolName), an effect list and an Interface/ElementAttribute placement.
The attach-node lifetime rule
An attached clip lives exactly as long as the node attachMovie returned. attachMovie allocates the
node, attaches the symbol (0x115BB34), then 0x4744EC sets the owner byte node+0x58 to 1 and registers
an onUnload listener at node+0xA0 whose callback is 0x7941C4. The node destructor (vf0 0x1C3158 ->
0x1C318C) releases the listener and, while +0x58 is set, invokes removeMovieClip on the clip. If the
clip is unloaded some other way (removed, or its parent removed), 0x7941C4 clears +0x58 first, so a
clip is never removed twice. Lookup nodes (GetMember vf83 / vf84, _root) never set the owner byte.
So releasing an attach handle removes the clip, and a lookup handle for the same clip is a different node whose release leaves the clip alone. Code that keeps attached clips on screen must hold their attach handles (and release children before parents).
Menus as GameDB data
A menu is data: layers, trees of nodes, widgets, elements and their placements, all under Interface/.
- The menu manager record
Interface/Menu/MenuManager::Menu(at[[UIManager+0x50]+0x518]) links the default pause layer (PauseRoot/PauseLayer), the title screen root (FrontEndRoot/FrontEnd) and one layer per screen: world map, zone map, quest log, appendices, war upgrades, the Army screen (Hierarchy/HierarchyLayer), the captain detail (FactionMember/FactionMember), upgrades, equipment, gems, store, gold, mercenaries, skins, options and more. - Layers.
Interface/Menu/MenuLayerhas 48 layer classes and 74 layer records (one more class,Graveyard, survives only in an old patch). Every layer has the common fieldsContentSymbol,LayerFX, the title fields,DirectExitAction, sounds for init / activate / sub-layer push and pop / deactivate / term, aTransition(Interface/Menu/MenuTransition, 43 records),Components,NavPane, a status header type,AutoWidgets, aHUDState, a default layer state,MenuSelectionPredicatesand a telemetry alias; each class adds its own (the Army screen's has 97 fields, most of them actions).Interface/Menu/MenuLayerList(32) are stacks of layers pushed together (Hierarchy_Full,NewGameSetup_Full...). - Trees, nodes and widgets.
Interface/Menu/MenuTreeBase(155) are lists;Interface/Menu/MenuTreeNodehas 58 node classes (skills, gems, traits, quests, skins, game modes ...);Interface/Menu/MenuWidgetholds 271 widgets;Interface/Menu/MenuComponentholds buttons (78), checkboxes, cycles, sliders and text entries. The trait card's rows are tree nodes too (see Captain traits); the Skills screen's node state is computed in code (see Items). - Elements and placement.
Interface/Elements(242) andInterface/ElementAttribute(962). An attribute is a screen placement and its animation:AppliedProperties,InterpolationTime,DelayTime,InterpolationType,VisibleOnInterpolateTerm,UseSafeZone,Position(a screen fraction),Offset,Scale,Alpha,Z,ZScale,Rotation, a 3D rotation andFilters(Interface/ElementFilter: bevel, blur, colour matrix, drop shadow, glow). Attributes come in named pairs such asAgeGate_Active/AgeGate_Inactive, an element's placement with its layer active and inactive. - Predicates.
Interface/Predicates/*gate nodes and actions:Menu(66),HierarchyState(115),Hierarchy(28) and 39 more classes. - Text and input.
Interface/GFX/Fonts::Main,Interface/GFX/FontMapping(Console,IntHUD,LatinHUD),Interface/GFX/KeyMaps(InGame,Menu) and 109Interface/GFX/Textstyles for the engine's own debug text. Menus take input through command sets (Profile/CommandSets).
Layer objects are instances of per-class engine types: the mode-select layer is vt_0x1F92AF0 (see
Saves, the store and the network layer), the Army screen layer vt_0x1F7B588 (its job vt_0x1F7B748 at
+0x9C8, run 0x189C4F8) and the captain detail layer vt_0x1F7CB10 (job vt_0x1F7CC18, run 0x18CA348).
The HUD
The HUD is the Player view. Interface/ElementSetup::Setup (108 fields) configures it: HUDBase ->
Interface/HUD/Widget/HUDManager, five event managers (centre, left, right ...), the item widget, the
mercenary widget, DebugHUDElements -> Interface/HUD/Widget/FXTest, the bar effects and fill rates,
toast events and minimap thresholds.
The HUD builder 0x18C3E80 creates the widgets by name: combat (CUIHUDCombat), photo mode, combo, the
event managers, the standard, keyboard, quick-time, centre and top prompt groups, combat hints, the pausing
tutorial, pinned challenge, character and quest-start presentations, quest failure, brand notification,
the crosshair (CUIHUDCrosshair), skill points, score and timer. Last it tests
GameModes.UseMercenaryWidget on the record at [UIManager+0x430]: set, it builds the mercenary summon
bar (CUIHUDMercenaryWidget, the Desolation of Mordor bodyguard slots); clear, the item widget.
HUD content is driven by states: Interface/HUD/Widget (28), Interface/HUD/WidgetList (20),
Interface/HUD/State (44) and Interface/HUD/StateConfig (38). Each menu layer names the HUD state it
wants (the Army screen: Interface/HUD/State/Hierarchy). Button prompts are data as well (see Items); the
ammo counter and the reticle colour are written into the widget at [[UIManager+0x30]+0x108] by combat
code (see Combat).
The Army screen
The hierarchy movie and the 3D stage
The Army screen is a 3D stage in a dedicated menu world with the hierarchy.gfx overlay on top. The
movie draws only the 2D panels: its 22 HIE- symbols are the enemy overview, the trait card parts
(HIE-TraitHeader, -TraitGroup, -TraitContent, -TraitDivider), dialogs (upgrade, target, cancel),
quest start and complete panels, assault and defence groups, contract overview and the player icon. No
symbol describes rows or seats: the captains, the seats and the fort are world objects. (The instance
names Row0 - Row9 belong to the component library's control-bindings screen, and OverlordTree /
WarchiefTree to the zone map's fort highlight.)
Faction/HierarchyMenu/Setup::Default (114 fields) configures the screen: the stage bundle
bundles\hierarchymenu.bndl, the menu world collision prefabs\ui\worlduimenuws_collision.bndl, the
henchmen, power-struggle and blood-brother link effects, the fidget bundle
bundles\animationcharacters\menu\fidget.bndl (at most 20 fidgets at once), one camera state per mode
(army, mercenary, orders, garrison and contract assignment, store, map, pause ...), the character model
manager and 38 tutorials.
Each army's stage is chosen by its Faction/Definition.WorldUIFaction -> Faction/World/Faction (10
records): Instance names the stage rig (prefabs\ui\wuim_regionfaction_ws.bndl for five regions,
wuim_minasfaction_ws for Minas Ithil, wuim_desert_dlc4_ws and wuim_dlc4merc_faction_ws for the
Desolation of Mordor), FortPicker the fort pieces, and Member / GarrisonMember / VendettaMember
the seat grids. The section is requested by the region's stage object in the menu world (vt_0x1FA4008,
named Hier.Map.Region_*) when that object activates: its vf16 0x1A34E78 maps the record Region to the
region's army, resolves the world section and requests it into the object's own group (0x4B420C), then
links the object into the active-stage list 0x2675860. The activation runs when the menu world is
rebuilt during a load, not on a visit to the Army screen.
Stage holders and the world-UI owner
The world-UI owner [base+0x26FFB60] (vt_0x1F97950; see Engine core) keeps the stage holders in a map
at +0x2B8 keyed by army record: 0x50-byte entries with the holder at +0x48, looked up by
0x455EDC(owner, army). The armies of one region share one holder. A holder (vt_0x1FA2FB8):
| Offset | Meaning |
|---|---|
| +0x158 | the replay event |
| +0x160 | the holder's frame {position, rotation}, from which grid seats are placed |
| +0x198 / +0x1A0, +0x1A8 / +0x1B0 | the stage characters: arrays of 0x28-byte entries, object at +0x20 |
| +0x1B8 | the summary container (its +0x9F50 is the army) |
| +0x1C0 | the stage manager |
| +0x1E8 | the fort (vt_0x1FA2698); +0x274 requests a fort build |
| +0x275 | rebuild every stage character |
| +0x276 | rebuild the rows |
The holder tick 0x16EE24 (from the owner's per-frame Update, vf18 0x16F4A0) runs the fort reconciler
0x2EC464, which diffs the wanted fort pieces against the built ones; then, when +0x275 is set, prunes
relationships (0x1B5E13C) and rebuilds every stage character from the summaries (0x1D7A918), writing the
word +0x275 = 0x100 so that the row rebuild 0x1A284D8 follows on the next pass. The engine sets +0x275
itself when it sets a holder up for an army (0x1A29168, from 0x19C3760 and 0x19C3F60). With the menu
closed an army has no stage objects; they are built fresh from the summaries when the screen opens.
Stage characters and member summaries
A captain on the stage is a CWorldUIFactionMember (vt_0x1F733C0, 0xB58 bytes, placement constructor
0x1822164), created through the object server by class name. It is built from the army's member
summary, not from the FactionMember.
| Stage character offset | Meaning |
|---|---|
| +0xAC | position of the animated root (the hip), on top of the seat |
| +0x110 | the character model (vt_0x1F6F950) |
| +0x1A8, +0xAF8 | the cast seat position |
| +0x668 | the mount, for a mounted captain |
| +0x720 | the previous look, kept while a new one is set up |
| +0x728 | -> its private look (0x40 bytes, constructor 0x62DCB4) -> a private summary copy |
| +0x740 | the preview states (Faction/HierarchyMenu/PreviewTypeStates) |
| +0x760 | the model object |
| +0xB18 | its world-UI type record (Faction/World/Character::FactionMember, FactionMember_WorldMap ...), not the member |
| +0xB3C | set when the preview states must be rebuilt |
A member summary (vt_0x1DB1070, 0x1A8 bytes) is one member's frozen description:
| Offset | Meaning |
|---|---|
| +0x30 | a copy of the member's inventory (slot items, colours at +0xC8, overlays at +0xE0) |
| +0x40 | a copy of the appearance (piece for piece like the live appearance object) |
| +0x48 | the faction |
| +0x58 .. +0x60 | a copy of the active traits |
| +0x70 | a copy of the markers |
| +0x88 | the key {u64 FM+0x178 army, u32 FM+0x174 pool index, u32 0} |
| +0x100 | location |
| +0x120 | level |
| +0x130 | combat tree (FM+0x188) |
| +0x138 / +0x140 / +0x148 / +0x150 | class, role, personality, tribe |
| +0x168 / +0x178 | row, rarity |
| +0x188 | the flag word (FM+0x508) |
| +0x190 | bit 0: the name is known (FM+0x510 bit 0) |
There are three generations of copies. The army context keeps one summary per member ([ctx+0x30],
lookup 0x1B5C0F8(FS, &key) -> 0x16FC5C, a pure read); the open stage draws from its own container of
copies (0x16FC1C(owner, army), none shared with the army's); and each stage character draws from a third,
private copy behind +0x728. The engine's refills: 0x169168(summary, member) re-reads the whole member
into a summary (appearance from 0x75D2FC, items from 0x75D314, class, tree, role, personality, tribe,
level, flags, the trait list rebuilt from FM+0x198 and the markers from FM+0x1B0); its sources are the
member's mock-member snapshot when one exists (see The Nemesis system). 0x62BF54 refills a container copy
(or appends one for an unseated member).
The per-captain redraw is 0x19C395C(owner, &key), which the engine calls after a Training Order is
applied (0x1C51630): holder 0x455EDC -> summary 0x1B5C0F8 -> stage character 0x1A28FAC(holder, key) ->
UI_CWorldUICharacter 0x627F40(character, summary, 0, 0). Every step returns quietly when the army is
not on a stage. 0x627F40 chooses the look (0x2EB2DC, below), and 0x62EEE4 swaps the new look in, rebinds
the model and, only when the look test 0x170564 reports a change, tears down the selection and model
objects, rebuilds them and sets +0xB3C. The look test does not compare the death bits (flag bits 2 and
21), so a change of state alone does not rebuild a captain's preview pose.
Under modification: a value written into an army summary reaches the open stage only when the screen is entered again, because the stage draws from its own copies; a stage character's private copy is deep (it owns its item instances), so refilling it in place frees items the model has bound.
Every frame while the screen is open, the owner's tick runs the character model manager
(HierarchyCharacterModelMgr, ConfigSwapsPerFrame) -> 0x628BF8 -> the stage character's vf63 0x3680B0
(its summary) -> 0x30DE28, which walks the summary's trait list for a MountAITemplate.
Seats, placement and animation
A fort's composition is data: Faction/World/Faction.FortPicker -> Faction/World/Fort/Picker ->
fort setups -> piece entries {faction predicate, Army-screen predicate, pieces} -> pieces {world
sections, position, rotation, fixed seats}. The upper rows (Overlord, inner and outer warchiefs, the
first captain row) sit on fixed seats (WorldUIFixedFactionMemberPos: a row and column, a position and a
rotation), which any piece can register (0x629EEC -> 0x62A17C). The other rows are a grid
(_StructuresWorldUI.FactionMember: RowGap, ColumnGap) placed from the holder's frame (0x1D79DF0).
A seat is dropped onto the stage's collision by a ray, 0x62AA6C(seat, offset, out): on a hit the height
becomes the hit point plus the offset, on a miss the seat keeps its data position. The stage character
(its vf11) calls it through 0x62AA48 with a zero offset, so the character's animated root, the hip, is
placed on the seat itself.
Stage idles are chosen by data and played by clip name. A preview state's
HierarchyPreviewTypeAnimData.RootAnimChoice / RootInAnimChoice is a choice group; 0x42E72C takes the
first child whose predicates pass (0x42E984 -> 0x42EAFC; inputs: the combat tree's CharacterType, the
role, the personality and whether the name is known), 0x42E82C collects its Animation records, 0x42F794
picks one at random and 0x42F1D0 plays it by name (0x4C8A0C -> 0x302540: the name's hash looked up in the
skeleton's animation set). A name the character's set lacks plays the bind pose. Fidgets fire only for
tags the current choice carries.
The detail view plays the effects of Faction/World/Interactable/FX/States::FM_Army_Detail: its entries
choose, by species predicate, an InteractableFXData whose FX names a ClientFX sequence
(Hierarchy_Detail, Hierarchy_DetailOlag, Hierarchy_RiderDetail); 0x2ECF5C reads the link on every
update.
Unknown captains: the silhouette
A captain whose name the player does not know is drawn as a generic model. The look chooser 0x2EB2DC
takes, in order: the decapitation picker (DecapModelPicker, 0x2EB258 -> 0x2EB14C) when the summary's
flag word has bits 17 and 21 set; else, when the stage character's type record has a DefaultModel and
0x170744(summary, flag) is false, that default model (0x2EA718); else the captain's own model (summary
+0x138), built by 0x2EBA60 from the appearance copy.
0x170744 returns true when the member (found from the key at summary+0x88) has killed the player
(FM+0x4C4 non-zero: such a captain is always shown); with the flag argument set, when a further
predicate passes; otherwise it returns summary+0x190 bit 0, the copy of the name-known bit (see The
Nemesis system for intel levels).
Faction/World/Character |
DefaultModel |
KilledByDecapitation |
|---|---|---|
FactionMember, _Assignment, _GarrisonMember, _ModelCam, _Quest, _Stash, _WorldMap, WorldMap |
Character/Models/HierarchyUnknown |
Head_Stick |
FactionMember_Portrait |
PortraitUnknown |
Head_Stick |
GraveyardVendor |
HierarchyUnknown |
none |
FactionMember_StoreFront, Talion, Mount and the other nine |
none | none |
The Army camera
The Army screen has two camera branches.
- The overview. The army camera state
Faction/HierarchyMenu/Cameras/States::CS_Army_Cursornames a fixed overview, but itsUseWorldMapCameraPredicateisFaction/World/Predicates/HierarchyMenu::HierarchyFreeCursor, which passes while the top menu layer is the Army screen, contract assignment or the mercenary layer (and not in replay mode). While it passes, the camera belongs to the world-map layer:Interface/Menu/MenuLayer/WorldMap.HierarchyCameraState->Interface/Camera/CameraStates::HierarchyFreeCam, whose two orbit keys (_StructuresUIOrbitPointParamsprimaries 340 and 341) are the near and far ends of the zoom: radius 1025 and 5250, vertical field of view 25 degrees. Two keys are what make a zoom axis. - The captain detail.
CS_Char_Detailhas no world-map predicate. ItsUICameraSettingsis an array of 153Faction/HierarchyMenu/Cameras/UICameraStateentries, each chosen by a predicate on the captain's row, seat and state (Orc_Row2_L,Orc_SlotOverlord_Row4_3,Orc_Dead,Talion...) and naming one of 111 distinct camera states. Each of the 111 has exactly one orbit key, so its radius is a fixed distance and the detail view has no zoom.
An orbit key (_StructuresUIOrbitPointParams) holds fov, OrbitRadius and Roll as plain floats of
the key, and CameraOffset, CameraOffsetRange and CameraAdjust as shared float-pool pairs (type 7;
see GameDB). CameraOffsetRange is the (yaw, pitch) freedom of the key in degrees: the overview's two keys
340 and 341 carry (24, 12) at radius 1,025 and (16, 10) at radius 5,250, both with fov 25. A key also has a node offset (orbit and focal offsets with position and rotation tracking masks) and a
DepthOfField link to Client/DepthOfFieldSettings (near and far in-focus and blurred distances, blur
intensities). The reader 0x527A18 copies all of it into the camera when the state starts, so a key edited
while its state runs takes effect the next time the state starts. WorldMapCameraMode is 4 on every
Army camera state except two: CS_Store_ProductDisplay_Cursor (5) and CS_Mercenary_Cursor (6).
The title screen, save select and loading screens
The title screen and its message of the day
The title screen's root layer is Interface/Menu/MenuLayer/FrontEndRoot::FrontEnd (class
CUIFrontEndRootLayer): content symbol LIB-MenuList, the selection tree FrontEndRoot, a build
version widget, the new-game layer list NewGameSetup_Full, and a message-of-the-day node
Interface/Menu/MenuTreeNode/StoreMOTD/Default (cooldown colour, size and a 10-second time). The
message's text comes from the online service's broadcast feed, not from the localization tables.
Interface/Shared names a front-end world, worlds\reference\frontend\frontend.world06c, which does not
ship, and a background bundle with two images cross-faded every 10 seconds.
The skin choice is a tree node too: Interface/Menu/MenuTreeNode/Skin::Skin_DarkTalion names the
character model PlayerSkin_DarkTalion_Main and stores its unlock, notification and viewed state in
Profile/GlobalOptions.
Save select
The save-select screen is the mode-select layer Interface/Menu/MenuLayer/GameModeSelect::ModeSelect.
Its row state, availability test, the Continue / New Game / Delete actions and the confirmation box are
in Saves, the store and the network layer; a new game pushes the SingleOption::Difficulty layer.
Loading screens and Production Notes
A loading screen is an Interface/LoadingScreen record (27): Allowed, Platform, a GameData predicate,
completed / incomplete quest gates, required game modes and world, a fast-travel state, background slides
with cycle and fade times, a bundle, an optional movie, portrait settings, valid tips and enter / exit
audio hooks.
CUILoadingScreen::CUILoadingScreen (0x18D2024) looks up the elements LoadingBG, LoadingBGCaption
and LoadingProgressBar. Then, only when the byte 0x2A2637D is set (one reader, no writer in the
executable), it finds the element ProductionNotes (symbol LIB-ProdNotes in the component library),
gets its child Field and writes Monolith's notes into it: Debug/Shared::Debug.DebugMOTD (a "Project
Timeline" header) followed by every DebugMOTDDates entry (DisplayString plus DateString through the
month-name formatter 0x18D2608), separated by <br>. The five shipped dates run from the fourth DLC's
release to the fifth DLC's bug lockdown (May to June 2018). In the retail game the byte is never set, so
the notes never appear.
The Monolith debug menu
What shipped
The controller that built and ran Monolith's developer menu was compiled out; everything around it shipped.
| Piece | Where | Content |
|---|---|---|
| The movie | debugmenu.gfx in interface\bundles\debug.embb (UI_Assets; the bundle also holds the controller glyph textures and interface\textures\debug\debugsheet.tex) |
title "Monolith / Debug Menu" in the $Console font; 12 widgets: DBG-MenuRoot, -MenuButton, -ItemsWithDesc, -ConsoleCommands, -CheatEntry, -LevelList, -LevelEntry, -ScriptHookEntry, -ScriptHookPrompt, -ProdNotes, -Prompts, -Loading |
| The view | Interface/GFX/UI::Debug |
the movie and debug.bndl |
| The root | Debug/Shared::Debug |
the command set Profile/CommandSets/DebugMenu, enter / exit sounds, the Production Notes text |
| Console pages | Debug/ConvarShortcut |
34 pages (QA, AAA, HDR, Camera, Combat, Inventory, Presentation, AI info, nav mesh, nodes, markup and world review, game speed, frame stepping ...), 530 items, each a title and a console macro |
| Cheats | Interface/Cheat (14) and Interface/Cheat/Order (9 shown) |
God, Health, Clip, RemoveAI, SafeZone, Invisible, quest unlock / complete / teleport, CrimeRemove, AllAchievements, DSDebugCam ... |
| Captain actions | Debug/FactionMember::Default |
20 actions (level up / down, decapitate, set as follower / spy / dead / prisoner / revenge target, get intel, swap positions, eject, hire mercenary ...), each a faction event |
| Script hooks | Debug/DebugScript (135), Debug/LevelScriptType (14), Debug/MetadataType (94) |
names and parent links |
| Layout | Interface/ElementAttribute::DebugMenu_Root, _Active, _Inactive, _SubLayer, _PromptBar; buttons DebugMenu_BaseBtn, _LevelListBtn, _ConsoleCommand, _ShowcaseBtn, _ScriptHookEntry; MenuTransition::Debug |
header at the top right (1.0, 0.0), the active page at (0.05, 0.12), the prompt bar at (0.5, 0.98); inactive pages turn 60 degrees about Y, so pages flip like cards |
| Levels | WorldInfo.InDebugMenu |
11 worlds; six *_debug worlds exist as records |
The widgets are the self-animating skins described above (DBG-MenuButton: out 0, off 9, in 10,
on 14). DBG-ConsoleCommands is a page list with an 18-row sub-menu and a grey command preview;
DBG-ItemsWithDesc a list with a description panel; DBG-CheatEntry a row with a checkbox.
The shell that still runs
The open / close logic is intact behind the byte 0x2A2636D, which has four readers and no writer:
- the console commands
UI.OpenDebugMenu(0xD652B0) andUI.CloseDebugMenu(0xD64AD0), registered inCUIThread.cpp; SetDebugMenuShown0x7B5980([UIManager+0x88], show), behind the script callSystem:ShowDebugMenu;- the Army screen's
ActionOpenDebughandler 0x18CC238.
All four act on [UIManager+0x88] and do nothing while the byte is clear. SetDebugMenuShown pushes
Profile/CommandSets/DebugMenu on the input stack (0x1170D44; DisableAllOtherCommands with 26 menu
commands enabled, so gameplay input is blocked), plays EnterMenuSFX and sets the open byte
[[UIManager]+0x88]+0x7AE; hiding pops the set (0x1170D34) and plays ExitMenuSFX. The Army screen
action pushes the set and sets the byte without a sound. While the byte is set, IsDebugMenuOpen
0x381EEC makes the renderer 0x382044 call 0x381E68, which draws a full-screen black quad at alpha 0xDD
over the world and the HUD, and the gameplay-input test 0x17B91EC fails.
The input data is live too: Interface/Shared.DebugCommandSet (DebugMenuActivate_Hydra) is pushed at
every game start (0x1780A04), and Profile/Commands binds DebugMenuActivate1-4 (held together: Home or
9, End or 0, Left and Right; on a pad L3, Start and D-pad left and right). The input registration
0x3123CC gives the debug commands their IDs (DebugMenuActivate1-4 0xC8-0xCB, spectator and God
shortcuts 0xCC / 0xCD, AI info next / last 0xCE / 0xCF, debug spawn modes 0xD0-0xDB, captain list / save /
load 0xDC-0xDF, the debug camera 0xBC-0xC7).
What was stripped
- The view is never built:
Debugis not inInterface/Shared.UIViews, sodebug.bndland the movie are never loaded. - No code names the menu's records:
DebugMenu_,DBG-,Debug_Menu,ScriptHook,ProdNotesandShowcaseoccur nowhere in the executable as strings (controls in the same scan:NavMarkers12,MessageBox25,HierarchyLayer4,FrontEndRoot5), and the component, attribute and view records have no inbound RecordLinks. - The page data has no reader: the fields
ConvarShortcut.Key / Title / String / ShowInDebugMenu / CloseMenuOnCommand,Interface/Cheat.CheatConsoleString,WorldInfo.InDebugMenu,Debug/FactionMember.ActionsandDebugScript.Parenthave no accessor, whileDebug/Shared.DebugMenuCommandSeton the same record does (0x7B5A0C). - Nothing opens it from input: the open functions have no caller besides the script binding, the
console registry and the Army screen action, and the
DebugMenuActivatecommands are referenced only by their registrar. - Most page commands are not in the retail console: the 530 items expand to 1,711 console commands;
85 name a live command, 18 a handler compiled down to
ret 0, and 1,608 a name registered nowhere.
The only shipped script that touches the menu is the Bright Lord finale, which prints "Finale Part 4
Triggered" and calls System:ShowDebugMenu(false), the call the menu's Script Hook page made after firing
a script.
The cheat backend
The cheat manager is [GameManager+0x6CA8]: a vector of 0x28-byte entries (+0x10 id, +0x18 the
Interface/Cheat record, +0x20 toggleable, +0x21 on), built by 0x180EF9C, with per-id allowed flags
(0x65B0E4). The retail game registers one entry, God. The names table 0x22F55A8 reads GOD, HEALTH,
CLIP, REMOVEAI, SLOWMO (ids 0 to 4).
- Toggle 0x65ADDC(manager, id): finds the entry by id, checks the allowed flags (0x65B0E4) and the
record's
CheatActivatedString(0x65AEA8), then calls Apply with the entry's+0x21state. - Apply 0x65AD7C(manager, id, on, character system): God stores the byte
manager+0x30and callsCCharacter::SetCanDamage(!on), then refills health; Health refills through 0x65ADCC on the player's combat object; Clip calls 0x19BFDA0(player, on, 1); RemoveAI calls 0x180EC7C and clears three fields of the client shell's+0xA50object; SlowMo starts the slow-motion recordDebug(0x7CE80C). 0x180F184 re-applies Clip when the player spawns.
The Interface/Cheat console strings (Cheat_God ...) are not registered in the retail console. The
captain actions are faction events fired by CreateAndProcessFactionEvent 0x1B5B610(FS, event, member,
other) on the Army screen's highlighted member, [[base+0x26FFB10]+0xA78] (see The Nemesis system).
The console it drives
ExecuteCommand is the engine interface's vf29 (0x1180D8C): it hands the line to 0x1182FA4, which looks
the name up in the console registry (Find, see The executable) and runs the command synchronously on the
calling thread, splitting ;-separated commands. A name value line whose name is not registered
creates a new settings variable (0x1182F10) instead of failing. Of the registered commands, 98 of 449
have handlers compiled down to ret 0.
The console's output goes nowhere. Command output and the engine log both end in the sink 0x6AAC14,
which under the lock 0x27C45A0 calls the function pointer [base+0x27C45C8] if set, then writes to a
stream if the byte 0x27C45D1 is set. In the retail game the pointer is null (written only by its static
initialiser 0x585A0 and the exit path 0x1196AF4) and the byte has one reader and no writer.
The debug camera these pages drive is described in Characters, the player and the camera.
Tutorials
Tutorials are data. Four families of records drive them:
| Family | Records | What a record holds |
|---|---|---|
Interface/Tutorial/Pausing |
116 | a modal pane set that pauses play: HUDState (Interface/HUD/State.PausingTutorial), HeaderString, DismissString, Panes (348 _StructuresTutorialPausingPaneData structures in all: BodyString, image, or video, an Interface/Movies record), its own Bundle (interface\bundles\kraken\tutorials\*.bndl), the input CommandSet that holds play (Profile/CommandSets.TutorialModal_A), InputLockout (0.5 s), RequireAllPressed, HoldAndPress, InputHoldDuration and enter / exit sounds |
Interface/Tutorial/Gameplay |
120 | an in-play hint: DisplayString, duration, sounds |
Interface/Tutorial/Menu |
82 | a menu tutorial: a Setup (screen position) and OnEnter, OnExitBegin, OnExit steps (253 _StructuresTutorial_MenuStep) |
Faction/HierarchyTutorial |
62 | an Army-screen tutorial: a predicate, ExitMenuOnFinished and FirstState, a chain of Faction/HierarchyTutorial/States (84) |
A menu step applies or removes an input command set (HierarchyTutorial_A,
HierarchyTutorial_LStickX ...), enables or blocks mouse movement and fast travel, advances on the
common input, and may change the menu around it through three record kinds: WidgetData (18:
Show_ / Hide_ a widget such as the fort status header or the hierarchy overview), TreeData (24:
restrict a menu tree to some nodes, ChallengesRestrict, GemSocketsRestrict ..., or clear the
restriction) and MenuSelectionData (8: lists of Interface/Menu/MenuSelection/Predicate records to
disable, for example every order on a captain during the Bruz ambush tutorials). Layer data adds
per-screen settings (ShowMemberNavMarker / HideMemberNavMarker on the Army screen, SelectMtnPass on
the quest log).
An Army-screen state names the menu tutorial to show, an optional map presentation, a preset or job
member or the mercenary entry point to select, the view, a camera transition to wait for (with a
command set to apply meanwhile), PopupDelay (1.0 s in PlayerDiedFirstTime_State01), DelayNextState
and Next, and closing conditions (CloseOnPositionSelected, CloseOnNewEquipmentHighlighted). The
tutorials' predicates live in Faction/HierarchyTutorial/Preadicates (83 records, 73 fields; the
category's name carries the misspelling). TutorialViwed links a Combat/Predicates/Bool record (has
this tutorial been seen) and AnyTutorialsViwed, AllTutorialsViwed and NotTutorialsViwed test other
tutorials; the rest test the Army screen's mode and state (normal, replay, interrogation, commanding,
orders, assault setup; overview or details), the selected captain's known state (known, location known,
full info, presumed dead, branded, decapitated, henchman ...), the active game mode, inventory and
new-item counts, skill points and store counts.
From script. TutorialPausingShow / TutorialPausingHide and TutorialGameplayShow /
TutorialGameplayHide are methods of the CCharacter script class (0x1BB2108 resolves the
Interface/Tutorial/Pausing record and calls the character's vtable slot 99); the player raises
OnTutorialPausingDismissed / OnTutorialGameplayDismissed. System:GetHierarchyTutorialViewed
(0x1B86BA4) checks a Faction/HierarchyTutorial record against the viewed list reached through the
player ([CGameClientShell]+0x888, with its byte +0x3880; the test is 0x30C46C). The HUD classes are
CUIHUDPausingTutorial and CUIHUDGameplayTutorial; the Army-screen tutorials are run by the
hierarchy menu's tutorial manager (HierarchyMenuTutorialMgr.cpp), and the console names a
HierarchyMenu.ResetTutorials command.
Open questions
- What class is
[UIManager+0x88], the owner of the debug-menu open byte? The UI manager's constructor 0x1889674 only zeroes the slot. In a running game the object starts with a heap pointer rather than a vtable and holds a run of text-style entries that name the font "Anonymous Pro" with sizes 45 and 100 (the name is not a literal in the executable, so it comes from data); the open byte sits at+0x7AE, near the object's end. - What the
PreLoadview was for, whether itspreload.gfxstill ships (no archive entry has that name and no interface bundle's manifest names it, butingame.bndl's manifest does not namesp.gfxeither, and that movie ships inside its container), and which movies hold the UI render list's mode 8 and 0x10 entries. - What consumes the
DebugMenuActivate1-4command IDs (0xC8-0xCB). Data enables them: bothProfile/CommandSetsrecordsDebugMenuActivate_Hydra(pushed at every game start) andDebugMenuActivate_Menulist all four inEnableCommands, next to the AI-info and FactionMember debug commands. No code names them: neither their record links (0x9FD00091 - 0x9FD00094) nor their name hashes occur in the executable. A consumer would test the numeric IDs. - Which code selects the
Interface/LoadingScreenrecord for a given load. Data names few of them: no shipped script names a loading screen (0 of 2,562 script sources), and the fields that can link one areGameModes.InitialLoadingScreen(8 of 13 game modes nameIntro1, oneDLC3_FastTravel_FromUI),Presentation/Elements.LoadingScreen(set on 0 of 3,000 elements),Regions/Region.ExitOnlineModeLoadingScreenOverride(0 of 19 regions) andFaction/MoneywallSetup/UI.LoadingScreen. In the shipped databaseIntro1is linked 8 times andDLC3_FastTravel_FromUIonce, whileFastTravel_DesertLoadScreenis linked by no record at all, so the per-region fast-travel screens are chosen in code. - Where the viewed-tutorial list is stored and whether it is saved with the game or the profile.
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.