Oblivion

The "pref" system, introduced in NorthernUI 2.0.0, allows UI authors to implement user-configurable options without requiring users to edit files manually, and without having to hardcode the options into a DLL. Each such option -- a "UI pref" or simply "pref" -- is defined in a special XML file and can have its value changed at run-time using specific XML operators. In essence, Oblivion XML traits can be used as setters -- as "code," so to speak, rather than content.

I implemented this solely so I could add HUD options and whatnot without having to hardcode every single one of them; the system is totally unnecessary for any content authors who are using scripting. I'm documenting the system for the public because there are quite a few mods that inject content into the HUD, and some are actively maintained and attempt to be compatible with NorthernUI; since NorthernUI 2.0 uses prefs as the basis for not only HUD scaling options, but also options to change large portions of the HUD layout, any mods wishing to be fully compatible will need to know at least the basics of how this system works and how to use it -- enough to check the values of the "layout" and "scaling" options.

Overview
At startup, NorthernUI will open every *.XML file in Oblivion/Data/menus/NorthernUI/prefs/. These files will be assumed to contain definitions for UI prefs. Each definition consists of the pref's name and its default value. A basic pref file should look like this:

<?xml version="1.0"?>
<prefset>
<pref name="NameOfMyPref" default="5" />
</prefset>


Once a pref is defined, you can use its value in menu XML:

<rect name="example">
<red> <copy src="xxnPrefs()" trait="_NameOfMyPref" /> </red>
</rect>


You can also modify the value in menu XML:

<rect name="checkbox">
<_setter>
<copy src="me()" trait="clicked" />
<xxnOpPrefModifyValue>NameOfMyPref</xxnOpPrefModifyValue>
<copy>2</copy>
<xxnOpPrefCarousel>NameOfMyPref</xxnOpPrefCarousel>
</_setter>
</rect>


And you can reset it to its default from menu XML:

<rect name="reset_button">
<_resetter>
<copy src="me()" trait="clicked" />
<eq>1</eq>
<xxnOpPrefResetValue>NameOfMyPref</xxnOpPrefResetValue>
</_resetter>
</rect>


The current values of all prefs are saved in Oblivion/Data/Plugins/OBSE/NorthernUI.xmlprefs.ini.


System details
How are prefs made available to the UI?
NorthernUI creates a hidden tile whose traits can be referenced by way of the xxnPrefs() selector. It then takes each pref, prefixes the prefs' names with an underscore, and creates traits with these names on the hidden tile. The values of these prefs are synchronized to these traits, such that operators can read the value of a pref using the SRC attribute.

Note that Oblivion discards operators that refer to a non-existent tile, and that the xxnPrefs() tile will not exist if NorthernUI is not installed. Using this code snippet as an example, if NorthernUI isn't installed, then the trait's value will always be 5, and the <mult /> operator won't exist in memory.

<rect name="example">
<red>
<copy>5</copy>
<mult src="xxnPrefs()" trait="NameOfMyPref" />
</red>
</rect>


If you're using prefs, but you also need your code to work predictably even when NorthernUI is not installed, then you can work around this by supplying a fallback value:

<rect name="example">
<red>
<copy>5</copy>
<mult>
<copy>0</copy> <!-- fallback -->
<copy src="xxnPrefs()" trait="NameOfMyPref" />
</mult>
</red>
</rect>


How can the UI change a pref's value? When does that happen?
Specific operators have been made available and can be used to change the value of a pref. Changes are applied at the last possible moment. In order to understand what this means, we must first understand how Oblivion handles changes to traits. Consider the following code:

<rect name="button">
<!-- pretend I cared enough to write the code that makes something clickable here -->
</rect>
<text name="counter">
<user0>
<add src="button" trait="clicked" />
</user0>
<string>
<copy src="me()" trait="user0" />
</string>
</text>


When the button is clicked, the game sets its "clicked" trait to 1. The game then looks through everything that pulls the "clicked" trait, and finds the "user0" trait on the counter. The game fully recomputes the user0 trait (by executing all of its operators in sequence), and then it looks through everything that pulls the "user0" trait. It finds the "string" trait on the counter, updates that, and sees that nothing pulls the trait. Satisfied, the game wraps up and is then done handling that one change to the "clicked" trait. (And then the game sets "clicked" back to 0 on the same frame, and the process repeats.) You may recognize that this is a recursive process.

Operators, then, will "run" in three situations: when the game finishes parsing the operator while reading your menu's XML; when the game finishes parsing the operator's containing trait; and then any time the operator's containing trait recomputes its value as part of the above recursive process. This applies both to ordinary operators, like add, and to the operators that the UI pref system uses to modify a pref's value.

When a pref operator runs, we change the pref's value immediately, but we don't save the change to a file, and we don't immediately forward the change to the xxnPrefs() tile. Instead, we wait until the trait recomputation process is finished -- until we're "outside" of all recursion -- and then we forward the change to the xxnPrefs() tile (which itself causes a trait recomputation). We still don't save the pref change to a file; that happens only when you exit the operator's containing menu.

Questions and common pitfalls
Why are prefs only saved when a menu is closed?
The main motivation for this was sliders and scrollbars (which, actually, are the same thing in Oblivion).

Sliders use XML to compute their values; all of the buttons are handled entirely by trait operators. Dragging, however, requires help from the executable. Accordingly, every slider has a spare trait that is used solely to give the executable an opportunity to influence the slider's value. When the executable wants to force a slider to a certain value, it first forces that specific trait to -999999; this forces the slider to an extremely low value, which is inevitably clamped to the slider's XML-defined minimum. Then, the executable forces the trait to the desired value, which updates the slider. Finally, the executable forces the trait back to 0, so that it doesn't do anything the next time you click a slider button. So, that's three changes made to the trait on every frame you drag the slider thumb.

(Ordinary clicks result in two trait changes per frame (the "clicked" trait is set to 1 and then back to 0).)

So, if we made changes to a pref every time a pref operator ran, then dragging a scrollbar would cause us to access and modify the saved pref file on the user's hard drive three times per frame. Yikes! It's much cleaner, much more performant, and much less stressful on the hard drive to just save settings when the menu closes.

Why is there an operator to "modify" a pref's value, but not an operator to "set" a pref's value?
A "set" operator would only be useful if we allowed XML to manage state, as is done for the values of sliders and scrollbars in the vanilla game. XML can manage state by using traits that never overwrite their last-stored values with <copy /> operators; however, these traits always initialize to zero. It is impossible to set the initial values of these traits to anything else due to how the game actually populates/computes/fills/etc. operator operands at run-time.

Consider a trait like this:

<trait>
<operatorThatReturnsAValueOnlyOnceAndThenDoesNothing>
<someHypotheticalLoadPrefOperator src="otherTile" trait="_prefName" />
</operatorThatReturnsAValueOnlyOnceAndThenDoesNothing>
<add src="otherTile" trait="clicked" />
<someHypotheticalSetPrefOperator src="otherTile" trait="_prefName" />
</trait>


When an operator is parsed and created, its containing trait is told to immediately recompute its value. This means that the <trait /> trait here actually computes its value multiple times before its containing tile has finished loading. The game also tells all traits in a tile to recompute when the tile itself is finished loading -- just for good measure.

If any SRC operator in this trait experiences an update, then the trait will recompute at that time as well. This is what allows this trait to react to (otherTile) being clicked.

The (otherTile) you see referenced in there may not have had its traits load yet, in which case any operator referencing it has an operand of zero. This means that our "run once" operator will always return zero. Eventually, the trait in (otherTile) will load in, causing our <trait /> trait to recompute, but by then, our "run once" operator has already fired and disabled itself from running again.

The fact of the matter is, there's no way to know when all SRC operators inside of a trait have resolved their values, and so there's no way to know when it's safe to run any sort of "run once" operator. Without a "run once" operator, you can't initialize an XML trait that maintains state to any value other than zero. This means that widgets for changing UI prefs, like sliders and checkboxes, wouldn't be able to display the pref's current value when their containing menu is opened.

If we forego a direct "set" operator and just use a "modify" operator, with one or two other specialty operators as needed, then we can use a setup like this:

<_setter>
<copy src="otherTile" trait="clicked" />
<xxnOpPrefModifyValue src="otherTile" trait="_prefName" />
</_setter>
<trait>
<copy src="xxnPrefs()" trait="_prefName" />
</trait>


In this case, XML doesn't maintain state at all. We just tell the game when to make changes.

Why do we use specific operators, like "modulo" and "carousel" and "clamp?" Can't we just do that stuff with the "modify" operator?
Consider the case of a checkbox: in order to map its value to true and false, we'd want a special variant on a modulo operator: we'd want to leave the value unchanged if it equals 2, but modulo it otherwise. So why can't we implement it in XML? Well, consider this:

<_setter>
<copy src="xxnPrefs()" trait="_Pref" />
<sub>
<not src="xxnPrefs()" trait="_Pref" />
</sub>
<mult>-1</mult>
<mult src="otherTile" trait="clicked" />
<xxnOpPrefModifyValue>_Pref</xxnOpPrefModifyValue>
</_setter>
<user0> <!-- some trait to display the current state -->
<copy src="xxnPrefs()" trait="_Pref" />
</user0>


In theory, that should achieve what we want, right? That should toggle the value between 2 and 1, by subtracting 1 whenever the current pref value is 2 and adding 1 whenever the current pref value is 1. However, there's a subtle problem here.

When you click on a tile, the game sets the "clicked" trait to 1, and then sets it back to 0, all on the same frame. When the game finishes changing a trait's value -- including all of the other operators that have to update in response -- we process any pref value changes that occurred and propagate those changes to the UI. So here's the sequence of events that occurs with the above markup:

1) The game sets "clicked" to 1. The _setter trait contains an operator that refers to the "clicked" trait, so the _setter trait is forced to recompute its value, executing all of its operators in sequence. This flips the checkbox pref's value between true and false.

2) The operators in _setter make changes to a pref's value.

3) The game finishes processing all traits and operators that had to update in response to "clicked" being set to 1. We now push all of the pref changes that were made to the UI...

4) ...which entails setting the "_Pref" trait on xxnPrefs(). The _setter trait contains an operator that refers to that "_Pref" trait, so the _setter trait is forced to recompute its value, executing all of its operators in sequence.

5) The operators in _setter make changes to a pref's value. Our code has now run twice in a row, with the second run totally cancelling the first run out.

6) The game finishes processing all traits and operators that had to update in response to the "_Pref" trait changing. (Fortunately, NorthernUI's DLL checks for recursion here, and it'll never update xxnPrefs() as a result of xxnPrefs() being updated; if that check wasn't there, then we would become trapped in infinite recursion.)

7) The game sets "clicked" to 0. This kicks off the same sequence of events as when "clicked" was set to 1, except that since we <mult /> everything by "clicked," we just end up trying to modify the pref's value by zero twice in a row (which does nothing -- the "modify" pref operator is programmed to ignore changes-by-zero).

So prefs can only be modified with hardcoded operators: if the same trait that tries to modify a pref's value also pulls that pref's value (however directly or indirectly), then the modification goes wrong. Which is why we...

<_setter>
<copy src="me()" trait="clicked" />
<xxnOpPrefModifyValue>_Pref</xxnOpPrefModifyValue>
<copy>2</copy>
<xxnOpPrefCarousel>_Pref</xxnOpPrefCarousel>
</_setter>


There. A working checkbox that alternates between &false; and &true;.

Reference
Available operators
Before we can go over the operators available for altering prefs, we must first define our terms. Consider the add operator in this code snippet:

<copy>5</copy>
<sub>1</sub>
<add>3</add>


The add operator returns the result of ((5 - 1) + 3), or (4 + 3). Here, 4 shall be called the "input," and 3 shall be called the "operand." The operator's output, 7, shall be called the "result" or the "return value."

There are two things, also, to note about any operators that take a pref name as their operand: these operators are not guaranteed to be compatible with MenuQue's undocumented string operators (e.g. "append"); and it doesn't matter whether the pref name is prefixed with an underscore (i.e. "_prefName" is treated as "prefName" for consistency with xxnPrefs()).

xxnOpPrefCarousel
The operand is a string and indicates the name of the pref to modify. This is similar to a modulo operator, except that it never returns zero or a negative number. If the input is zero, then this operator does nothing; if the pref's current value is less than or equal to zero, then the pref will be set to the input; if the pref is already equal to the input, then it shall not be changed; otherwise, the pref will be set to its current value modulo the input.

This operator's result is the input, unmodified.

xxnOpPrefClampToMax
The operand is a string and indicates the name of the pref to modify. The pref will be set to whichever is larger: its current value, or the input.

This operator's result is the input, unmodified.

xxnOpPrefClampToMin
The operand is a string and indicates the name of the pref to modify. The pref will be set to whichever is smaller: its current value, or the input.

This operator's result is the input, unmodified.

xxnOpPrefModifyValue
The operand is a string and indicates the name of the pref to modify. The pref's value will be increased by the input. (If the input is zero, then no change is made.)

This operator's result is the input, unmodified.

xxnOpPrefModulo
The operand is a string and indicates the name of the pref to modify. The pref will be set to its current value modulo the input.

This operator's result is the input, unmodified.

xxnOpPrefResetValue
The operand is a string and indicates the name of the pref to modify. If the input is equal to 2 (i.e. &true;), then the pref will be reset to its default value. Otherwise, this operator does nothing.

This operator's result is the input, unmodified.


Pref definitions
NorthernUI will look over the Oblivion/Data/menus/NorthernUI/prefs/ folder and attempt to load any XML file inside as a "pref definitions" file. NorthernUI only checks for loose files; BSA-packaged files in this directory will not be detected or loaded.

(NorthernUI v2.0.0 and v2.0.0.1 used a different path, Oblivion/Data/NorthernUI/prefs/. This had to be changed in order to simplify installation for Wrye Bash users; that mod manager intentionally ignores any folders it doesn't recognize unless manually told to do otherwise.)

If an XML file fails to parse, then nothing will be done with the file. If major errors are found in the parsed data, then all content after the error will be skipped. If minor errors are found in the parsed data, then the affected data will be thrown out.

Major errors

  • A root node that is not named <prefset />
  • A <pref /> element nested inside of another <pref /> element

Minor errors
  • A pref element with no name; the pref will be discarded
  • A pref whose name starts with an underscore, or whose name contains spaces, slashes (forward or back), double-quotes, at-signs, or parentheses; the pref will be discarded
  • A pref with the same name as another pref; the earliest-loaded pref is the one that is kept
  • A pref that has one or more min-version attributes, none of which have valid values; the pref will be discarded
  • A pref that has one or more max-version attributes, none of which have valid values; the pref will be discarded

Warnings
  • A pref with multiple name attributes; the last name encountered during parsing will be used
  • A pref with multiple default values; the last valid value encountered during parsing will be used
  • A pref whose default value is not a valid number; the value will be skipped
  • A pref with no valid default value; it will use 0 as its default value (I reserve the right to change how the mod reacts to this in the future)
  • A pref with multiple min-version attributes; the last valid version number encountered during parsing will be used
  • A pref with multiple max-version attributes; the last valid version number encountered during parsing will be used

• • •

The following attributes are considered valid for pref elements:

name
Required. Specifies the pref's name.

default
Specifies the pref's initial value.

min-version
Optional. A series of one or more numbers separated by periods, indicating a version number. If this attribute exists and NorthernUI's version number is below the one specified here, then the pref will not be loaded into memory. The value of this attribute is considered invalid if: it is zero-length; it contains any glyphs other than numeric digits and periods; it contains two or more consecutive periods; it starts or ends with a period; it contains more than four numbers separated with periods; or any of its numbers are higher than 2147483647.

max-version
Optional. A value of the same format and with the same constraints as that of the min-version attribute. If this attribute exists and NorthernUI's version number is above the one specified here, then the pref will not be loaded into memory. Note that you are allowed to specify just a min-version attribute or just a max-version attribute; you don't have to use both.

• • •

The XML parser is custom-made and not fully spec-conformant, but it is stricter than Oblivion's parser and enforces the following rules:

  • XML declarations are optional and their contents are ignored, but if they are present, then they must be at the very start of the file. Nothing -- not even comments or whitespace -- may precede them.
  • Unrecognized XML entities are a syntax error and cause the parser to abort. Documents parsed as UI prefs support all of the XML entities defined in Oblivion XML, including those defined by DLLs.
  • CDATA is supported in text-content, tag names, and attribute names.
  • Improperly-nested tags and unclosed tags are a syntax error, unlike in Oblivion, and will consistently cause parse failures.
  • Per spec, two consecutive dashes ("--") are not allowed inside of comments except as part of the start or end delimiters.
  • Attribute values may be wrapped in double-quotes or single-quotes, unlike in Oblivion, which only supports double-quotes.

All errors and warnings are written to NorthernUI's log file. The log file also lists the values of all loaded prefs at startup.

Appendix A: Pref operators for common widgets
Checkbox
This allows for values 1 and 2.

<_setter>
<copy src="me()" trait="clicked" />
<xxnOpPrefModifyValue src="me()" trait="_nameOfMyPref" />
<copy>2</copy>
<xxnOpPrefCarousel src="me()" trait="_nameOfMyPref" />
</_setter>


Enumpicker
This widget allows you to cycle through a set of predefined values using a "next" button and a "previous" button. The XML below allows for values between 1 and _optionCount, inclusive.

<_setter>
<copy>0</copy>
<add>
<copy src="child(button_right)" trait="clicked" />
<sub  src="child(button_left)" trait="clicked" />
</add>
<xxnOpPrefModifyValue src="me()" trait="_nameOfMyPref" />
<copy src="me()" trait="_optionCount" />
<xxnOpPrefCarousel src="me()" trait="_nameOfMyPref" />
</_setter>


Slider
To be put on the slider's thumb:

<_setter>
<copy>0</copy>
<add>
<copy src="sibling(horizontal_scroll_right)" trait="clicked" />
<sub  src="sibling(horizontal_scroll_left)" trait="clicked" />
<mult src="parent()" trait="user3" /> <!-- step distance -->
</add>
<add>
<copy src="sibling(horizontal_scroll_rightside)" trait="clicked"/>
<sub  src="sibling(horizontal_scroll_leftside)" trait="clicked"/>
<mult src="parent()" trait="user4" /> <!-- jump distance -->
</add>
<add src="me()"     trait="user9" />
<add src="parent()" trait="user5" />
<xxnOpPrefModifyValue src="parent()" trait="_nameOfMyPref" />
<copy src="parent()" trait="user1" />
<xxnOpPrefClampToMin src="parent()" trait="_nameOfMyPref" />
<copy src="parent()" trait="user2" />
<xxnOpPrefClampToMax src="parent()" trait="_nameOfMyPref" />
</_setter>

Article information

Added on

Edited on

Written by

DavidJCobb

0 comments