LogicOnOffButton¶
The canonical "device button that goes orange when there's an error" widget. A MonoBehaviour attached to every LogicUnitBase prefab (Logic Reader, Logic Writer, Logic Memory, Logic Stopwatch, Logic Math, IC Housing, etc.). It is NOT attached to ElectricalInputOutput lineage prefabs (Transformer, Battery, AreaPowerControl, PowerTransmitter, PowerReceiver) -- so transformers do not flash on error in vanilla, and a mod that wants this behaviour on a transformer must either graft a custom component on (preferred: MaterialPropertyBlock emission lerp; see related Patterns page) or attach a LogicOnOffButton clone with its own shipped material assets.
Where it lives¶
Field declaration on LogicUnitBase (decompile L384230):
LogicUnitBase exposes the field as a serialized reference; the actual widget is positioned and materialised in the prefab. Subclasses inherit the field (Logic Reader, Logic Writer, Logic Memory, Logic Stopwatch, Logic Math, IC Housing, all the other "logic unit" prefabs).
Transformer : ElectricalInputOutput : Device : SmallGrid : Thing (L373755, L403300). LogicUnitBase : SmallDevice : Device : SmallGrid : Thing (L384220). The two lineages diverge at Device; the OnOffButton field is unique to the LogicUnitBase branch.
The four-state machine¶
The button has three logical states and four materials:
(Decompile L174840-174845.)
Four [SerializeField] private Material assets bound in the prefab (L174849-174858): powerOff, powerOn, error, errorEmissive. The first three are stable steady-state materials; error and errorEmissive alternate at 2 Hz during the Error state.
State transition trigger¶
RefreshState() (decompile L174885-174932) reads parentLogicUnit.Powered and parentLogicUnit.Error to pick the new state:
if (parentLogicUnit.Powered)
{
logicUnitButtonState = LogicUnitButtonState.OnPowered;
if (parentLogicUnit.Error == 1)
{
logicUnitButtonState = LogicUnitButtonState.Error;
}
}
else
{
logicUnitButtonState = LogicUnitButtonState.Off;
}
if (logicUnitButtonState == _currentState)
{
return;
}
_currentState = logicUnitButtonState;
switch (_currentState)
{
case LogicUnitButtonState.Off:
if (parentLogicUnit.ShouldPlayLogicSound)
parentLogicUnit.PlayPooledAudioSound(Defines.Sounds.LogicOffBeep, SoundOffset);
buttonRenderer.material = powerOff;
break;
case LogicUnitButtonState.OnPowered:
if (parentLogicUnit.ShouldPlayLogicSound)
parentLogicUnit.PlayPooledAudioSound(Defines.Sounds.LogicOnBeep, SoundOffset);
buttonRenderer.material = powerOn;
break;
case LogicUnitButtonState.Error:
if (_errorFlashTask.Status != UniTaskStatus.Pending)
_errorFlashTask = ErrorFlashing();
break;
}
RefreshState() is invoked from LogicUnitBase.OnInteractableUpdated (decompile L384563-384570) when the device's Powered or Error interactable state flips:
if (OnOffButton != null
&& (interactable.Action == InteractableType.Powered || interactable.Action == InteractableType.Error))
OnOffButton.RefreshState();
So the canonical signal source is the Interactable of type Powered or Error. Server-side mutation of Thing.Error via OnServer.Interact(InteractError, ...) fires the standard animator-state replication; the client receives it, OnInteractableUpdated runs on the client, RefreshState() runs, and the flash starts. Multiplayer is implicit.
The 250 ms flash loop¶
ErrorFlashing() (decompile L174942-174963) is an async UniTask driving the alternation:
private async UniTask ErrorFlashing()
{
while (parentLogicUnit != null && _currentState == LogicUnitButtonState.Error)
{
if (parentLogicUnit.ShouldPlayLogicSound)
parentLogicUnit.PlayPooledAudioSound(Defines.Sounds.LogicErrorBeep, SoundOffset);
buttonRenderer.material = errorEmissive;
await UniTask.Delay(250);
if (_currentState != LogicUnitButtonState.Error)
break;
buttonRenderer.material = error;
await UniTask.Delay(250);
if (_currentState != LogicUnitButtonState.Error)
break;
}
}
250 ms on, 250 ms off, 2 Hz cycle. The loop exits cleanly when _currentState leaves Error (another RefreshState() call switched it). It is a coroutine, not an animation curve and not an Animator; the in-prefab MeshRenderer.material field is reassigned each cycle.
Audio: Defines.Sounds.LogicErrorBeep plays once per 500 ms cycle when ShouldPlayLogicSound is true (gated to avoid spam in dense logic networks).
Bookkeeping fields (decompile L174869-174871):
The CancellationTokenSource is declared but never used in the visible body of ErrorFlashing(); the loop self-terminates on the _currentState check.
Implications for modders¶
A mod that wants LogicOnOffButton-style flashing on a device outside LogicUnitBase (e.g. a Transformer) cannot inherit the behaviour. Three viable approaches:
MaterialPropertyBlockemission lerp on a custom MonoBehaviour. Add a small component to the device's prefab GameObject (via runtimeAddComponentfrom a Harmony postfix onTransformer.OnRegistered), find the on / off button'sMeshRendererby name or hierarchy walk, and lerp the emission color of aMaterialPropertyBlockat 2 Hz. No prefab asset modification, no shipped materials. Robust against vanilla material updates. See related Patterns page.- Clone the LogicOnOffButton class. Ship four custom materials in the mod's AssetBundle; attach a
LogicOnOffButton-shaped component referencing them; driveRefreshState()from your own logic. Heavyweight (asset import); strictly preserves the vanilla look. - Reuse
Thing.Error == 1and hope the prefab's existing animator picks it up. Almost never works for non-LogicUnitBasedevices because no animator state is bound; the field flip is silent. Verify per-prefab via InspectorPlus before relying on this.
Verification history¶
- 2026-06-02: page created. Sourced from Agent 5's PowerGridPlus transformer-priority research turn (session scratch under
.work/, since removed). Decompile lines verified against.work/decomp/0.2.6228.27061/Assembly-CSharp.decompiled.cs.
Open questions¶
- Whether the four
Materialassets are reused across everyLogicUnitBaseprefab or each prefab has its own bound materials. Affects whether a clone-the-LogicOnOffButton approach can share asset references. - Whether
LogicUnitBasesubclasses outside the "logic family" (e.g. cartridge readers, sensor lenses) reuseOnOffButtonor omit it. Verify via InspectorPlus snapshot on a representative sample.