Use Global Settings in C#
Navigate tabs and categories, create settings from code, read the three values each setting holds, and subscribe to the events that fire when a value changes.
A global setting is a mod setting that belongs to the player and applies in every world. Use this page to read one from C#, create one from code, and react when the player changes it.
Reach global settings through
IModGlobalSettings, which Gears hands to
OnGlobalSettingsLoaded and which is also available as IGearsMod.GlobalSettings. The types live
in GearsAPI.Settings, GearsAPI.Settings.Global and GearsAPI.Settings.Base.
Find a setting in the tree
Global settings form three levels:
- IGlobalModSettingsTab
GetTabCreateTabGetOrCreateTab- IGlobalModSettingsCategory
GetCategoryCreateCategoryGetOrCreateCategory- IGlobalModSetting
GetSettingCreateSettingGetOrCreateSetting
Every level offers the same three lookups. Get… returns null when the name is unknown, Create…
throws when the name is already taken, and GetOrCreate… does whichever is needed.
A tab also has AddCategory and RemoveCategory, and a category has AddSetting and
RemoveSetting. Add… moves a category or setting that Gears created, such as one you removed
from somewhere else, and returns false for any object Gears did not create. There is no AddTab
or RemoveTab. Every level can list its children. For the full member lists, see
IModGlobalSettings,
IGlobalModSettingsTab and
IGlobalModSettingsCategory.
CreateTab throws ArgumentException when the name already exists. Use GetOrCreateTab when
your XML may declare the same tab. The same applies to categories and settings.
A category is itself an IGlobalModSetting, which is how it gets a caption, description and preview
image. Its RestartScope always reads None, and GetAllSettings() on a category returns the
category first, followed by its settings.
Create a setting from code
CreateSetting<T> takes one of the closed setting interfaces:
| Interface | Value types that work |
|---|---|
ISelectorGlobalSetting<T> | int, float, string, any enum |
ISliderGlobalSetting<T> | int, float |
ISwitchGlobalSetting<T> | bool, string, any enum |
IColorSelectorGlobalSetting | fixed, UnityEngine.Color |
IControlBindingSetting | fixed, no value |
Create settings in InitMod, which runs once ModSettings.xml has been read:
public void InitMod(IGearsMod mod)
{
IGlobalModSettingsTab tab = mod.GlobalSettings.GetOrCreateTab("General", "myModTabGeneral");
IGlobalModSettingsCategory display = tab.GetOrCreateCategory("Display", "myModCatDisplay");
// "Fov" is the name you look the setting up by; "myModFov" is a key in your Localization.csv.
ISliderGlobalSetting<int> fov = display.GetOrCreateSetting<ISliderGlobalSetting<int>>("Fov", "myModFov");
fov.SetAllowedValues(increment: 5, min: 45, max: 120);
fov.DefaultValue = 65;
fov.SettingValue = 65;
fov.SelectedValue = 65;
fov.UiFormatter = "0";
fov.DescriptionKey = "myModFovDesc";
fov.RestartScope = SettingRestartScope.None;
}Always set DefaultValue, SettingValue and SelectedValue on a setting you create. A fresh
setting holds default(T) in all three, which is 0, false, null or black.
GetOrCreate… is the right shape here precisely because the XML has already been read. If
ModSettings.xml declared the tab, category or setting, you get that object back and whatever you
assign onto it overrides what the XML said; if it did not, you get a fresh one. The player's
saved value is restored on top of either, after InitMod returns.
The display key you pass to GetOrCreate… applies only when Gears creates the object. For an
object the XML already declared, the XML's display key stays, and DisplayKey is read-only.
Avoid declaring the same setting in both ModSettings.xml and code. One silently overrides the
other, and it is easy to lose track of which. Pick one place per setting.
CreateSetting<T> throws in two cases:
ArgumentExceptionwhenTis not one of the interfaces above, such asIGlobalModSettingitself.InvalidOperationExceptionwhen the value type has no implementation for that family, such asISliderGlobalSetting<string>orISelectorGlobalSetting<double>, or has no registered parser, such asISelectorGlobalSetting<TimeSpan>. See Use Custom Value Types.
GetOrCreateSetting<T> throws ArgumentException on top of those, when a setting of that name
already exists but is not a T:
Setting 'Fov' in category 'Display' is a ISliderGlobalSetting<Single>, not the requested
ISliderGlobalSetting<Int32>. A setting declared in both ModSettings.xml and code must use the same
type in both.That happens when ModSettings.xml and your code disagree about a setting's type — an int slider
in one and a float slider in the other, say. Since the XML is read before InitMod, the setting
already exists by the time your code asks for it, so the two declarations have to agree.
Members every setting has
IModSetting is what every setting has, categories
included:
NameandDisplayKeyfor identity.CaptionKeyandDescriptionKeyfor the description pane.Enabled, which grays the row out when false, and theOnEnabledevent that fires when it flips. See Disabling a setting for what else it changes.- The
IsChangedandIsDefaultflags. Get…Text()helpers that do the localization lookups for you.AddPreview(string path)for the setting's own image, andAddPreview(T value, string path)onIValueModSetting<T>for an image shown while one value is selected. These are the code equivalent of the<Preview>element. The last setting-wide image wins, and the first image registered for a given value wins.RefreshUI(), which you call after changing a value from code. See Change a setting from code.
Three more methods drive the settings window, and they define the three-value model below:
| Method | Effect |
|---|---|
ResetToDefault() | Sets the selected value to the default value. The applied value is untouched. |
DiscardCurrentChange() | Sets the selected value to the applied value. |
ApplyCurrentChange() | Sets the applied value to the selected value, raising OnValueChanged if the value changed. On a global setting it also raises OnSettingApplied, even when the value did not change. |
ResetToDefault() moves only the selected value. To reset the value your mod acts on, call
ApplyCurrentChange() after it.
IGlobalModSetting adds what only a player-level
setting has: a RestartScope, the Category and
Tab it belongs to, and the OnSettingApplied event that ApplyCurrentChange() raises.
The player sees only categories and settings that are in the settings tree. A category that was
never added to a tab, or a setting that was never added to a category, has a null Tab and does
not appear in the settings window. The same applies to one you removed with Remove….
Change a setting from code
Assigning SelectedValue or SettingValue changes the value and raises the matching event, but the
settings window neither redraws the row nor learns that the setting moved. Call RefreshUI() on the
setting afterwards and it does both: the row redraws, Apply commits the change along with whatever the
player edited, and closing the window or switching to another mod discards it.
hudScale.SelectedValue = 1.25f;
hudScale.RefreshUI(); // without this the row still reads the old valueIf your code changes SettingValue, set SelectedValue to the same value before you call
RefreshUI(). Otherwise the row counts as changed, and Apply puts SettingValue back to the
old selected value:
hudScale.SettingValue = 1.25f;
hudScale.SelectedValue = 1.25f;
hudScale.RefreshUI();Call it after changing the allowed values too, since the row caches the list it draws. Changing
Enabled is the exception: that refreshes the row on its own.
Disabling a setting
Set Enabled to false to gray a row out, which is how you make one setting depend on another. On a
value setting it also does two things that are easy to miss:
- Assigning
SelectedValuestill changes the value, but raises noOnSelectedChanged. ApplyCurrentChange()does nothing at all. The applied value does not move, and neitherOnValueChangednorOnSettingAppliedfires.
So a disabled setting keeps the applied value it had, and your listeners go quiet until you enable it again. World settings have the same gate. See Make One Setting Depend on Another for what that means in practice.
The three values a setting holds
Every value setting holds three values of its type T:
| Property | Meaning | Changed by |
|---|---|---|
DefaultValue | what a reset goes back to | defaultValue in XML, or your code |
SettingValue | the applied value: saved to disk, returned by modsetting(), and the one your mod should act on | the player pressing Apply, the saved-settings restore, or your code |
SelectedValue | what the player currently has picked in the menu, which they may not have applied yet | the settings window, ResetToDefault, DiscardCurrentChange, or your code |
All three live on IValueModSetting<T>, which also
carries the per-value preview images and the OnSelectedChanged event.
IGlobalValueSetting<T> adds OnValueChanged on
top of that.
Read SettingValue when you want the value the player has committed to, and subscribe to
OnValueChanged to react when it changes. OnSelectedChanged fires on every click in the menu,
before the player has saved, so use it for live previews only.
Every setter is a no-op when the new value equals the old one, so these events fire only on a real change.
Read and write a setting as a string
Code that handles settings without knowing their value type goes through
IValueModSettingBase. Its Set…FromString
methods parse with the type's registered parser; when the text does not parse, Gears logs it and
keeps the old value.
On the way out, GetUiFormattedSettingValue() returns the text the player sees, after the
formatter, the prefix and localization. GetSerializedFormattedSettingValue() returns what Gears
writes to disk and what modsetting() returns.
Selector
ISelectorGlobalSetting<T> sets the allowed
values, either from an explicit T[] or from an increment, minimum and maximum.
ISelectorSettingBase underneath it carries the
presentation: UiFormatter, SerializationFormatter, LocalizationPrefix, Wrap and
SelectByIndex.
Gears builds the display strings when they are first asked for, so a formatter or prefix you assign after the values still applies.
SetAllowedValues(increment, min, max) works only on an int or float selector. On a string or
enum selector it logs that it needs a numeric type and does nothing. It also requires
increment > 0 and min <= max. Otherwise it logs the problem and leaves the values alone. An enum
selector starts with every declared member.
Slider
ISliderGlobalSetting<T> exposes the range as the
read-only Increment, Min and Max, which you set together through SetAllowedValues. The
formatting comes from ISliderSettingBase.
Only int and float sliders exist. A slider's SetAllowedValues does not check its arguments, so
pass an increment greater than 0 and a minimum no larger than the maximum.
Switch
ISwitchGlobalSetting<T> holds the two sides as
LeftValue and RightValue, which you set together through SetSwitchValues.
ISwitchSettingBase underneath it has the
LocalizationPrefix, the GetLeftText() and GetRightText() lookups, and SelectButton and
GetSelectedButton for working in terms of sides rather than values.
A bool switch is false on the left and true on the right. An enum switch takes the first two
declared members. A string switch has no sides until you call SetSwitchValues. Switches ignore
the formatter string.
SelectedButton is nested inside
ISwitchSettingBase, so write ISwitchSettingBase.SelectedButton.Left or add
using static GearsAPI.Settings.Base.ISwitchSettingBase; to the file.
Color
IColorSelectorGlobalSetting adds no
members of its own. Everything comes from IGlobalValueSetting<Color>, where Color is
UnityEngine.Color.
Gears serializes the value as R,G,B in the range 0–255, and shows only the setting-wide preview
image. Per-value previews are ignored.
Key binding
IControlBindingSetting is not a value setting.
It has no SettingValue, and it adds only a PlayerAction property and a ClearBinding() method of
its own.
Declaring a <Binding> in XML or creating one in code gives the player a row to rebind. The row
works with the action's keyboard and mouse binding only. Controller bindings are not shown or
changed by it. Attach the game PlayerAction it controls yourself:
// myPlayerActionSet is your own InControl.PlayerActionSet, and Boost is a PlayerAction on it.
IControlBindingSetting boost = category.GetSetting<IControlBindingSetting>("Boost");
if (boost != null)
{
boost.PlayerAction = myPlayerActionSet.Boost;
}You can also tag a static PlayerAction field or property with [SettingPlayerAction] and let
BindSettingsClass assign it. See
Attach a PlayerAction to a Binding.
Assigning PlayerAction records the action's current binding as the baseline the row compares
against, so assign it once, after your action set exists. Until you do, the row reads
- Missing PlayerAction -.
The methods IModSetting declares act on that PlayerAction rather than on a stored value:
| Member | Effect on a Binding |
|---|---|
ResetToDefault() | Calls PlayerAction.ResetBindings(), putting the action back to the bindings it was declared with, controller bindings included. |
DiscardCurrentChange() | Restores the keyboard and mouse binding recorded when you assigned PlayerAction, or when the row was last applied. |
ApplyCurrentChange() | Raises OnSettingApplied and records the current binding as the new baseline. |
ClearBinding() | Removes the keyboard and mouse binding, which is what the row's clear button does. |
IsChanged | True while the action's binding differs from the baseline. |
IsDefault | Always false. |
All of them do nothing to the binding while PlayerAction is null. ApplyCurrentChange() still
raises OnSettingApplied then.
Gears does not store bindings in its saved settings file. The binding lives with the PlayerAction
set it belongs to, and that set is responsible for saving it.
Restart scope
Set RestartScope to None, Reload or
Restart on a global setting to tell the player that a change needs a world reload or a game
restart. Gears only shows the warning. Applying the value is still your code's job.
Save the player's values
Gears writes the saved global settings file when the player presses Apply in the Mods window, and again at startup.
Call IModGlobalSettings.SaveSettings(), or IGearsMod.SaveSettings(), to write it at any other
time, such as after your code has changed a SettingValue. The file is
<user data folder>\Gears\ModSettings.xml, shared by every mod, and you never need to edit it by
hand.