"Setting a [ConVar] to the value it already holds does nothing"
▸ SYMPTOM
You drive an action through a [ConVar] (a console command that regenerates terrain, rebuilds a world, reapplies a setting) and on a fresh play session it does nothing. No effect, and no log line at all, not even the setter's own diagnostics.
Run it once, change something, run it again with a different value and it works. Run it twice with the same value and the second run is silent.
▸ CAUSE
The engine's ConVar layer suppresses same-value writes before the C# setter fires. If the convar already holds the value you're assigning, the property setter never runs, so even a setter with no equality guard of its own does nothing.
This compounds badly with statics: [ConVar]-backed statics survive Play stop/start (they are assembly-scoped, not scene-scoped). So a convar left at 42 by a prior play session makes a fresh session's my_convar 42 a no-op, because the value is "already set." The action keyed to the setter never happens, and nothing logs.
It also fires on C# code assignments to the property, not just console commands:
MyDial = 2; // if MyDial already == 2, the setter body never runsSo orchestration code that chains convar-property assignments is structurally fragile. A build loop that does Terrain = seed; Aerial = 2; per candidate can never ship its second candidate: candidate 1 leaves Aerial at 2, so candidate 2's Aerial = 2 is suppressed, nothing rebuilds, and every later candidate is falsely rejected as "not built."
▸ FIX
Don't drive build steps through the ConVar property from code. Call the underlying apply method directly and keep the ConVar as a thin console shim:
[ConVar( "my_dial" )]
public static int MyDial
{
get => _dial;
set { _dial = value; Apply( value ); } // console-only entry point
}
// orchestration code calls this directly, setting the backing field alongside:
public static void ApplyDirect( int value ) { _dial = value; Apply( value ); }If you must go through the convar, force a sentinel transition first so the write is never same-value:
MyDial = 0;
MyDial = 2; // now a real change, setter runsOr make the action idempotent on session start instead of setter-driven.
Diagnose by the silence: a convar command that logs on every apply but printed nothing means the setter never ran. The value was already there.
When you add a new build dial, ship its ApplyXDirect(...) twin in the same commit, so orchestration code never has a reason to touch the property.
The positive half: setters that DO fire
The rule above is easy to over-read into "convar setters never run." They do. A [ConVar] on a string property whose setter has side effects fires that setter from a console command on every write that actually changes the value (confirmed live in-editor on engine 26.07.22). The suppression is same-value-only. So a console-driven live A/B switch (a string profile name whose setter normalizes the input, stores it, and re-applies the new profile across every live instance in the scene) is a legitimate, working pattern. String-typed convars are subject to exactly the same same-value no-op as numeric ones; the console-shim rule stands for code that orchestrates, but it is not a claim that the setter is dead.
▸ WHY IT WORKS
The suppression happens in the engine's convar plumbing, upstream of your C# setter, so the only reliable way to guarantee your side-effect runs is to not depend on the setter firing. Calling the apply method directly (with the backing field set alongside) makes the effect unconditional; a sentinel transition makes the write a genuine value change so the setter fires. Reading the silence as a signal turns "mysteriously did nothing" into a one-line diagnosis: no apply log means the value was already set.
- Added the positive half: a string [ConVar] setter with side effects DOES fire on a real value change; the suppression is same-value-only. Re-verified on engine 26.07.22.
- Published