Authoring Magma UI
Everything here is either transcribed from the RE'd wire format (see
.mgb and the field-name join) or
measured across the 50-package vanilla corpus — 773 areas, 5,422 elements, 10,949 keyframes — by
decoding every ui\localized\pcwidescreen\eng\ui\*.mgb to XML and counting. Claims that are
inferred rather than confirmed are marked (inferred) at the point they are made.
The one working example on this page — hello.mgb.xml —
encodes, re-parses, and round-trips byte-identically through jackall mgb encode / decode. It has
not been shown on screen in-game.
Magma is the in-house UI engine behind every Far Cry 2 screen: main menu, HUD, pause menus, the
in-game map editor. A screen is a package — one .mgb binary, optionally paired with a
.mgb.desc sidecar. This section is the authoring counterpart to the format pages: those describe
the bytes, these describe how to make something the game will actually draw.
| Page | What it answers |
|---|---|
| This page | What the model is, how to build and verify a package, the rules that keep it loadable |
| Reference | Every element and attribute you can write in the .mgb XML, and what values are legal |
The .mgb.desc sidecar | The plain-XML companion file: resource manifest, load order, nav-bar prompts |
| Patterns | How shipped screens do buttons, lists, fades, focus, masks — with the reusable common.mgb catalogue |
| Interop with the Dunia engine | Where behaviour actually lives: the action registry, dispatch, and how runtime data reaches a widget |
| Limits | The hard edges of the format, and the failures that are silent |
The model in six facts
- A package is a flat list of areas. An
Areais a self-contained mini-scene with its own coordinate space, its own frame timeline, and its own element list. There is no area nesting on the wire — a package isPackage → Area* → Element*, exactly two levels deep. - Nesting is done by instancing. An element whose widget is an
AreaInstance/PageInstance/ButtonInstanceembeds another area — from this package or another one — at a position. That is the only composition mechanism, and it is what makes the tree arbitrarily deep in practice. 1,280 instances across the corpus; of the 1,273 area links they and the list/slider widgets carry, 624 point intocommon.mgb. - Every element is a widget plus a timeline.
Elementcarries the shared header (visibility, mask mode,UserData), then a list ofKeyframes, then the widget's own body. Geometry and colour live in the keyframes, never in the widget — a static element is one with a single keyframe atIDX="0". 63% of elements are exactly that. - Everything is animated by frame index. The area declares
FRAMERATE; each keyframe declaresIDX, an integer frame number; the engine interpolates between them. Playback is controlled byActions attached to keyframes — 1,523 of the corpus's 1,853 action executers hang off a keyframe, andStop/GotoFrameIndex/Continueaccount for almost everything they fire. - Names are CRC32 hashes, one-way. Areas, elements, keyframes, property keys and cross-package
references are all stored as
CRC32(name). You author a string, the file keeps 4 bytes. Two objects with the same name in the same scope are indistinguishable; a name you never wrote down is unrecoverable. - The
GenericObjectTableis the package's export surface. It maps a name to a path into the tree, andmagma::Engine::LoadPackageregisters every loaded package's table globally. It is how native C++ code finds a page by name, and the only part of your package the outside world can address.
The authoring loop
.mgb is binary, has no lengths and no sentinels, and a single wrong field width silently corrupts
everything after it. You do not hand-write it — you write the XML interchange format
(JackAll's, not Magma's own .mgm) and build it:
# start from something real
jackall mgb decode "<extracted>/ui/localized/pcwidescreen/eng/ui/options.mgb" -o options.xml
# edit options.xml, then
jackall mgb verify options.xml
jackall mgb encode options.xml -o options.mgb
mgb encode reads the result straight back before writing it, so a package that survives the
command is at least structurally loadable. The XML reader is deliberately strict: a misspelled
attribute or an element in the wrong place is an error naming the offender, rather than the silent
degradation Magma's own XML loader does.
mgb verify covers the class of mistake that is loadable: a reference to a name the package never
declares. Package::ResolveLinks drops one it cannot find without failing the load, so a link to a
missing element is a valid package that draws a screen with something absent — see
Limits. It takes either the XML or the built .mgb, and
--page <NAME> additionally checks that a page is reachable by the name native code will look it up
under.
Getting the .mgb into the game is a separate problem with two answers:
- Replace a shipped package — rebuild the archive it lives in (
patch.fat), keeping the same path. The engine loads it throughCMagmaConfigUIResource→CMagmaUIResource::LoadPackageInMagma. - Add a new package — hook the file reader and serve your own bytes under a
UI\…path, then bind a page to it by name. This is what FCSE does; see the menu system page andtools/FCSE/assets/README.md.
The minimal package
hello.mgb.xml is a complete, verified starting point:
1,540 bytes compiled, one Page with a fade-in panel, a text label, and the standard back-prompt
strip instanced out of common.mgb. Its shape, with the 166-entry <TYPES> block elided:
<MagmaPackage sentinel="CD0000AB" version="2010000" flag="false"
POOLCOUNTS="0 0 0 …65 zeros…"
PAGESIZE.w="1280" PAGESIZE.h="800"
DISPLAYOFFSET.x="160" DISPLAYOFFSET.y="40" DEFAULTMATERIAL="">
<TYPES>…166 <TYPE id="…"/> entries, copied verbatim from any decoded package…</TYPES>
<USERDATA name="hello"><PROPERTIES /></USERDATA>
<MATERIALS materialExtra="0" />
<FONTSUBSTS /><FONTS /><FONTFAMILIES />
<CHILDREN>
<Area slot="101" type="Page" FRAMERATE="30" CURRENTFRAME="0"
STATICBOX="0 1280 0 800" SINGLE_GLOBAL_SELECTION="true">
<USERDATA name="HELLO_PAGE">
<PROPERTIES><PROPERTY key="LAYER" type="2" value="10" /></PROPERTIES>
</USERDATA>
<CHILDREN>
<!-- element list: Placeholder, RectShape, Text, PageInstance -->
</CHILDREN>
<DEFAULT_ELEMENTS>
<DEFAULT_ELEMENT CONTROLLER="255" ID="p_prompts_navbar" />
</DEFAULT_ELEMENTS>
</Area>
</CHILDREN>
<GENERICOBJECTTABLE name="hello">
<GENERICOBJECTS>
<GENERICOBJECT name="HELLO_PAGE">
<LINK slot="101" LASTOBJECTTYPE="Page" IDS="hello HELLO_PAGE" />
</GENERICOBJECT>
</GENERICOBJECTS>
</GENERICOBJECTTABLE>
</MagmaPackage>
Three parts of that are not optional and not obvious:
<TYPES>is a fixed 166-entry block. It is byte-identical in all 50 shipped packages (verified: one distinct block across the corpus), so theslot="…"numbers are constants for this build —101is alwaysPage,74alwaysImage. Copy the block verbatim and use the slot table. Do not renumber it.POOLCOUNTSis 65 memory-pool hints. All-zero works; they pre-reserve allocation chunks and affect no offset.- The
GENERICOBJECTTABLEentry is what makes the page reachable.CUIPageBase::Inithashes a page-name string and looks it up throughGenericObjectServer::FindGenericObject. No entry, no binding — the package loads and draws nothing.
Ten rules that keep a package loadable
- Copy the
<TYPES>block; never invent slot numbers. A slot that resolves to a class outside the fiveMakeArea/ fourteenMakeElement/ nineMakeActionExecutersets makes the engine dereference a null pointer with no guard. - Geometry is
u16, and negatives wrap.x = -181is written65355. Every coordinate, includingLEFT/RIGHT/TOP/BOTTOMandPOSITION, is unsigned 16-bit. 11% of the corpus'sImageStatecoordinates are wrapped negatives. RectStateorder is LEFT, RIGHT, TOP, BOTTOM — not left/top/right/bottom. Getting this wrong produces a plausible-looking rectangle in the wrong place.- Colours are ARGB (
0xAARRGGBB), authored as an 8-hex-digit attribute.00FFFFFFis transparent white — the start of a fade, not cyan. - The keyframe state class is decided by the widget class, not by you. An
Imageelement's keyframes areImageState, aText's areTextState, aPageInstance's areScaleState. See the state table. - Every element needs at least one keyframe. Zero keyframes means no geometry and nothing drawn. There are no zero-keyframe elements in the corpus.
- A cross-package
AREAreference names a package by hash of its bare name (PACKAGE="common"), aMATERIALLINKnames it by path string (PACKAGE="\common.mgb"). Two different conventions in the same file; mixing them silently fails to resolve. - A material's texture path resolves against the package's own name, so a package the engine knows by a wrong path renders untextured white quads rather than erroring.
FullLinkIDSis a path, not a name.IDS="fcse FCSE_PAGE p_menu_nav #36150990 l_menu_nav_list"walks package → page → element → the area that element instances → element inside it. Every step must exist.- Actions come from a closed registry. You can only fire action names the game already
registers with
ActionServer— 6 magma built-ins plus 81 hardcoded game actions, with no data path into the table. The registry and how it dispatches; the names shipped packages use.
Where the rest of the knowledge lives
.mgb/.mgb.descformat — the wire layout, the type table, the validation record. Read this if you are writing a tool, not a screen..mgbfield names — the per-offsetLoadVisitorjoin and the fullUtil::GetTypetag table these pages quote enum names from.- The menu system — how a native C++ page class binds
to a Magma layout, what
CUIPageBase::FetchMagmaElementsrequires by name, and how FCSE reaches a page of its own.