Bind Settings with Attributes
Tag static fields, properties and methods with a setting path, then call BindSettingsClass to assign the settings and subscribe to their events.
Instead of looking up each setting by hand, tag static fields, properties and methods
with an attribute that names the setting, then hand the class to BindSettingsClass. Gears assigns
the setting to each tagged field and property, and subscribes each tagged method to the event the
attribute names.
The attributes live in GearsAPI.Attributes.
MyHud and MyMod in the examples below stand for your own classes. Gears knows nothing about
them.
The attributes
| Attribute | Goes on | Binds to |
|---|---|---|
[Setting] | a static field or property | assigns the setting itself to the member |
[SettingPlayerAction] | a static PlayerAction field or property | reads the member and assigns it to a Binding setting's PlayerAction |
[SettingOnValueChanged] | a static method | SettingValue changed, because the player pressed Apply or your code assigned it. On a world path, invoke-only |
[SettingOnSelectedChanged] | a static method | SelectedValue changed, which means the player moved the control in the menu |
[SettingOnEnabled] | a static method | Enabled was turned on or off |
[SettingOnApplied] | a static method | the setting was applied |
Every attribute takes the setting path as its first argument. A null, empty or whitespace path
compiles, but the attribute constructor throws ArgumentException the first time Gears inspects the
class. See Exceptions that stop a bind.
[SettingOnValueChanged] and [SettingOnSelectedChanged] take one more, optional argument,
includeInSync.
Setting paths
| Settings | Format | Segments |
|---|---|---|
| Global | "TabName.CategoryName.SettingName" | exactly 3 |
| World | "CategoryName.SettingName" | exactly 2 |
The names are the name attributes from ModSettings.xml, or the names you passed to Create….
They are not the display keys.
Put global and world members on separate types. Each BindSettingsClass call resolves every
tagged member on the type you hand it, using that side's segment count. A world path on a type you
pass to the global call is reported as a bad path format, and the reverse is also true.
Bind a settings class
Bind in InitMod. ModSettings.xml is read before it runs, so every setting already exists,
and one InitMod binds both your global and your world class. Then call
SyncSettingsToClass from the loaded callbacks to pick the values up:
public void InitMod(IGearsMod mod)
{
mod.GlobalSettings.BindSettingsClass(typeof(MyGlobalListeners));
mod.WorldSettings.BindSettingsClass(typeof(MyWorldListeners));
}
public void OnGlobalSettingsLoaded(IModGlobalSettings modSettings)
{
modSettings.SyncSettingsToClass(typeof(MyGlobalListeners));
}
public void OnWorldSettingsLoaded(IModWorldSettings worldSettings)
{
worldSettings.SyncSettingsToClass(typeof(MyWorldListeners));
}Bind once, then sync whenever values arrive. Binding is one-time wiring, and InitMod runs exactly
once. The loaded callbacks are about values, and OnWorldSettingsLoaded runs again on every world
load and rejoin. Splitting the work this way keeps each callback to one job.
BindSettingsClass does the whole wiring job. It assigns the tagged fields and properties first,
then subscribes the tagged methods, so a handler can read any member the same call binds. It calls
no listener of its own. SyncSettingsToClass does that.
You can bind from OnGlobalSettingsLoaded or OnWorldSettingsLoaded instead. It works, and repeat
calls are a no-op. It costs you two things: the binding sits in a
callback that runs repeatedly, and you cannot bind a world class until a world loads.
Create any code-defined settings before you bind them. BindSettingsClass resolves every path as it
runs, and reports and skips a setting that does not exist yet.
Binding is opt-in. Nothing scans for these attributes until you call BindSettingsClass.
Binding the same type twice
Binding is never repeated. Gears remembers which types a settings tree has already bound; hand it the same one again and nothing is bound a second time — no field reassigned, no property setter run again, no listener subscribed twice. It is a no-op, not an error, and nothing is logged.
That makes BindSettingsClass safe to call from anywhere you like, as often as you like. Without it
a second bind would add a second handler to every event, which then runs twice on every change —
the kind of bug that shows up as doubled work long after the call that caused it.
Re-binding is not how you get new values into your listeners — binding never calls one. Use
SyncSettingsToClass for that, which matters most
on the world side, where OnWorldSettingsLoaded fires again on every load and rejoin with a new
world's values already applied.
"Already bound" is tracked per settings tree, so the global and world trees keep their own record. Binding one type does not stop a different one binding.
BindSettingsClass(null) throws ArgumentNullException. Gears logs most other problems and skips
that one member, and the rest still bind.
Exceptions that stop a bind
Two problems are not caught, and they stop the whole bind rather than one member:
- An attribute with a null, empty or whitespace path, whose constructor throws
ArgumentException. - A
[Setting]property whose setter throws.
The exception leaves BindSettingsClass, so the members after it are not bound, and the type is
not recorded as bound. Called from InitMod, it is logged as
Error thrown in InitMod() by <YourType> for <Your Mod>.
Bind a setting to a field or property
Tag a static field or a static property with [Setting("…")], and Gears assigns the setting the
path names:
public static class MyGlobalListeners
{
[Setting("General.Display.HudScale")]
private static ISliderGlobalSetting<float> hudScale;
[Setting("General.Display.Units")]
public static ISelectorGlobalSetting<string> Units { get; private set; }
public static float CurrentScale => hudScale.SettingValue;
}Either kind of member must be:
- Static. Gears reports and skips a tagged instance field or property.
- Declared as a type the setting actually is. Any interface the setting implements works:
IModSetting,IGlobalModSetting,IValueModSetting<T>,ISliderGlobalSetting<T>. Gears reports a mismatch, most often the wrongT, and leaves the member alone.
A field must also be assignable, so neither readonly nor const. Gears reports and skips
both.
A property must also have a setter. Both { get; set; } and { get; private set; } bind, and
Gears calls the setter, so a property with a body runs whatever logic it contains. A setter that
throws stops the bind. A get-only
property, written { get; } or as an expression body, has nothing to assign through, so Gears
reports and skips it.
Gears reads members only from the type you pass, so it does not find a tagged member inherited from a base type. Two members may name the same setting. Any field initializer runs before binding, so the bound setting wins.
Tagging an auto-property's backing field with [field: Setting("…")] also works and binds through
the field path. Tagging the property itself is simpler and gives a clearer error when something is
wrong.
Attach a PlayerAction to a Binding
A Binding row rebinds a PlayerAction that your mod
owns. Tag a static field or property that holds that action with [SettingPlayerAction("…")], and
BindSettingsClass assigns it to the Binding's PlayerAction. This example assumes a <Binding>
named Boost in the Vehicle category of a Controls tab:
using GearsAPI.Attributes;
using InControl;
public static class MyGlobalListeners
{
// PlayerInput is your own InControl.PlayerActionSet, and Boost is a PlayerAction on it.
public static readonly PlayerInput Input = new PlayerInput();
[SettingPlayerAction("Controls.Vehicle.Boost")]
private static PlayerAction Boost => Input.Boost;
}This works the other way round from [Setting]. Gears reads the member and never writes to
it, so the member must be:
- Static. Gears reports and skips an instance field or property.
- Declared as
PlayerAction, or a class derived from it. Gears reports and skips any other type. - Readable. A field may be
readonly. A property needs a getter, which may be private.
The path must name a Binding setting. Gears reports and skips a path that names any other kind of setting.
Create your PlayerActionSet before you bind. Gears reads the member once, while
BindSettingsClass runs, and never again. SyncSettingsToClass does not read it. A member that is
null at that point is reported and skipped, and the row reads - Missing PlayerAction - until you
assign PlayerAction yourself.
Bind a method to an event
A listener method must be:
- Static. Public and private both work, and Gears does not see instance methods.
- Declared on the type you pass. Gears does not scan members inherited from a base type.
- An exact signature match for the event, parameter types included:
| Attribute | Required signature | Works on |
|---|---|---|
[SettingOnValueChanged("...")] | static void M(IValueModSetting<T> setting, T newValue) | global value settings; also world value settings, invoke-only |
[SettingOnSelectedChanged("...")] | static void M(IValueModSetting<T> setting, T newValue) | global and world value settings |
[SettingOnApplied("...")] | static void M(IGlobalModSetting setting) | global settings, including Color and Binding |
[SettingOnEnabled("...")] | static void M(IModSetting setting, bool isEnabled) | every setting, global and world |
T is the setting's value type: int for an int slider, bool for a bool switch,
UnityEngine.Color for a color, your enum for an enum selector. The first parameter is
IValueModSetting<T>, not IGlobalValueSetting<T> or ISliderGlobalSetting<T>. A more derived
parameter type does not match.
OnEnabled is declared on IModSetting, so [SettingOnEnabled] is the one listener that works on
every setting type. It fires only when Enabled actually changes value, not on every assignment.
A method may carry several attributes, so one method can listen to several settings.
Sync settings to a class
A listener only ever fires on a change, so a mod that wires one up still has to read the setting's
current value somewhere to start off in the right state. SyncSettingsToClass does that: it calls
the listeners on an already-bound class with the value each setting holds right now, exactly as if
the event had fired. Nothing is bound or subscribed, so it is safe to call as often as you like.
It is on both IModGlobalSettings and IModWorldSettings, and belongs in the loaded callbacks —
the point at which values become real — with the binding done in
InitMod:
public void OnGlobalSettingsLoaded(IModGlobalSettings modSettings)
{
// Starts the mod off in step with the player's saved values.
modSettings.SyncSettingsToClass(typeof(MyGlobalListeners));
}SyncSettingsToClass is the only thing that calls a listener with a value no event delivered, so a
bind followed by a sync runs each opted-in listener exactly once.
Opting a listener in
Only listeners that asked for it are called. Pass includeInSync: true on the attribute:
[SettingOnValueChanged("General.Display.HudScale", true)]
private static void HudScaleChanged(IValueModSetting<float> setting, float newValue)
{
// Called by SyncSettingsToClass with the saved scale, and on every save after that.
MyHud.Scale = newValue;
}| Attribute | includeInSync: true passes |
|---|---|
[SettingOnValueChanged] | the setting's current SettingValue, the applied value |
[SettingOnSelectedChanged] | the setting's current SelectedValue, what the UI shows |
It defaults to false, so a listener written without it is skipped — it runs only when its event
fires. Only these two attributes take it: [SettingOnApplied] and [SettingOnEnabled] do not, since
neither carries a value to hand over.
Opting in changes nothing about the subscription. The call is on top of it, not instead of it, so the handler still runs on every later change.
Named arguments read better when the path is long:
[SettingOnSelectedChanged("General.Display.HudScale", includeInSync: true)]Keeping up with world values
This is also how a mod keeps up with world values. OnWorldSettingsLoaded fires on every world load
and rejoin, with the new values already applied, so calling SyncSettingsToClass there hands each
world's values to the listeners you bound once in InitMod. See
Hand each world's values to your listeners.
Loading a value in raises nothing
This call is not just a convenience — it is the only way a listener hears about a value Gears loaded. While Gears reads values from XML, the value events are held back:
| What moved the value | OnValueChanged / OnSelectedChanged |
|---|---|
| Gears restoring the player's saved global values at startup | silent |
| Gears loading a world's values, or resetting them to defaults for a client | silent |
| The host opening Mods World Settings, which resets each selected value to its default first | OnSelectedChanged raised |
| The player moving a control in the menu | raised |
Your own code assigning SettingValue or SelectedValue | raised |
Without that, a mod that binds early would get a burst of change events for values that had only
just arrived, and then a second round from SyncSettingsToClass. Loading is silent, and you
ask for the values when you are ready.
What the call guarantees
Every field, property and listener on the type is already bound by the time any handler runs, so a
handler can read any [Setting] member on the class, and anything it changes reaches the class's
other listeners.
The call ignores Enabled. A real OnValueChanged or OnSelectedChanged is suppressed while a
setting is disabled, but SyncSettingsToClass calls the method either way, so a mod always
starts in sync with what is saved. Check setting.Enabled in the handler if that matters to you.
A handler that throws is reported to the log and the remaining calls still run.
SyncSettingsToClass(null) throws ArgumentNullException. A type that was never bound is
logged and nothing happens, so a typo'd or forgotten BindSettingsClass shows up in the log rather
than silently doing nothing.
Gears warns when the sync is missing
A class you bind and never sync is the easy mistake to make, so Gears reports it. When
OnGlobalSettingsLoaded or OnWorldSettingsLoaded returns, Gears looks at the classes bound on that
settings tree and warns about each one that has a listener written with includeInSync: true and was
not handed to SyncSettingsToClass during that callback:
[Gears] [GearsMod] 'MyMod.MyWorldListeners' of 'My Mod' was bound with BindSettingsClass and has [SettingOnValueChanged] or [SettingOnSelectedChanged] listeners written with includeInSync: true, but SyncSettingsToClass(typeof(MyWorldListeners)) was not called in OnWorldSettingsLoaded(). Those listeners were not given the values just loaded. Call SyncSettingsToClass on it in OnWorldSettingsLoaded().The check runs every time either callback fires, not once. OnWorldSettingsLoaded fires again on
every world load and rejoin, and each of those needs its own sync, so a load that skips it is
reported even when an earlier load did sync.
A class whose listeners all left the opt-in off is never reported. Nothing on it asked for the loaded values, so the sync it did not get would have called nothing.
[SettingOnValueChanged] on a world path
A world setting has no OnValueChanged event — its applied value arrives with the world rather than
changing under the player. [SettingOnValueChanged] still works there, bound invoke-only: never
raised, and called only by SyncSettingsToClass.
That call is the only way such a listener runs, so write it with includeInSync: true. The opt-in
rule has no exception here, and Gears logs no warning. Without the flag, Gears binds the method and
never calls it.
// On a world path this is "hand me the value this world applied", not "tell me when it changes".
[SettingOnValueChanged("Zombies.SpawnMultiplier", includeInSync: true)]
private static void SpawnChanged(IValueModSetting<int> setting, int newValue)
{
MyMod.SpawnMultiplier = newValue;
}Example
This example uses the ModSettings.xml from Getting Started, plus a
<Color> setting named Accent in the HUD category of a Colors tab:
using GearsAPI.Attributes;
using GearsAPI.Settings;
using GearsAPI.Settings.Global;
using GearsAPI.Settings.World;
using UnityEngine;
public static class MyGlobalListeners
{
[Setting("General.Display.HudScale")]
private static ISliderGlobalSetting<float> hudScale;
[Setting("General.Display.Units")]
private static ISelectorGlobalSetting<string> units;
// includeInSync: SyncSettingsToClass hands this the saved scale, so the HUD does not
// wait for the next save to pick it up.
[SettingOnValueChanged("General.Display.HudScale", true)]
private static void HudScaleSaved(IValueModSetting<float> setting, float newValue)
{
MyHud.Scale = newValue;
}
[SettingOnSelectedChanged("General.Display.HudScale")]
private static void HudScalePreview(IValueModSetting<float> setting, float newValue)
{
MyHud.PreviewScale(newValue);
}
[SettingOnValueChanged("General.Display.Units")]
private static void UnitsSaved(IValueModSetting<string> setting, string newValue)
{
MyHud.Units = newValue;
}
[SettingOnValueChanged("Colors.HUD.Accent")]
private static void AccentSaved(IValueModSetting<Color> setting, Color newValue)
{
MyHud.Accent = newValue;
}
[SettingOnApplied("General.Display.HudScale")]
[SettingOnApplied("General.Display.Units")]
private static void AnythingApplied(IGlobalModSetting setting)
{
MyHud.Redraw();
}
[SettingOnEnabled("General.Display.HudScale")]
private static void HudScaleEnabledChanged(IModSetting setting, bool isEnabled)
{
MyHud.ScaleOverrideActive = isEnabled;
}
}
public static class MyWorldListeners
{
// [Setting] works the same on both sides, with the two-segment world path.
[Setting("Zombies.SpawnMultiplier")]
private static ISliderWorldSetting<int> spawnMultiplier;
// OnSelectedChanged is the one real event on a world setting. It fires while the host picks values.
[SettingOnSelectedChanged("Zombies.SpawnMultiplier")]
private static void SpawnPreview(IValueModSetting<int> setting, int newValue)
{
}
// Invoke-only on a world path: never raised, so it has to opt in to be called at all.
[SettingOnValueChanged("Zombies.SpawnMultiplier", includeInSync: true)]
private static void SpawnApplied(IValueModSetting<int> setting, int newValue)
{
MyMod.SpawnMultiplier = newValue;
}
// OnEnabled is on IModSetting, so it works here too.
[SettingOnEnabled("Zombies.SpawnMultiplier")]
private static void SpawnEnabledChanged(IModSetting setting, bool isEnabled)
{
}
}Log lines when a member does not bind
Every message below comes from [Gears]. Each one skips that member only, and the rest still bind.
The exception is the last line in the listener table, which reports a whole class and skips nothing.
Listeners
| Message | Cause |
|---|---|
[SettingOnValueChangedAttribute("…")] on 'Ns.Type.Method' must use the format "tabName.CategoryName.SettingName". Skipping registration of listener. | wrong number of path segments; a world path is "CategoryName.SettingName" |
[SettingOnValueChangedAttribute] on 'Ns.Type.Method' references unknown setting "…". Skipping registration of listener. | no setting at that path |
[SettingOnApplied("…")] on '…' targets a setting that does not support OnSettingApplied (only global settings expose it). Skipping registration of listener. | [SettingOnApplied] on a world path |
[SettingOnValueChangedAttribute("…")] on '…' targets a setting that holds no value and raises no 'OnValueChanged'. Skipping registration of listener. | either value attribute on a Binding, which holds no value at all. A world setting is not this case: it holds a value, so the listener binds invoke-only |
'Ns.Type' has not been bound, so its listeners cannot be invoked. Call BindSettingsClass on it first. | SyncSettingsToClass for a type no BindSettingsClass ever saw |
[SettingOnValueChangedAttribute] on '…' has an invalid signature, skipping registration of listener. Expected: static Void Method(IValueModSetting`1, Int32). | wrong parameter types, wrong parameter count, or a non-void return. An instance method is not reported: Gears never looks at it |
[SettingOnValueChangedAttribute("…")] on '…' threw when it was invoked with the setting's current value: … | your handler threw on a SyncSettingsToClass call; the remaining calls still run |
'Ns.Type' of '<Your Mod>' was bound with BindSettingsClass and has [SettingOnValueChanged] or [SettingOnSelectedChanged] listeners written with includeInSync: true, but SyncSettingsToClass(typeof(Type)) was not called in OnGlobalSettingsLoaded(). … | the class was bound but never synced in that callback. Carries the [GearsMod] tag, and names OnWorldSettingsLoaded() for a world class. See Gears warns when the sync is missing |
Field bindings
| Message | Cause |
|---|---|
[SettingAttribute("…")] on 'Ns.Type.field' must use the format "tabName.CategoryName.SettingName". Skipping binding of field. | wrong number of path segments; a world path is "CategoryName.SettingName" |
[SettingAttribute] on 'Ns.Type.field' references unknown setting "…". Skipping binding of field. | no setting at that path |
[Setting("…")] on '…' is declared const and can never be assigned. Skipping binding of field. | a const field |
[Setting("…")] on '…' is declared readonly and cannot be assigned. Remove readonly. Skipping binding of field. | a static readonly field, or the backing field behind a [field: Setting] on a get-only auto-property |
[Setting("…")] on '…' is not static. Only static fields can be bound. Skipping binding of field. | an instance field |
[Setting("…")] on '…' is declared as 'ISliderGlobalSetting<Single>' but the setting is a 'SliderSettingInt'. Skipping binding of field. | the field's declared type does not fit the setting, usually the wrong T |
Property bindings
| Message | Cause |
|---|---|
[SettingAttribute("…")] on 'Ns.Type.Property' must use the format "tabName.CategoryName.SettingName". Skipping binding of property. | wrong number of path segments; a world path is "CategoryName.SettingName" |
[SettingAttribute] on 'Ns.Type.Property' references unknown setting "…". Skipping binding of property. | no setting at that path |
[Setting("…")] on '…' has no setter and cannot be assigned. Give it one, even a private one. Skipping binding of property. | a { get; } or expression-bodied property |
[Setting("…")] on '…' is not static. Only static properties can be bound. Skipping binding of property. | an instance property |
[Setting("…")] on '…' takes index parameters and cannot be assigned. Skipping binding of property. | an indexed property, which C# cannot declare as static |
[Setting("…")] on '…' is declared as 'ISliderGlobalSetting<Single>' but the setting is a 'SliderSettingInt'. Skipping binding of property. | the property's declared type does not fit the setting, usually the wrong T |
PlayerAction assignments
| Message | Cause |
|---|---|
[SettingPlayerActionAttribute("…")] on 'Ns.Type.Member' must use the format "tabName.CategoryName.SettingName". Skipping assignment of PlayerAction. | wrong number of path segments |
[SettingPlayerActionAttribute] on 'Ns.Type.Member' references unknown setting "…". Skipping assignment of PlayerAction. | no setting at that path |
[SettingPlayerAction("…")] on '…' targets a 'SliderSettingInt', not a Binding setting. Skipping assignment of PlayerAction. | the path names a setting that is not a Binding |
[SettingPlayerAction("…")] on '…' is not static. Only static fields and properties can be read. Skipping assignment of PlayerAction. | an instance field or property |
[SettingPlayerAction("…")] on '…' is declared as 'Object' but must be a 'PlayerAction'. Skipping assignment of PlayerAction. | the member's declared type is not PlayerAction |
[SettingPlayerAction("…")] on '…' has no getter and cannot be read. Skipping assignment of PlayerAction. | a set-only property |
[SettingPlayerAction("…")] on '…' takes index parameters and cannot be read. Skipping assignment of PlayerAction. | an indexed property, which C# cannot declare as static |
[SettingPlayerAction("…")] on '…' threw when it was read: … Skipping assignment of PlayerAction. | the property's getter threw |
[SettingPlayerAction("…")] on '…' was null when BindSettingsClass ran. Create the PlayerActionSet before binding. Skipping assignment of PlayerAction. | the member held no action yet when you bound the class |
Use World Settings in C#
Read the per-world settings the host picks and every client receives, and learn how they differ from global settings.
Use Custom Value Types
The value types Gears handles without registration, how to use your own enums, how to register a parser and formatter for another type, and the limit on which types can back a setting.