MCM Advanced Features
Clone this wiki locally
- Script Initialization
- Custom Content
- Key Conflict Management
- Script Versioning
Normally, when working with arrays or other variables that cannot be initialized immediately at the point of declaration, you have to put initialization code in
For your config menu, however, you should rather use the
OnConfigInit event we provide and avoid
If that's not possible and you have to use
OnInit for some reason, make sure to call
parent.OnInit() the original event handler of SKI_ConfigBase is not skipped.
string myArray ; WRONG event OnInit() myArray= new myArray ; ... endEvent ; Meh... avoid using OnInit() event OnInit() parent.OnInit() myArray= new myArray ; ... endEvent ; OK event OnConfigInit() myArray= new myArray ; ... endEvent
The reason you should avoid
OnInit() is that config menus should be designed so that MCM (which is part of SkyUI) can be removed at any time and added again later. If that happens, the variables of your script will be reset, BUT
OnInit() won't be called again. This may leave some of variables uninitialized.
OnConfigInit() doesn't have this problem.
There are certain settings you might want to apply after each game reload, for example scripted INI changes or other modifications that persist in memory.
To do this, extend
OnGameReload of SKI_ConfigBase. Be aware that this is not part of the base API, so you'll be responsible for calling
parent.OnGameReload(). Not doing that will break the menu.
event OnGameReload() parent.OnGameReload() ; Don't forget to call the parent! Utility.SetINIFloat("someSetting", someValue) endEvent
If your config menu contains a lot of options, you might want to consider diving them into several categories. Or, even if all options would fit into a single page, it might still be a good idea to separate general from advanced settings.
For this purpose config menus support multiple pages. Setting them up is straightforward.
Define your page names by setting the
Pages property of your config menu. You can either do that in the Creation Kit property editor, just like you set
ModName, or you can do it in the
OnConfigInit event of your script. An example for the scripted variant:
event OnConfigInit() Pages = new string Pages = "Page 1" Pages = "Page 2" endEvent
Now, whenever your config menu is active, the list of pages will be shown below the mod name.
In general, setting up options for multiple pages works exactly the same as using a single page; you add them in
OnPageReset. The only difference is that you check the
page parameter when deciding which options to add:
event OnPageReset(string page) if (page == "Page 1") ; Add page 1 options elseIf (page == "Page 2") ; Add page 2 options endIf endEvent
By default, after selecting your mod from the main list, no page is active and the
page parameter is is
"". Since we didn't handle this case in our example, the option list will be blank until the user has chosen a page. On way to fill that void is adding a custom logo of your mod . For further details on this topic, see Loading custom content.
Pages and other events
Since each option ID is unique, for all the other events it won't matter which option a page is on. However, if you have a lot of options, you may want to spread your code over several functions.
In this scenario, it's helpful being able to check the current page even outside of
OnPageReset; you do that by accessing the
event OnOptionSelect(int option) if (CurrentPage == "Gameplay") HandleGameplayOptionSelect(option) elseIf (CurrentPage == "Immersion") HandleImmersionOptionSelect(option) endIf endIf
Be aware that this makes moving around options between several pages, or renaming pages, slightly more tedious, since you have to change more than just
OnPageReset when doing so.
While the basics about options have already been covered, there are two more topics that have been left out so far: Option flags and grouped updating.
Add*Option functions have an optional
flags parameter. It can be used to enable certain behavior for the option. Accepted flags are
OPTION_FLAG_NONE, to clear the flags;
OPTION_FLAG_DISABLED, to grey out and disable the option.
int flags if (isMyOptionDisabled) flags = OPTION_FLAG_DISABLED else flags = OPTION_FLAG_NONE endIf AddToggleOption("Toggle this", myToggleValue, flags)
To change the flags later in combination with
Set*OptionValue functions, use
If you use the
Set*OptionValue functions to change several options at once, Papyrus may slightly delay function execution at any point in the script. This results in asynchronous updating of the option display.
To prevent this,
SetOptionFlags support an optional
noUpdate parameter. Example:
SetTextOptionValue(oid1, "Value1", true) ; Don't redraw the list yet SetTextOptionValue(oid2, "Value2", true) ; ... SetTextOptionValue(oid3, "Value3", true) ; ... SetTextOptionValue(oid4, "Value4") ; Refresh now
It's possible to load content from an external file into the option list area. The original option list will be hidden when that happens.
Supported source file formats are SWF for Flash movies and DDS for images. PNG and some other image formats may work as well, but they will most likely be imported in bad quality.
Here's a practical example used in SkyUI to display the animated logo:
event OnPageReset(string a_page) ; Load custom .swf for animated logo that's displayed when no page is selected yet. if (a_page == "") LoadCustomContent("skyui/skyui_splash.swf") return else UnloadCustomContent() endIf ; ... rest of OnPageReset
The path of the loaded file is relative to Data/Interface/.
Note that you have to call
UnloadCustomContent() manually, to remove the custom content and show the original option list again. This isn't done automatically so custom content can stay active across several pages.
LoadCustomContent supports two optional parameters for X and Y position offset. (0,0) is the top left corner of the option list area. The dimensions of this area are 770x446, the horizontal center point is at (376,223), which is not exactly at half of the width because the right side holds the scroll bar. To calculate the offsets for a 256x256 image, have a look at this example:
X offset = 376 - (imageWidth / 2) = 376 - 128 = 258 Y offset = 223 - (imageHeight / 2) = 223 - 128 = 95
To support multiple languages in your config menu, you can utilize the UI localization capabilities provided by SKSE. This section will explain how this works exactly.
The game itself stores the translated strings in
LANGUAGE is replaced by the current game language, i.e.
Translate_ENGLISH.txt. This file contains lines of key/value pairs for the translated strings, separated by a tab stop. Each key has to start with the
$ sign. Example:
... $Back Back $Backstabs Backstabs $Barters Barters ...
Whenever the contents of a textfield in UI match a key in this file, it's replaced with the translated value. Even without SKSE, you could add your own translations by modifying the original translates file. The problem is that this would lead to conflicts if multiple mods want to extend this file.
That's why SKSE adds support to load translations from additional files. For each active mod in the load order,
Data/Interface/Translations/modname_LANGUAGE.txt is checked for translations, where
modname is replaced by the name of the mod data file (i.e.
SkyUI_ENGLISH.txt, if the data file is
SkyUI.esp). The translation format is the same as described above.
Important: The character encoding of the translation files must be UTF16 LE (aka UCS-2 LE) with BOM. Use a text editor like Sublime or Notepad++ to save with this encoding.
Be aware, however, that these translations only work, if the textfield contents match the translation key exactly. Given
"$Hello" results in
"$Hello World" stays
Loading translation files from inside BSA file is possible. Languages the game supports are CZECH, ENGLISH, FRENCH, GERMAN, ITALIAN, POLISH, RUSSIAN, SPANISH and JAPANESE. Only the file that matches your current language is loaded; there is no fallback to ENGLISH as default.
For simple words or short sentences, you should follow the convention of naming the key the same as the translated string:
$Are you sure? Are you sure?
For longer strings, rather pick a different key for practical reasons. You should always add a prefix that is unique to your mod in this case, to avoid name collisions with other mods:
$MYPREFIX_QUESTION1 Are you sure?\nAre you ABSOLUTELY sure you want to continue??????
If your mod is named
MyMod.esp, localize your page names by adding
$General General $Advanced Advanced $Help Help
$General Allgemein $Advanced Fortgeschritten $Help Hilfe
Then, in the config menu, name your pages accordingly:
Pages = "$General" Pages = "$Advanced" Pages = "$Help"
When using the KeyMap option type to map buttons to custom controls, conflicts may arise because these buttons are already in use.
OnOptionKeyMapChange reports these conflicts, so you can react accordingly. The
conflictControl string parameter contains the name of the conflicting control, or
"" of there was no conflict.
conflictName is the name of the mod that owns the control, or
"" if it's a part of the regular game.
Reacting to conflicts
In some cases, for example when defining a control that is only used in a certain context, you may choose to ignore any conflicts.
For regular game-play controls, it should be a good idea to display a confirmation dialog.
event OnOptionKeyMapChange(int option, int keyCode, string conflictControl, string conflictName) if (option == myKeymapOID) bool continue = true if (conflictControl != "") string msg if (conflictName != "") msg = "This key is already mapped to:\n\"" + conflictControl + "\"\n(" + conflictName + ")\n\nAre you sure you want to continue?" else msg = "This key is already mapped to:\n\"" + conflictControl + "\"\n\nAre you sure you want to continue?" endIf continue = ShowMessage(msg, true, "$Yes", "$No") endIf if (continue) myKey = keyCode SetKeymapOptionValue(_option, keyCode) endIf endIf endEvent
The mechanism for conflict detection described just now relies on mods reporting their used keys via
GetCustomControl. Otherwise, only conflicts with standard controls can be detected.
GetCustomControl is easy, just check the passed
keyCode against all keys you are using and - if matched - return a descriptive name of the control it's assigned to. Otherwise, return
string function GetCustomControl(int keyCode) if (keyCode == myKey) return "Turn 180 degrees" else return "" endIf endFunction
As soon as you released the first version of a config menu, you'll have to account for people upgrading to a newer version from an older save. To understand why it can be a problem, have a look at [this article](http://www.creationkit.com/Save_File_Notes_(Papyrus\)).
There are several ways you could handle this:
- Force users of your mod to make a 'clean save' after each new version.
- Delete the old config menu quest and create a new one.
- Incrementally upgrade your existing config menu quest at run-time.
The 'clean save' method is tempting, because it doesn't require any action from you as the mod author. But it comes with several major drawbacks:
- If users don't follow instructions (it happens), you'll end up with undefined behavior. Usually, this means that things break.
- You'll lose any progress associated with your mod.
- There have been reported issues where 'clean save' actually results in 'broken save'. Officially, removing mods at runtime is not supported, so there may be all kinds of issues.
One scenario where 'clean saves' may still be used is for alpha/beta testing for a smaller group of users who are supposed to know what they're doing, though even there it's advised to instruct them to keep a save around that is actually clean (i.e. has never been used with your mod before).
Config menu quest replacement
The second method would be deleting your old config menu quest and creating a new one. This is recommended if you're doing major changes to the script and an incremental update would not be feasible. The downside is that all previous settings are reset to their default values.
Based on [the article](http://www.creationkit.com/Save_File_Notes_(Papyrus\)) that was linked earlier in this section, when making changes to a script that has already been deployed in a release, we can define a few rules:
- It's save to change functions and event handlers.
- Don't remove or rename variables or properties; only add new things.
- Once you referenced a custom type (a custom script), removing this type will break your script, even if you removed all references to the former.
If there are fundamental problems with these rules when you want to update the script, consider using the previous approach of replacing the old quest by a new one.
More things to consider:
- If you change default values of variables that have already been initialized, they'll keep their old value. You can't, for example, add new pages by just changing the original property value.
- OnConfigInit (or OnInit) will not be executed again.
To work around these issues, we already provide an infrastructure for script versioning as part of the ConfigMenu API. First implement
GetVersion() to return the current revision of your script. Don't use a variable to hold the version but return a literal number. The default implementation returns 1:
; SCRIPT VERSION int function GetVersion() return 1 ; Default version endFunction
Each script keeps track of an internal version and detects when it's different from the return value of
GetVersion(). If that's the case, the
OnVersionUpdate event is triggered. The following example will illustrate things further.
Version 1 is just a regular script.
OnVersionUpdate can be omitted and left at their default value.
; INITIALIZATION event OnConfigInit() Pages = new string Pages = "Page 1" Pages = "Page 2" endEvent
Version 2 now adds two more pages.
; SCRIPT VERSION int function GetVersion() return 2 endFunction ; INITIALIZATION event OnConfigInit() Pages = new string Pages = "Page 1" Pages = "Page 2" endEvent event OnVersionUpdate(int a_version) ; a_version is the new version, CurrentVersion is the old version if (a_version >= 2 && CurrentVersion < 2) Debug.Trace(self + ": Updating script to version 2") Pages = new string Pages = "Page 1" Pages = "Page 2" Pages = "Page 3" Pages = "Page 4" endIf endEvent
If this script is run on a save for the first time, it'll execute
OnConfigInit, then instantly
OnVersionUpdate to version 2. If it's run on a save that already used the first version, only
OnVersionUpdate will be executed.
Version 3 adds a couple of variables that have to be initialized.
; SCRIPT VERSION int function GetVersion() return 3 endFunction ; PRIVATE VARIABLES ; -- Version 3 -- int myVar = 0 string myArray ; INITIALIZATION event OnConfigInit() Pages = new string Pages = "Page 1" Pages = "Page 2" endEvent event OnVersionUpdate(int a_version) ; a_version is the new version, CurrentVersion is the old version if (a_version >= 2 && CurrentVersion < 2) Debug.Trace(self + ": Updating script to version 2") Pages = new string Pages = "Page 1" Pages = "Page 2" Pages = "Page 3" Pages = "Page 4" endIf ; a_version is the new version, CurrentVersion is the old version if (a_version >= 3 && CurrentVersion < 3) Debug.Trace(self + ": Updating script to version 3") myVar = Utility.RandomInt(0,100) myArray = new string ; ... endIf endEvent
An alternative version 3 that just runs
OnConfigInit again when updating:
; SCRIPT VERSION int function GetVersion() return 3 endFunction ; PRIVATE VARIABLES ; -- Version 3 -- int myVar = 0 string myArray ; INITIALIZATION event OnConfigInit() Pages = new string Pages = "Page 1" Pages = "Page 2" Pages = "Page 3" Pages = "Page 4" myVar = Utility.RandomInt(0,100) myArray = new string endEvent event OnVersionUpdate(int a_version) if (a_version > 1) Debug.Trace(self + ": Updating script to version " + a_version) OnConfigInit() endIf endEvent
Note that this is just one way of handling updates. You can always use own preferred method.