ColorSwatch¶
Vanilla game class at Assets.Scripts.Objects.ColorSwatch. Serializable container for one entry in GameManager.CustomColors. Holds two Material references (Normal and Emissive) plus light and localization metadata. Thing.CustomColor holds a reference to one of these; ThingSaveData.CustomColorIndex is the on-disk identifier of which swatch is active on a given Thing.
Declaration and fields¶
ColorSwatch is a C# class (not a struct), serializable for Unity inspector editing, with no explicit base. Fields in declaration order:
| Member | Type | Notes |
|---|---|---|
Name |
public string |
Inspector display name (ReadOnly). |
_index |
private int = -1 |
Backing cache for Index. |
Bit |
public int |
Access-bit identifier. Consumed by the color-scanner / screen-color UI path: GetAccess => CustomColor.Bit (decompile line 338789), screenColorItem.Bit = colorSwatch.Bit (338892), ScannedThing.HasAccess(gasItem.Bit) (338931), _selectedColorBit = colorSwatch.Bit (338989). Documented as unused at 0.2.6228.27061; see Verification History. |
StringKey |
public int |
Localization key (ReadOnly). |
PaintOnly |
public bool |
Added since 0.2.6228.27061. Marks a swatch as spray-paint-only: hidden from logic color dropdowns and rejected by logic / IC10 Color writes, and rendered with a metallic shader response. See "PaintOnly flag" below. |
Normal |
public Material |
Standard (non-emissive) material. Always set on live swatches. |
Emissive |
public Material |
Emissive material. MAY BE NULL: vanilla code contains if (CustomColor.Emissive == null) null-checks, so not every swatch ships an emissive variant. Colors without Emissive cannot be rendered in emissive mode via the material-swap path. |
Cutable |
public Material |
Present on the inspector; code path that consumes it has not been traced. Re-checked at 0.2.6403.27689: the declaration at decompile line 295158 remains the only occurrence of the identifier in Assembly-CSharp. |
Light |
public Color = Color.white |
Color applied to LensFlare.color on each SetCustomColor call. |
Color |
public Color = Color.white |
Forced base color (ReadOnly). |
Static shader property IDs (private, added since 0.2.6228.27061):
private static readonly int MaskColorId = Shader.PropertyToID("_MaskColor");
private static readonly int MaskMetallicId = Shader.PropertyToID("_MaskMetallic");
private static readonly int MaskSmoothnessId = Shader.PropertyToID("_MaskSmoothness");
Properties:
public int Index { get; }: lazy, callsGameManager.GetColorIndex(this)on first access and caches in_index.public bool IsSet => Index != -1.public string DisplayName => Localization.GetName(this).
Methods:
public string ToTooltip()returns"<color=yellow>" + DisplayName + "</color>".public void ApplyToSuitMaterial(Material mat), see "PaintOnly flag" below.
There is no DLCType field on ColorSwatch. A swatch carries no entitlement information; the DLC requirement for a color lives on the spray can prefab that dispenses it. See ../GameSystems/DLCGating.md.
PaintOnly flag¶
public bool PaintOnly does not exist in the 0.2.6228.27061 declaration and was added by 0.2.6403.27689. Its verbatim inspector tooltip:
[Tooltip("When true, this colour can ONLY be applied by spray painting - it is hidden from the logic colour dropdowns and rejected by IC10/logic Color writes. Used for cosmetic paint-only colours (e.g. the metallic spray cans). Default false so all existing colours stay logic-selectable (an absent value deserialises to false).")]
public bool PaintOnly;
The flag drives two independent behaviors.
Logic selectability. GameManager.GenerateColorStrings() builds the logic dropdown from only the non-PaintOnly swatches, keeping a parallel LogicColorIndices list so dropdown positions still map back to real swatch indices:
LogicColorIndices.Clear();
List<string> list = new List<string>(Singleton<GameManager>.Instance.CustomColors.Count);
for (int i = 0; i < Singleton<GameManager>.Instance.CustomColors.Count; i++)
{
if (!Singleton<GameManager>.Instance.CustomColors[i].PaintOnly)
{
LogicColorIndices.Add(i);
list.Add(Singleton<GameManager>.Instance.CustomColors[i].DisplayName);
}
}
ColorStrings = list.ToArray();
public static bool IsLogicSelectableColor(int colorIndex)
{
if (0 <= colorIndex && colorIndex < Singleton<GameManager>.Instance.CustomColors.Count)
{
return !Singleton<GameManager>.Instance.CustomColors[colorIndex].PaintOnly;
}
return false;
}
Note IsLogicSelectableColor returns false for an out-of-range index, so it doubles as a range check.
Metallic shader response. PaintOnly selects a metallic / smooth surface in both places a swatch is pushed into a material:
public void ApplyToSuitMaterial(Material mat)
{
if (!(mat == null))
{
mat.SetColor(MaskColorId, Color);
mat.SetFloat(MaskMetallicId, PaintOnly ? 0.85f : 0f);
mat.SetFloat(MaskSmoothnessId, PaintOnly ? 0.85f : 0f);
}
}
The same 0.85 pair appears in the mask-material component at decompile line 35041-35045:
public void SetColor(ColorSwatch colorSwatch)
{
_material.SetColor(MaskColor, colorSwatch.Color);
_material.SetFloat(MaskMetallic, colorSwatch.PaintOnly ? 0.85f : 0f);
_material.SetFloat(MaskSmoothness, colorSwatch.PaintOnly ? 0.85f : 0f);
}
ApplyToSuitMaterial callers: decompile lines 123788, 310669, 345793, 428172, 428610, 429946.
PaintOnly is not an entitlement flag. It coincides with the Metallic Paints DLC set in this version (the tooltip names the metallic spray cans as the motivating case) but it carries no DLCType and no DLC code path reads it. A mod must not treat PaintOnly == true as "requires DLC": the two happen to overlap today and nothing in the game guarantees they keep overlapping. The entitlement gate is documented in ../GameSystems/DLCGating.md.
ColorSwatch list¶
GameManager.CustomColors is a public List<ColorSwatch>, populated via the Unity inspector on the GameManager prefab (serialized field, not assembled at runtime). GameManager.IsValidColor(int index) checks 0 <= index < CustomColors.Count. Thing.SetCustomColor silently drops invalid indices: the method returns early without setting the color and without logging.
Because Emissive is optional per swatch, any code path that wants to render a Thing in emissive mode must null-check CustomColor.Emissive before relying on the material swap; for swatches where Emissive is null, only the shader-property path (setting _EmissionColor on the Normal material) produces visual change.
Normal vs Emissive selection¶
Thing.SelectColorSwatchMaterial(bool emissive) is overridable. At 0.2.6428.27798 the body below, quoted from an override, is also Thing's own virtual declaration (327891-327898); Frame (150572-150579) and Wall (329872-329879) override it with exactly this code, and PowerTool, which the 0.2.6228.27061 reading cited as the canonical body (decompile line 139053-139061), does not override it. SetCustomColor(int, bool) calls it to pick the material (327917); SetCustomColor(bool) does not, and reads CustomColor.Emissive / Normal directly:
public override Material SelectColorSwatchMaterial(bool emissive)
{
if (!emissive)
{
return CustomColor.Normal;
}
return CustomColor.Emissive;
}
Thing.SetCustomColor(int index, bool emissive = false) (decompile line 302860-302911) calls SelectColorSwatchMaterial(emissive) to choose the material, then for each CustomColorMapping entry in _customMaterials:
After the per-mapping swap, the method writes the shader-level _EmissionColor vector on every ThingRenderer:
EmissionColor = Color.white * (emissive ? 1f : 0f);
foreach (ThingRenderer renderer in Renderers)
{
renderer.SetShaderVectorProperty(EMISSION_COLOR, EmissionColor);
renderer.SetShaderFloatProperty(DiffuseIndexPropertyID, DiffuseIndex);
...
}
EMISSION_COLOR is a cached Shader.PropertyToID("_EmissionColor") on Thing. At 0.2.6428.27798 only the material swap reaches the screen; the _EmissionColor write reaches no material. ThingRenderer.SetShaderVectorProperty (329422-329425) only stores the value in the renderer's private _vectorProperties dictionary (329192), which nothing reads; ThingRenderer.FlushShaderProperties (329427-329429) is empty and never called; and the one other reader of Thing.EmissionColor, Structure.UpdateAllMaterialPropertyBlocks (320430-320444), is never called. What the call changes on screen is the CustomColorMapping swap (301512-301547): SetEmissive puts the swatch's Emissive material in the mapped slot, and SetColor puts Normal back or, on a texture-array mapping, restores the texture-array material after an earlier SetEmissive and sets _DiffuseIndex in the renderer's MaterialPropertyBlock. The same call also sets each LOD lens flare's colour to CustomColor.Light (327933-327939) and raises OnColorChange (327940-327943). EmissionColor itself goes from (0, 0, 0, 0) to (1, 1, 1, 1), not from Color.black. The 0.2.6228.27061 reading described two independent signals here, the material swap and the _EmissionColor shader property.
Transience of the emissive flag¶
The emissive parameter is not saved or synced. On Thing itself it survives only as EmissionColor, which SetCustomColor(int index, bool emissive) sets to Color.white * (emissive ? 1f : 0f) (327945) on each machine; SetCustomColor(bool emissive) redoes the material swap without touching it (327954-327974), so only the int overload recomputes both the swap and the _EmissionColor write. There is no IsEmissive field on Thing or on ThingSaveData, and neither the flag 32 block nor the join record carries the flag. The 0.2.6228.27061 reading said the parameter is NOT stored on Thing and that each SetCustomColor call recomputes the swap and the _EmissionColor write. Consequences:
- Save/load:
ThingSaveDatapersists onlyCustomColorIndex.Thing.DeserializeSavecallsSetCustomColor(saveData.CustomColorIndex)when the index is>= 0(327298-327301 at 0.2.6428.27798), with the defaultemissive: false; the 0.2.6228.27061 reading named the methodThing.LoadSimData(decompile line 302262-302287), and no method of that name exists at 0.2.6428.27798. From that 0.2.6228.27061 reading, not re-checked since: a flare that was glowing when the save was written loads non-glowing (see Open questions). - Network: no colour path carries the emissive flag. Server to client, colour travels in the flag
32block of the delta state, applied byThing.ProcessUpdatethroughSetCustomColor(index)(328391-328394), and in the per-Thing join and new-Thing record, applied byThing.DeserializeOnJointhroughSetCustomColor(b)(328122-328125); both calls use the defaultemissive: false.ThingColorMessagetravels the other way, client to server, and carries only(long ThingId, int ColorIndex):OnServer.SetCustomColor(39796-39810) callsthing.SetCustomColor(colorIndex)without passing an emissive value before a remote client sends it, andThingColorMessage.Process(283381-283384) does the same on the server. Emissive state is local-only; see../Protocols/ThingColorMessage.md. - Any re-entry reverts, unless the class re-applies emission: color change, color-message receive on the server, a client's colour update or join record, save-load; each path passes
emissive: false, so the base method sends every mapped renderer not taggedNotPaintableback throughCustomColorMapping.SetColorand setsEmissionColortoUnityEngine.Color.white * 0f, all four channels zero rather thanColor.black(327915-327951). Some classes put the emission back after the base call:WallLight(333693-333701),Beacon(346853-346861),PortableLight(359277-359285),PortablePortal(359346-359354) andStackableLight(360277-360286, the base ofChemLightandRoadFlare) callSetCustomColor(OnOff && Powered);LandingPadModular(188461-188468) passesOnOff && _flashto the base instead of the argument;GrowLight(403764-403767) passesfalseand then callsSetBulbMaterial(OnOff && Powered).
Mods that want emissive/glow to persist and sync must store their own IsGlowing flag per Thing and re-apply emissive after every SetCustomColor entry.
Vanilla callers of SetCustomColor with emissive: true¶
At 0.2.6428.27798 many sites can pass emissive: true, through either overload, and none of them runs on a wall, pipe, cable or frame. The 0.2.6228.27061 reading found only two, ChemLight.OnInteractableUpdated (then at decompile line 322433) and RoadFlare.OnInteractableUpdated (then at 334170), each called when the light's state flips to On.
- Through
SetCustomColor(int index, bool emissive), the only overload that also setsEmissionColor:LandingPadModular.SetCustomColorpassesOnOff && _flashto the base method instead of its argument (188463), andDiode.OnInteractableUpdatedcallsSetCustomColor(ColorState, OnOff && Powered)(331566). - Through
SetCustomColor(bool emissive), which only swaps materials (327954-327974), with an argument that can be true:ChemLight.OnInteractableUpdated(348410) andRoadFlare.OnInteractableUpdated(360342), both with the literalemissive: truewhen the light is switched on;LandingPadModular.FlashLights(188473) andOnInteractableUpdated(188489);LandingPadTaxiThreshold.OnInteractableUpdated(189267);LandingPadTaxiTile.FlashLights(189410);Diode.OnInteractableUpdated(331570);FlashingLight.ToggleLightsOn(332280);WallLight.SetCustomColor(333699),OnAnimationStop(333718),ToggleLightsOn(333773) andOnFinishedLoad(333853);Beacon.WaitThenSetColor(346850),SetCustomColor(346860) andOnInteractableUpdated(346866);PortableLight.OnAnimationStop(359272),SetCustomColor(359284) andOnInteractableUpdated(359292);PortablePortal.OnAnimationStop(359341),SetCustomColor(359353) andOnInteractableUpdated(359369);DiodeSlide.ToggleLightsOn(401448) andDeserializeSave(401465). - Not settled by the code:
StackableLight.SetCustomColor(360285), whichChemLightandRoadFlareinherit, passesOnOff && Powered, andPoweredcan be true only if the prefab carries aPoweredinteractable (see Open questions). - Never true despite the shape:
PortableLight.OnAnimationStart(359258) andPortablePortal.OnAnimationStart(359327) make the call insideif (!OnOff), andGrowLightpassesfalseto the base from both of its overrides (403764-403774) and callsSetBulbMaterial(OnOff && Powered)instead, so on aGrowLighttheWallLightcalls above passfalse.
Painted structures (walls, pipes, cables, frames) never receive emissive: true, which is why spray-painting a red pipe never produces a glow: the API supports it but no caller invokes it for them.
From the 0.2.6228.27061 reading, not re-checked at 0.2.6428.27798: because ChemLight and RoadFlare re-apply emissive: true on every state change (via OnInteractableUpdated firing on the On/Off toggle), the transience of the flag does not matter for flares in single-player. In multiplayer it still fails to sync: a flare turned on by the server does not glow for remote clients.
Vanilla swatch inventory (v0.2.6228.27061)¶
GameManager.CustomColors ships with 12 entries in game version 0.2.6228.27061. All 12 carry both Normal and Emissive materials; none has Emissive == null.
| Index | Name |
|---|---|
| 0 | ColorBlue |
| 1 | ColorGray |
| 2 | ColorGreen |
| 3 | ColorOrange |
| 4 | ColorRed |
| 5 | ColorYellow |
| 6 | ColorWhite |
| 7 | ColorBlack |
| 8 | ColorBrown |
| 9 | ColorKhaki |
| 10 | ColorPink |
| 11 | ColorPurple |
Verified via the Plans/GlowPaintProbe/ probe plugin's OnPrefabsLoaded enumeration on 2026-04-21 in game version 0.2.6228.27061. The per-entry breakdown (name="..." normal=yes emissive=yes) is recorded in the probe's startup log lines.
The if (CustomColor.Emissive == null) null-checks that appear in vanilla decompiled code (documented above in "Declaration and fields") remain defensive code: the possibility of a null Emissive is coded against, but no shipping vanilla swatch currently hits the null branch. Mod-added swatches may still populate Emissive as null, and any mod iterating CustomColors across installs should continue to null-check.
Metallic swatch addition (v0.2.6403.27689)¶
The Metallic Paints DLC (DLCType.MetallicPaints, Steam app 4842920) adds four spray cans on top of the twelve vanilla ones, confirmed from rocketstation_Data/StreamingAssets/Data/paints.xml (Tool Manufactory recipes) and rocketstation_Data/StreamingAssets/Language/english.xml (localization keys):
ItemSprayCanMetallicBronzeItemSprayCanMetallicGoldItemSprayCanMetallicObsidianItemSprayCanMetallicSilver
CustomColors holds 16 entries at 0.2.6403.27689: the vanilla twelve at indices 0-11 unchanged, plus the four metallic swatches at indices 12-15. All four carry PaintOnly = true, and no vanilla swatch does. Confirmed by runtime enumeration (see below for method).
| Index | Swatch name | PaintOnly |
Dispensing prefab | Thing.DLCType |
|---|---|---|---|---|
| 0-11 | ColorBlue ... ColorPurple |
false | ItemSprayCan<Color> |
None |
| 12 | ColorObsidian |
true | ItemSprayCanMetallicObsidian |
MetallicPaints |
| 13 | ColorSilver |
true | ItemSprayCanMetallicSilver |
MetallicPaints |
| 14 | ColorBronze |
true | ItemSprayCanMetallicBronze |
MetallicPaints |
| 15 | ColorGold |
true | ItemSprayCanMetallicGold |
MetallicPaints |
Two naming traps. The swatch names carry NO Metallic prefix (ColorObsidian, not ColorMetallicObsidian) while the prefabs do (ItemSprayCanMetallicObsidian). And the swatch order (Obsidian, Silver, Bronze, Gold) matches neither alphabetical order nor the order the recipes appear in paints.xml (Bronze, Gold, Obsidian, Silver). Never derive one identifier from the other by string manipulation, and never assume the DLC ordering.
Every swatch's Normal Material is a distinct asset. A pairwise ReferenceEquals scan across all 16 Normal materials found no sharing at any index, including among the four metallic swatches. Every one of the 16 SprayCan prefabs resolves to exactly one swatch by ReferenceEquals(swatch.Normal, can.PaintMaterial) (16 cans, 16 distinct matches, one match each, no collisions). A Material-keyed swatch lookup is therefore sound, which is what makes GameManager.GetColorSwatch(Material) work and what lets a mod recover the color-index-to-DLCType map from the can prefabs.
Both swatch presence and prefab presence are independent of entitlement: a player who does not own Metallic Paints still has all 16 swatches in CustomColors and all 16 SprayCan prefabs in Prefab.AllPrefabs. Only acquisition of the item is gated (see ../GameSystems/DLCGating.md), which is precisely why an index-driven color picker bypasses the gate.
Verified on 2026-07-25 in game version 0.2.6403.27689 by runtime enumeration on the headless dedicated server, via the spp-color-swatch-probe scenario in TestRig/DedicatedServer/dev-plugins/ScenarioRunner/. The probe walks GameManager.Instance.CustomColors and Prefab.AllPrefabs, reading only managed state (ColorSwatch.Name, ColorSwatch.PaintOnly, Thing.PrefabName, Thing.DLCType) and comparing Material references with object.ReferenceEquals plus RuntimeHelpers.GetHashCode. Material.name and GetInstanceID() are deliberately avoided because the scenario pump runs on a UniTask worker rather than the Unity main thread. Fresh Mars2 world, ~60 Workshop mods loaded alongside.
No mod-registered swatches appeared despite roughly 60 Workshop mods being loaded in that run, so CustomColors.Count was exactly 16. Mod swatches, where a mod adds them, land at index 16 and up in an install like that one.
Vanilla swatch material naming (runtime)¶
Runtime observation via the Plans/GlowPaintProbe/ plugin on 2026-04-21 captured the material and shader details for painted pipes (Piping and InLineTank classes receiving SetCustomColor(index, emissive)):
| Mode | Material name | Shader | _EmissionColor |
_EMISSION keyword |
|---|---|---|---|---|
| Normal | TextureArrayColorSwatch (shared across all swatches) |
Custom/StandardTextureArray |
(0, 0, 0, 0) |
off |
| Emissive | Color<Name>Emissive (per-color, e.g. ColorPurpleEmissive) |
StandardInstanced |
HDR pre-baked per color | on |
Findings:
- The
Normalmaterial is SHARED across all swatches (TextureArrayColorSwatch) and selects the color via texture-array indexing, consistent with theCustom/StandardTextureArrayshader name. - The
Emissivematerial is PER-COLOR (one Material asset per swatch, namedColor<Name>Emissive), not shared. - The emissive shader is Unity's
StandardInstanced(GPU-instanced Standard), which supports the_EMISSIONkeyword and_EmissionColorproperty. - Emissive
_EmissionColorvalues exceed1.0on individual components, encoding HDR brightness that UltimateBloom picks up into a visible halo. ForColorPurple:(1.051, 0.000, 2.290, 1.000). SetCustomColor(index, emissive: true)drives two changes simultaneously: material asset swap (from shared Normal to per-color Emissive) AND_EmissionColorshader-property write. The_EMISSIONshader keyword is enabled on the Emissive material. At 0.2.6428.27798 only the swap reaches a material: the_EmissionColorwrite stays in a dictionary on theThingRenderer(see "Normal vs Emissive selection"). The HDR_EmissionColorlogged below is the Emissive material's own value, not theColor.whitethatSetCustomColorcomputes.
Runtime evidence for a Piping instance with CustomColor.Index = 11 (ColorPurple):
SetCustomColor(11, emissive: true)
-> renderer[0] shader="StandardInstanced" material="ColorPurpleEmissive" _EmissionColor=RGBA(1.051, 0.000, 2.290, 1.000) _EMISSION=on
SetCustomColor(11, emissive: false)
-> renderer[0] shader="Custom/StandardTextureArray" material="TextureArrayColorSwatch" _EmissionColor=RGBA(0.000, 0.000, 0.000, 0.000) _EMISSION=off
Verified visually: the pipe glows with a bloom halo in a dark room after F9; reverts to flat-colored pipe after F10. Confirmed via Plans/GlowPaintProbe/ probe logs on 2026-04-21 in game version 0.2.6228.27061.
Verification history¶
- 2026-04-21: page created. Decompile findings sourced from Assembly-CSharp.dll (ColorSwatch declaration, Thing.SetCustomColor body at line 302860-302911, PowerTool.SelectColorSwatchMaterial at line 139053-139061, Thing.LoadSimData at line 302262-302287, ChemLight.OnInteractableUpdated at line 322433, RoadFlare.OnInteractableUpdated at line 334170).
- 2026-04-21: added "Vanilla swatch inventory (v0.2.6228.27061)" section with the 12 shipping swatches and their Normal/Emissive presence, verified via the GlowPaintProbe plugin logs. Resolves the open question about how many swatches populate
Emissive: the answer is all 12. Defensive null-check guidance retained for mod-added swatches. - 2026-04-21: added "Vanilla swatch material naming (runtime)" section documenting the
TextureArrayColorSwatch+Custom/StandardTextureArraynormal pair and per-colorColor<Name>Emissive+StandardInstancedemissive pair, with HDR_EmissionColorvalues and keyword state. Confirmed visible bloom halo on a dark-room pipe via the probe's F9 / F10 flow. Verified throughPlans/GlowPaintProbe/plugin logs. - 2026-07-25: re-read the
ColorSwatchdeclaration against 0.2.6403.27689. Restamped "Declaration and fields" and added the newPaintOnlyfield, the three privateMask*Idshader property IDs,ToTooltip(), andApplyToSuitMaterial(Material). Added the "PaintOnly flag" section with the verbatim tooltip, theGenerateColorStrings/IsLogicSelectableColorlogic-dropdown filter, and the metallic shader response. Added "Metallic swatch addition (v0.2.6403.27689)". Added the explicit finding thatColorSwatchcarries noDLCType, cross-referenced to the new../GameSystems/DLCGating.md. - 2026-07-25: version drift on the
Bitfield. The 0.2.6228.27061 row read "Legacy field, currently unused". At 0.2.6403.27689 the identifier has four call sites inAssembly-CSharp(decompile lines 338789, 338892, 338931, 338989, the color-scanner / screen-color UI path). No fresh validator was spawned: the two statements are scoped to different game versions and both can be accurate, and the new reading rests on direct grep evidence rather than a competing interpretation of the same code, so per../WORKFLOW.mdRule 3 this is an additive version-scoped update rather than a contradiction. The original characterization is preserved by reference in the field row. WhetherBitwas genuinely unreferenced at 0.2.6228.27061 was not re-checked against that version's DLL. - 2026-07-25: re-checked
Cutableagainst 0.2.6403.27689. The declaration remains the only occurrence of the identifier; the open question below still stands. - 2026-07-25: resolved the two open questions raised earlier the same day by runtime enumeration on the headless dedicated server (
spp-color-swatch-probe, method and caveats recorded in "Metallic swatch addition").CustomColors.Countis 16 with the metallic swatches at indices 12-15 in the order Obsidian, Silver, Bronze, Gold, all fourPaintOnly = true. Every swatch'sNormalMaterial is a distinct asset (pairwiseReferenceEqualsscan, no sharing), and all 16SprayCanprefabs resolve one-to-one to a swatch by material reference, so aMaterial-keyed lookup is sound. Also recorded: swatches and prefabs are present regardless of entitlement, and no mod-registered swatches appeared under ~60 Workshop mods. "Metallic swatch addition" rewritten from the unconfirmed placeholder to the confirmed table; both questions removed from Open Questions. - 2026-09-21 (game version 0.2.6428.27798): corrected three bullets in "Transience of the emissive flag" in the same pass that corrected ThingColorMessage, whose Verification History records each fresh validator's verdict in full. (1) Stale name. Previous: "
Thing.LoadSimData(decompile line 302262-302287) callsSetCustomColor(saveData.CustomColorIndex)with the defaultemissive: false." New finding: noLoadSimDataexists at 0.2.6428.27798; the load isThing.DeserializeSave(327298-327301). Fresh validator verdict: B is correct, confirmed against the IL of the installed DLL. (2) Direction. Previous: "ThingColorMessage.Process(line 259881-259883) does the same on the receiver", in a bullet that presents the message as the network colour path. New finding: the message travels client to server only, and clients get colour through flag32inThing.ProcessUpdateand throughThing.DeserializeOnJoin. Fresh validator verdict: B is correct, quoting theOnServer.SetCustomColorbody (39796-39810). (3) Re-entry. Previous: "Any re-entry reverts: color change, color-message receive, save-load; each path passesemissive: falseand reverts the renderer to the Normal material with_EmissionColor = Color.black." New finding: the base method zeroes all four channels (Color.white * 0f, notColor.black), and seven light classes re-apply emission after the base call. Fresh validator verdict: B is correct, quotingWallLight.SetCustomColor(333693-333701); it could not settle the flare case from the decompile, which is now an Open question. Result: the three bullets rewritten under inline 0.2.6428.27798 stamps, the flare sentence kept as the 0.2.6228.27061 reading and marked as not re-checked. The section stamp stays at 0.2.6228.27061 because its opening paragraph was not re-verified. The sources pointerThing.LoadSimDatanow namesThing.DeserializeSave, and a decompile sources line was added. Bumped frontmatter verified_in / verified_at. - 2026-09-21 (third pass, game version 0.2.6428.27798): two conflicts, each with a fresh validator. (1) "Normal vs Emissive selection" said "So the renderer is driven by two independent signals on every color change: the material-swap (material asset changes) and the
_EmissionColorshader property (value changes fromColor.blacktoColor.white)", and "Vanilla swatch material naming (runtime)" said the same ofSetCustomColor(index, emissive: true). New finding: the_EmissionColorvalue reaches no material at 0.2.6428.27798; it stays inThingRenderer._vectorProperties, which nothing reads, and the one other reader ofThing.EmissionColoris never called. Fresh validator verdict: B is correct, quotingThingRenderer.SetShaderVectorPropertyand the emptyFlushShaderProperties(329422-329429), checked against the IL of the installed DLL. It added the lens-flare colour andOnColorChangeas the call's other visible effects, and reported that the float properties written beside it never reach a batched instance either, becauseStructure.SetCustomColorthrows for batched structures before its base call. Result: a 0.2.6428.27798 paragraph after the "Normal vs Emissive selection" code and a note on the runtime bullet, both under inline stamps; the 0.2.6228.27061 text and the runtime log are kept. (2) "Vanilla callers of SetCustomColor with emissive: true" said "Only two sites in vanilla passemissive: true" (ChemLightandRoadFlare) and "No other vanilla code path passesemissive: true." New finding: many sites across both overloads. Fresh validator verdict: Neither. The proposed census was right except thatPortableLight.OnAnimationStartandPortablePortal.OnAnimationStartcan never pass true (the call sits insideif (!OnOff)) andStackableLight.SetCustomColorcannot be settled from code; it quotedSetCustomColor(ColorState, OnOff && Powered);(331566) andSetCustomColor(on);(333773) against "only two sites". Result: the census rewritten to the validator's answer under an inline 0.2.6428.27798 stamp, the painted-structure sentence re-verified, and the single-player and multiplayer flare paragraph kept as the 0.2.6228.27061 reading and marked as not re-checked. Section stamps stay at 0.2.6228.27061. Added a sources line. - 2026-09-21 (fourth pass, game version 0.2.6428.27798): two conflicts, each with a fresh validator. (1) "Transience of the emissive flag" said "The
emissiveparameter is NOT stored onThing. EachSetCustomColorcall recomputes the material swap and the_EmissionColorwrite". New finding:SetCustomColor(int, bool)records the argument inThing.EmissionColor, per machine, andSetCustomColor(bool)does not write that field. Fresh validator verdict: B is correct, quoting theSetCustomColor(bool)body (327954-327974); it found the one writer at 327945 and no save or sync of the value. The same verdict corrected RenderingPipelineAndGlow. (2) "Normal vs Emissive selection" said "PowerTool.SelectColorSwatchMaterial(decompile line 139053-139061) is the canonical body". New finding:PowerToolhas no override at 0.2.6428.27798; the body isThing's own virtual declaration, overridden with the same code byFrameandWall. Fresh validator verdict: B is correct, quotingThing.SelectColorSwatchMaterial(327891-327898) and confirming from the installed DLL's metadata that onlyThing,FrameandWalldeclare the method. Result: both passages rewritten under inline 0.2.6428.27798 stamps, and the sources pointer now namesThing.SelectColorSwatchMaterial. Section stamps stay at 0.2.6228.27061. Also added an Open question that was raised but not checked (flare glow on remote clients). Added a sources line.
Open questions¶
ColorSwatch.CutableMaterial: inspector-visible but the consumer has not been traced. Possibly a cut-out / cutscene variant. Still unreferenced at 0.2.6403.27689 (declaration is the sole occurrence).- Whether
PaintOnlyand DLC entitlement stay correlated. At 0.2.6403.27689 the two coincide exactly (the fourPaintOnlyswatches are precisely the fourMetallicPaintsones), butPaintOnlycarries noDLCTypeand the game does not couple them, so this is an observed coincidence rather than a guarantee. Code should gate on the prefab-derivedDLCType, not onPaintOnly. - Does setting
_EmissionColoralone on the sharedNormalmaterial (without swapping to the Emissive asset) produce visible glow? The Normal shader isCustom/StandardTextureArray(a custom game shader); whether it honors_EmissionColorwith the_EMISSIONkeyword disabled is unknown. Because the Normal material is SHARED across all painted Things, the property-write path would require a per-instance material clone per../Patterns/UnityMaterialPerInstance.md. Relevant only for mod-added swatches whoseEmissiveis null, since every vanilla swatch has a pre-baked Emissive asset. - Does a lit flare still load non-glowing, and lose its glow on a repaint, at 0.2.6428.27798?
ChemLightandRoadFlareinheritStackableLight.SetCustomColor(int, bool)(360277-360286), which callsSetCustomColor(OnOff && Powered)after the base call, while they light themselves throughSetCustomColor(emissive: true)inOnInteractableUpdated(348410, 360342).PoweredisPoweredValue >= 1(324216), andPoweredValuestays 0 unless the Thing has aPoweredinteractable (HasPowerState, set inCacheStatesat 327722; getter 324218-324233); whether a flare prefab has one is prefab data the decompile cannot show.Thing.DeserializeSavealso replaysOnInteractableUpdatedfor join-synced interactables (327289-327297) before its colour load (327298-327301), and loads the colour only when the saved index is>= 0, so the order and whether the flare was painted both matter. Needs a runtime check. - Does a flare turned on by the server still fail to glow for remote clients at 0.2.6428.27798, as the 0.2.6228.27061 paragraph of "Vanilla callers of SetCustomColor with emissive: true" says? Raised, not checked, during the 2026-09-21 correction pass: it is not known whether a client runs
ChemLight/RoadFlareOnInteractableUpdated, which callsSetCustomColor(emissive: true), when the OnOff state reaches it.