-
Notifications
You must be signed in to change notification settings - Fork 59
Adding Mod Settings
One of the main features of HugsLib is the ability for mods to add persistent settings that can be changed by the player. The settings dialog is found in Options > Mod Settings.
Settings can come in a variety of types- the library has built-in controls for bool
, int
, string
and Enum
. Every other type will be treated as string
, unless CustomDrawer
is used (see below);
The easiest approach to adding settings is defining a class that extends HugsLib.ModBase
. All extending classes have the property Settings
that contains the SettingsHandle
s for that mod.
Note, that setting values are stored as strings by their Name
identifier, and have no known type until they are claimed by the mod that defined them.
The settings XML file is stored in the user profile folder. The default location on a Windows system on A16 is %USERPROFILE%/AppData/LocalLow/Ludeon Studios/RimWorld by Ludeon Studios/HugsLib/ModSettings.xml
.
Settings values matching their default are not written to the file.
Retrieving and creating a setting value is done using the same method. This is best done during the ModBase.DefsLoaded
callback, because this will give the setting handle the properly translated title and description after the player changes the game language. Titles and descriptions are used in the Mod Settings dialog to present the setting to the user.
Each setting only needs to be requested once, because SettingHandle
s are automatically updated with new values when modified by the user.
This example creates a boolean toggle setting:
public class SettingTest : ModBase {
public override string ModIdentifier {
get { return "SettingTest"; }
}
private SettingHandle<bool> toggle;
public override void DefsLoaded() {
toggle = Settings.GetHandle<bool>(
"myToggle",
"toggleSetting_title".Translate(),
"toggleSetting_desc".Translate(),
false);
}
}
- The first parameter is a unique name for that setting, and is the identifier it is stored under in the settings XML file.
- The second parameter is the title that will display next to the control in the settings dialog. It is best to keep the title short so that the player has an easier time skimming through the menu. What the setting does should be explained in the description.
- The third parameter is the setting description, displayed as a tooltip when hovering over the setting title or control in the settings dialog. The description can be
null
, in which case the tooltip will not be displayed. - The fourth parameter is the default value, which is the starting value of the setting, the value the setting will be reset to, and the fallback value if the setting fails to load for any reason.
Since default values of settings are not stored, changing the default value of a setting will change theValue
of the setting if the player has not modified it.
Using string.Translate
to populate the title and description is optional, but a good practice.
The GetHandle
method returns a SettingHandle
object that can be used to access the value of the setting and apply additional modifiers to it.
Note, that SettingHandle
s can be implicitly converted to the type of value they store, so the following ways of reading a setting value are equivalent:
if(toggle.Value) // do stuff
if(toggle) // do stuff
Settings of type int
and string
are created and accessed in the same manner, and have their own input controls. float
settings will use the default text field control.
Enum
settings can be quite useful to create settings that can be switched between multiple states. These settings require a string identifier prefix that will be used to display the setting options in a readable format.
The following example creates an enum setting with 3 options:
private enum HandleEnum { DefaultValue, ValueOne, ValueTwo }
public override void DefsLoaded() {
var enumHandle = Settings.GetHandle("enumThing",
"enumSetting_title".Translate(),
"enumSetting_desc".Translate(),
HandleEnum.DefaultValue,
null, "enumSetting_");
}
This creates a setting that requires the following strings to be provided in the language XML file:
enumSetting_title
, enumSetting_desc
, enumSetting_DefaultValue
, enumSetting_ValueOne
and enumSetting_ValueTwo
.
The fifth parameter (null
in the example) is the optional validator method that is not required for Enum
handles.
When the player closes the Mod Settings dialog after changing any setting, the ModBase.SettingsChanged
method is called for all mods.
Depending on what your settings do, it may not even be necessary to override it, however. Requesting the handles and reading their Value
property wherever it is needed may be enough, since the handle values are automatically updated.
These are additional properties in SettingHandle
that allow for specific configuration after the handle has been created.
This is the unique Id of the setting specified as the first argument to ModSettingsPack.GetHandle
. The name must only be unique to the mod that created the setting, and multiple mods can have settings with matching identifiers.
The validator method can be passed to ModSettingsPack.GetHandle
as the fifth parameter when creating a handle. It is used to ensure that values assigned to the setting by the player and those loaded from the XML file are valid. The method receives a string
argument and must return a bool
to indicate that the provided value is valid.
The first validation happens when the handle is requested. Retuning false
in the validator during that phase will discard the loaded value and the default value will be assigned to the handle. The validator is also called when the player changes settings in the menu- rejecting the passed value will simply prevent it from being assigned to the handle.
The library provides some convenience validators for commons setting types. This example creates an int
type handle that will only accept values from 0 to 30 (inclusive):
Settings.GetHandle("intSpinner", "title", "desc", 0, Validators.IntRangeValidator(0, 30));
An optional method that should return true
when the setting should be displayed in the menu. Useful for settings that should appear only in Dev mode or settings dependent on the values of other settings.
For some settings, the few basic editing controls will not be enough. Perhaps you will want a slider for your int
setting, a dropdown list with changing options, or, perhaps, you would prefer a button that opens a custom window for editing the value. Assigning a delegate to this property allows setting handle owners to draw their own control to be displayed in the Mod Settings menu. The delegate receives a Rect
argument- the screen area to do the drawing in, and must return a bool
to indicate if the setting has changed during the current update.
Make sure to assign SettingHandle.Value
when the setting changes so that it may be properly saved.
This is CustomDrawer
, but with even more power. When a delegate is assigned to to this property, it will take over all drawing of the handle in the Mod Settings menu. This will replace the title label, the editing control and the hover menu.
The replaced controls are easy to recreate: the editing control occupies the exact right half of the rectangle (try Rect.RightHalf
), the rest is the label, with Text.Anchor = TextAnchor.MiddleLeft
. The hover menu can be drawn with ModSettingsWidgets.DrawHandleHoverMenu
.
When using CustomDrawer
or CustomDrawerFullWidth
, this property can be used to expand the height of the setting entry. The default height is 32px.
When set to true
, this handle will never appear in the settings menu. This is useful for storing custom serialized data structures that should be independent of save files.
Note, that the setting can still be reset when the player resets all settings for your mod while having the "Include hidden settings" toggle checked. It's a good idea to provide a title for this reason- it appear in the tooltip of the toggle.
When true
, this handle will not be saved to the XML file. Useful in conjunction with CustomDrawer
for placing buttons in the settings menu. Note, that this will also remove the reset option from the context menu of the handle.
Defaults to true
. When enabled, the handle will have a reset option in its context menu which will set it to its default value.
It's a good idea to set this to false
only if you are using the setting to store important data- like player progress or achievements.
Useful for int
handles. Specifies by how much the + and - buttons should change the setting value.
An event that is dispatched each time the Value
of the handle changes. This goes for manual assignment, changing the setting in the Mod Settings dialog, or resetting it to default. The event is raised immediately after the value change, and does not wait for the settings dialog to be closed.
ValueChanged
can be used in addition to overriding the SettingsChanged
method.
The callback receives the changed handle as its only parameter. To get the new value during the callback, you can either capture the handle, or cast the argument received by the callback to the specific type: ((SettingHandle<int>)handle).Value
.
var callbackSetting = Settings.GetHandle<int>("intSetting", "Title", "Description");
callbackSetting.ValueChanged += handle => {
Logger.Message("Int setting received new value: " + callbackSetting.Value);
};
callbackSetting.Value = 3;
This property allows handle display order to be different than their creation order. This is useful if part of your handles are created at a different time. Defaults to zero, and sorted in ascending order. It's still recommended to create all handles at game startup, so that they may be visible to the player if he opens the settings before loading a game.
This property is important when changing setting values from code. For regular settings, it will be set to true
automatically whenever the handle Value
changes. When changing SettingHandleConvertible
handles, however, it should be set to true
manually.
The property value affects saving: if none of the settings have HasUnsavedChanges
set to true, nothing will be saved. Also, your ModBase.SettingsChanged
callback will only be called if at least one of your settings has HasUnsavedChanges
set to true whenever settings are saved to disk.
These are custom actions that you can assign to your handle to be shown whenever its context menu is opened (the hovering hamburger menu button is clicked).
A good use for these are preset values- this both gives the player a limited set of options and lets them set the value freely if they choose to:
var dangerLevel = Settings.GetHandle("dangerLevel", "Danger level", "Description here...",
0, Validators.IntRangeValidator(0, 100));
dangerLevel.ContextMenuEntries = new[] {
new ContextMenuEntry("Preset: Low", () => dangerLevel.Value = 10),
new ContextMenuEntry("Preset: Medium", () => dangerLevel.Value = 50),
new ContextMenuEntry("Preset: High", () => dangerLevel.Value = 80)
};
Mods that don't create a class extending ModBase
can still make use of custom settings. This is done by requesting a ModSettingsPack
from the settings manager:
HugsLibController.SettingsManager.GetModSettings("modIdentifier", modLabel);
modIdentifier
being a unique identifier for the mod. The identifier will be used in the XML file, so avoid spaces and special characters. The type returned by the manager is the same one as a ModBase
extending class would have access to through the Settings
property.
modLabel
(optional) will be used in the Mod Settings dialog when the ModSettingsPack
is displayed. Not used when requesting an existing pack.
ModSettingsManager.HasSettingsForMod
can be used to check if a ModSettingsPack
is already available.
These are specialized methods provided by ModSettingsPack
- the object returned by ModBase.Settings
ModSettingsPack.GetHandle
can also be called with only the handle name- this will return an existing SettingHandle
or null
;
Returns true
if a setting value is available. This includes already created handles, and existing values that have been loaded, but have not been claimed by a SettingHandle
yet.
Useful in conjunction with PeekValue
.
Returns the string value of a setting. If the value has already been claimed by a handle, returns the value of the handle, otherwise returns the loaded value.
This is useful if you need to access the value of a setting before handles are created, or if you are looking for a setting created by another mod.
See SettingHandle.ContextMenuEntries
above. Same principle, but for an entire category of settings.
It is possible to create settings of complex types like lists and other data structures. The key is providing methods that will convert the data structure from and to its string representation. This is done by extending HugsLib.Settings.SettingHandleConvertible
and using the new type as the type of a settings handle.
You can override the ShouldBeSaved
and return false
to prevent your object to be serialized and written to file- this is useful when the value is no longer null
, but the object is still in its default state.
This example stores a serialized List<int>
in a setting:
public override void DefsLoaded() {
var custom = Settings.GetHandle<CustomHandleType>("customType", "Label", null);
if(custom .Value == null) custom.Value = new CustomHandleType();
}
private class CustomHandleType : SettingHandleConvertible {
public List<int> nums = new List<int>();
public override bool ShouldBeSaved {
get { return nums.Count > 0; }
}
public override void FromString(string settingValue) {
nums = settingValue.Split('|').Select(int.Parse).ToList();
Log.Message(nums.Join(","));
}
public override string ToString() {
return nums != null ? nums.Join("|") : "";
}
}
If an exception is raised in either method, an error is displayed in the console and the handle value is reset to its default.
For the custom data type to be useful, the handle should use CustomDrawer
to draw a special control.
Alternatively, NeverVisible
could be used to create a hidden setting to store some kind of user data. Since SettingHandle
can't detect changes in your custom value, it's a good idea to either set HasUnsavedChanges
to true
, or call ForceSaveChanges
on the handle. In the first case the setting will be saved to file on game exit, the second will save the settings file immediately (use sparingly, since disk activity is involved).
Important: It is recommended to use null
as the default value for handles with custom data types to prevent both Value
and DefaultValue
from referencing the same object.
Be sure to check the value for null when accessing it- if CanBeReset
is true
, the player can reset your setting at any time.
While you can serialize your values manually, there are two utility methods that can be used to automatically serialize and de-serialize a custom setting value. These are especially useful if you are working with multiple fields of different types.
The only thing required are some additional annotations on your type and serialized fields:
[Serializable]
public class CustomHandleType : SettingHandleConvertible {
[XmlElement] public List<int> nums = new List<int>();
public override void FromString(string settingValue) {
SettingHandleConvertibleUtility.DeserializeValuesFromString(settingValue, this);
Log.Message(nums.Join(","));
}
public override string ToString() {
return SettingHandleConvertibleUtility.SerializeValuesToString(this);
}
}
Sometimes it is preferable to avoid adding HugsLib as a dependency, but it would still he useful to have configurable settings when the library is loaded. There is a sneaky way to accomplish that: referencing the library at compile time, but catching the TypeLoadException
that gets thrown at runtime if the library is not loaded.
Keep in mind, that all references to HugsLib types must be made inside a delegate wrapped in a try
block with a TypeLoadException
handler for this to work.
This example creates a toggle setting with a default value of true
.
Code and idea contributed by Zenthar.
[StaticConstructorOnStartup]
public class TestMod {
private static Func<bool> getSettingValue;
static TestMod() {
// load order does not matter- HugsLib initializes before StaticConstructorOnStartup
InitializeSetting();
Log.Message("Setting value: " + getSettingValue());
}
private static void InitializeSetting() {
const bool defaultValue = true;
// Need a wrapper method/lambda to be able to catch the TypeLoadException when HugsLib isn't present
try {
((Action) (() => {
var settings = HugsLibController.Instance.Settings.GetModSettings("TestModIdentifier");
// add a mod name to display in the Mods Settings menu
settings.EntryName = "Test Mod";
// handle can't be saved as a SettingHandle<> type; otherwise the compiler generated closure class will throw a typeloadexception
object handle = settings.GetHandle("testSetting", "Setting label", "Setting description", defaultValue);
getSettingValue = () => (SettingHandle<bool>) handle;
}))();
return;
} catch (TypeLoadException) {
}
getSettingValue = () => defaultValue;
}
}