Skip to content

Adding Mod Settings

UnlimitedHugs edited this page Jul 16, 2021 · 9 revisions

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 SettingsHandles 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.

Basic usage

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 SettingHandles 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);
	}
}
  1. The first parameter is a unique name for that setting, and is the identifier it is stored under in the settings XML file.
  2. 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.
  3. 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.
  4. 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 the Value 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 SettingHandles 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

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.

The SettingsChanged event

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.

SettingHandle reference

These are additional properties in SettingHandle that allow for specific configuration after the handle has been created.

Name

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.

Validator

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));

VisibilityPredicate

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.

CustomDrawer

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.

CustomDrawerFullWidth

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.

CustomDrawerHeight

When using CustomDrawer or CustomDrawerFullWidth, this property can be used to expand the height of the setting entry. The default height is 32px.

NeverVisible

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.

Unsaved

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.

CanBeReset

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.

SpinnerIncrement

Useful for int handles. Specifies by how much the + and - buttons should change the setting value.

ValueChanged

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;

DisplayOrder

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.

HasUnsavedChanges

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.

ContextMenuEntries

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)
};

Settings for non-library mods

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.

ModSettingsPack methods

These are specialized methods provided by ModSettingsPack- the object returned by ModBase.Settings

GetHandle

ModSettingsPack.GetHandle can also be called with only the handle name- this will return an existing SettingHandle or null;

ValueExists

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.

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.

ContextMenuEntries

See SettingHandle.ContextMenuEntries above. Same principle, but for an entire category of settings.

Custom setting types

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.

Serialization utilities

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);
	}
}

Optional settings (advanced)

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;
	}
}