Oblivion Mod:Oblivion XML
Oblivion's user interface is built using a proprietary XML dialect, and the relevant files can be easily edited by mod authors.
General concepts[edit]
Oblivion's UI is actually a 3D scene rendered overtop any gameplay or other displays. Every UI element has 3D geometry constructed for it at run-time; this geometry typically consists of numerous flat shapes layered overtop each other. As such, concepts related to 3D rendering — UV mapping, texture filtering, and so on — are occasionally relevant.
The user interface is defined using XML files. These files can define tiles — objects in a menu — and specify their traits — different aspects of their configuration, such as their size, position, or color. Traits can be specified as constant values, or as operators that can compute values dynamically.
Trait operators can be used to create interactive behaviors within menus, but this use is very specialized and very rare. It's far more common for behaviors to be provided by the game engine itself. Every menu consists of custom code in the engine, which interacts with specific tiles within the XML. These tiles are identified by their id trait values.
Some basic UI code would look like this:
<rect name="container"> <locus>&true;</locus> <x>0</x> <y>0</y> <width>500</width> <height> <!-- Enforce a 16:9 aspect ratio --> <copy src="me()" trait="width" /> <div>16</div> <mult>9</mult> <!-- The value is computed at run-time. This code means: copy my width, divide it by 16, and multiply it by 9. --> </height> <image name="icon"> <filename>some_picture.dds</filename> <!-- Set the image to use its "natural" size: --> <width> <copy src="me()" trait="filewidth" /></width> <height><copy src="me()" trait="fileheight" /></height> <!-- Position this image on the bottom-right corner of its container. --> <x> <copy src="parent()" trait="width" /> <sub src="me()" trait="width" /> </x> <y> <copy src="parent()" trait="height" /> <sub src="me()" trait="height" /> </y> </image> </rect>
That code sample defines a box in the upper-left corner of the screen, which is five hundred pixels wide and has a 16:9 aspect ratio. We could've computed the height ourselves, but we used operators to save us the trouble.
References[edit]
Prefabs[edit]
You can package any content into a reusable XML file, called a "prefab." Prefabs can be stored anywhere in Data\menus\prefabs\
, including in subfolders. A prefab can be used like this:
<include src="myPrefab.xml" />
When a prefab is used, the code is supposed to be included directly into the XML being parsed, as if you had simply copied and pasted it. This generally means that you can use prefabs for a collection of traits, for defining child tiles, or more. However, the XML parser can get fussy in some cases, such as when one prefab includes another under certain yet-to-be-determined circumstances.
Fundamental traits[edit]
The most important traits are:
- id
- Tells the executable that a tile is to be used for a specific purpose. The same ID number will have different meanings across each menu. IDs are generally responsible for clickable buttons, list panes (the IDs denote where to insert list items), readouts for stats and other values, and so on.
- x
- y
- width
- height
- Position and size values. For Text elements, the width and height are set automatically.
- locus
- A tile is positioned relative to the coordinates of the nearest ancestor tile whose locus trait is
&true;
. - filename
- For Image and NIF files, the texture or mesh file to display.
- filewidth
- fileheight
- The size of an Image tile's texture file, in pixels.
- string
- The text that a Text tile should display.
The others all come in handy in their own ways, but these are the traits you need to understand to be able to find your way around a file.
File formats[edit]
DDS[edit]
The DirectDraw Surface format is used for textures. It supports multiple compression modes. DXT1 (DirectX Texture 1) compression is used for textures with no alpha transparency or one-bit alpha transparency (i.e. every pixel is either fully opaque or fully transparent). The DXT3 and DXT5 compression algorithms are used for textures with full alpha translucency, but they produce files that are twice as large as DXT1. DXT3 is theoretically better for textures with steep alpha changes, while DXT5 is theoretically better for textures with smooth alpha gradients.
Note that Oblivion has three texture folders for menus: Menus, Menus50, and Menus80. The game apparently decides which textures to use based on the user's screen aspect ratio – specifically, the ratio of the true screen height in pixels to the UI-normalized screen height (i.e. 0.5, 0.8). The game will default to using textures from the Menus folder when the Menus50 and Menus80 files are missing.
The user's texture quality setting (iTexMipMapSkip in Oblivion.ini) also affects menu textures. Be warned: some popular DDS exporters (particularly GIMP-DDS) do not save non-mipmapped textures properly; they write a mipmap count of 1 when it should be 0. This causes the textures to display corrupted pixels if the player's texture quality isn't at the maximum setting. If you really must use non-mipmapped textures exported through GIMP, then you can fix this by hex editing your exported DDS files: change the four-byte value at offset 0x1C to zero.
Oblivion attempts to use bilinear interpolation on all menu textures. However, texture filtering is broken for an unknown reason. Code analysis has indicated that the problem occurs whenever the menu cursor is receiving texture filtering (which, in the vanilla game, is always). The effect of this is that you may see odd seams or dithering on some textures, and high-resolution textures scaled down may look badly blurred or even blurred and pixelated.
NIF[edit]
Games that use Gamebryo (formerly NetImmerse) rendering tech rely on the NetImmerse Format for 3D models and scenes. The file format has several variations, as data structures are changed or added to accommodate evolving technologies and the needs of specific games; this means that NIFs made for one game generally can't be used in another without being converted somehow.
As mentioned earlier, Oblivion's entire UI is rendered in 3D, and geometry is constructed for UI elements at run-time. However, the NIF tile type can be used to insert custom-made 3D geometry into the UI directly. This is commonly used to create animating elements.
XML[edit]
eXtensible Markup Language is a text syntax used to represent arbitrary data. A simplified XML dialect is used to define the content of Oblivion's menus.
Oblivion's XML parser takes a number of shortcuts:
- CDATA is not supported.
- XML headers/declarations and doctypes are not supported.
- Namespaces are not supported.
- Standard XML and HTML entities are not supported.
- Attribute values must be placed in double-quotes, not single-quotes.
- Everything is case-insensitive.
- Leading and trailing whitespace is ignored in all elements. "Whitespace" is defined as any single-byte character with a code lower than or equal to 0x20.
- The only supported attribute for tile elements is "name." This attribute is required; if it is missing, then parsing will fail and the game will crash.
- Attributes not explicitly specified in Oblivion's XML grammar should be avoided. It is unlikely that the parser will handle them properly.
- Oblivion only allocates 128 bytes for tag names, including a null terminator. Longer tag names are expected to cause memory corruption.
- Oblivion only allocates 4096 bytes for text content, including a null terminator. "Text content" refers to attribute names, attribute values, and any chunk of plain-text inside of an element (not including leading whitespace, but potentially including trailing whitespace). Longer spans of text content are expected to cause memory corruption.
- All content before the first "<" glyph in the file will be ignored.
Moreover, Oblivion's XML parser sometimes fails to distinguish between numeric and string values within XML elements. A value is considered "numeric" if it only contains digits, dashes, and periods. This means that the following values are all read as the number zero:
- 0
- 0.0
- -0.0
- .
- ...
- -.-.-.-.-.-
- ---
Advanced information[edit]
Screen size[edit]
Oblivion does not render its UI at native resolution; rather, it tries to normalize your current screen resolution. If the screen is wider than it is tall, then the game normalizes the UI resolution to a height of 960 pixels with a variable width; otherwise, the game normalizes the UI resolution to a width of 1280 pixels with a variable height. Attempting to retrieve the width or height from the screen()
selector will retrieve the normalized screen size; the UI has no way of directly accessing the true resolution.
A consequence of this is that content displayed in the UI will not map one-to-one with the source assets, even accounting for the minor distortions caused by 3D perspective and depth. If your screen resolution is, say, 1600 by 900 pixels, then the UI will render as if it were on a 1707x960px canvas and downscaled.
List panes[edit]
The list itself consists of a "clip window," which crops its visible contents to its bounds (compare to CSS overflow
); and an inner "pane" that slides up by the scroll index times the row height. (The scroll index is pulled directly from the list's scrollbar.)
Accordingly, all list items must be the same size on the axis along which the content scrolls. Oblivion XML doesn't allow tiles to size or scroll dynamically enough for anything else. One exception to this rule has been found in the vanilla menus: the lists of quests and quest stages in MapMenu allows for dynamic item sizes. However, the MapMenu class appears to do extra work to allow for this; the scrollbar scrolls by pixels instead of by whole rows, and the MapMenu still has to compute the height of the list contents and supply that height to the scrollbar as a max scroll position.
Oblivion XML does not directly control the scroll wheel behavior; the executable has its own rules for discerning when the scroll wheel "should" scroll a particular list. Typically speaking, the list pane must have a particular id, or must be targettable.
[edit]
It can be tricky to set up a list so that navigating off of either end brings you to the other end. You can't have list items conditionally use &last;
or &prev;
, or &first;
or &next;
, depending on their position in the list. Keyboard navigation will become buggy when &first;
and &last;
are used by list items inside of an &xlist;
container, and the reasons for this are not known.
What you have to do instead is define xup and xdown handlers on the xlist-container itself, and set them to &last;
and &first;
, respectively. Those handlers will only fire when a child xitem's own handlers fail to resolve to a valid tile. In plainer terms: the list container's xup only fires if you can't go up from the current list item, and the list container's xdown only fires if you can't go down from the current list item, and using the first and last entities from the list container does work.
[edit]
The list panes must have keyboard navigation traits pointed at each other (e.g. Pane 1's xright points to Pane 2, and Pane 2's xleft points to Pane 1). The list panes must not be targetable, but they and their list items must have xdefault traits set. The list panes must have their xlist trait set to &xlist;
, and the list items must have their xlist traits set to &xitem;
.
The panes can be "adjacent" or "sequential." That is, you can have one vertical list blend seamlessly into another vertical list by using xup and xdown on the lists, even if list items also use xup and xdown. If the list items' navigation traits fail to find a suitable tile, Oblivion searches upward until a suitable tile (such as the list container) is found.
Oblivion will remember the cursor's position in each list when moving back and forth across them. The game accomplishes this by modifying the xdefault trait values on the list items at run-time.
Parser overview[edit]
Oblivion uses a "two-and-a-half"-stage parser. The first stage examines a document's markup and generates a series of very basic, very generic tokens. The first-and-a-half stage examines those tokens and combines them when possible. The second stage converts tokens into Tile
objects in memory. Notably, templates only go through first- and first-and-a-half-stage parsing, and the resulting tokens are stored on each Menu
object in memory, keyed to the template name.
References between tiles (by way of operators using the src
attribute) are resolved at parse-time, and are not live-updated. That is to say: you cannot refer to a tile that doesn't exist yet (e.g. because it's generated from a template) and have that reference suddenly start working when the tile is generated. It is impossible for non-templated content to refer to templated content.
All relevant XML data — tile types, traits, operator types, and parse-time tokens — is identified using a common numeric ID space. Underscore-prefixed traits have IDs generated at runtime, starting from 10000. Code analysis suggests that these IDs are refcounted, and possibly recycled when such a trait is no longer in use.
Interactive widgets using XML-side behaviors[edit]
It is possible to maintain state and respond to mouse clicks purely within XML.
The vanilla scrollbars are a mixture of executable-side code and XML-side code. The menus' compiled code handles dragging the scrollbar thumb, but all click actions are responded to by XML. This behavior is not unique, and could in theory be replicated. To create such behaviors, you must understand two principles.
Traits are updated when they change, or when they depend on something that changes.
When the value of a trait is changed by the executable, it will be recalculated in full. Any trait whose operators select the changed trait will also be recalculated in full.
This is most useful when dealing with the clicked trait. When a tile is clicked on, its clicked trait is changed to 1 (triggering an update) and then changed back to 0 (triggering another update) on the same frame.
A trait that doesn't use copy operators can remember its previous computed values.
You can remember state by failing to use a copy operator in a trait. If the first operator isn't copy, then the current working value will be the last value computed for the trait. Consider:
<user0> <add src="me()" trait="clicked" /> </user0>
Every time the tile is clicked (assuming it's targetable), the clicked trait will be recalculated. This will trigger recalculation of the user0 trait. Every time the user0 trait is recalculated, the value of the clicked trait will be added to the previously calculated user0 value. This means that every time the tile is clicked, 1 will be added to the user0 trait, and then 0 will be added to the user0 trait. The user0 trait shown here tracks how many times the tile has been clicked on.
The vanilla scrollbars exploit this behavior. Clicking on the buttons or track triggers recalculation of the scrollbar thumb's scroll value. The scrollbar thumb takes its previously-calculated scroll value, adds in the clicked trait on the scrollbar buttons (multiplied by the scrollbar step value), adds in the clicked trait on the scrollbar track (multiplied by the scrollbar jump value), and constrains the result to the scrollbar's minimum and maximum values. This calculation, expressed in a plain syntax, is:
last computed value + ( (("scroll down" clicked) - ("scroll up" clicked)) * scrollbar step size ) + ( (("bottom half of track" clicked) - ("top half of track" clicked)) * scrollbar jump size ) all constrained to minimum and maximum
The vanilla scrollbars are also designed so that the executable can influence this behavior. The scroll value calculation includes two traits, user5 and user9, that can be used to adjust the scroll value (absolutely or in steps). With that in mind, the true calculation is:
last computed value + ( ( ("scroll down" clicked) - ("scroll up" clicked) + (user9) ) * scrollbar step size ) + ( (("bottom half of track" clicked) - ("top half of track" clicked)) * scrollbar jump size ) + user5 all constrained to minimum and maximum
When Oblivion wants to forcibly scroll a pane to the top, it:
- Sets the user5 trait to -999999.0. This triggers recalculation of the scroll value, which inevitably gets clamped to the scrollbar minimum.
- Sets the user5 trait to 0.0. This ensures that the next time the scroll value is recalculated (e.g. because the user clicks the scrollbar), user5 doesn't interfere.
When Oblivion wants to forcibly scroll a pane to a particular position, it:
- Sets the user5 trait to -999999.0. This triggers recalculation of the scroll value, which inevitably gets clamped to the scrollbar minimum.
- Sets the user5 trait to the target position. This triggers recalculation of the scroll value again: user5 is added to the current value.
- Sets the user5 trait to 0.0. This ensures that the next time the scroll value is recalculated (e.g. because the user clicks the scrollbar), user5 doesn't interfere.
Oblivion behaves similarly when the scrollbar is dragged, using user9 to influence the final scroll value.
Armed with all this information, you should be able to make similar widgets of your own. Some ideas:
- A carousel, where "left" and "right" buttons can be clicked to advance through a set of numbered pages. If you use the mod operator to clamp the current page number, instead of min and max, then you can make it so that going past the last page brings you back to the first, and vice versa.
- "Add" and "subtract" buttons, using the same implementation as the carousel, with min and max to clamp.
- A simple checkbox, which can work by counting clicks and displaying its state as the click count modulo 2.
- A set of radio buttons, where each button subtracts the clicked trait of the other buttons.