Passive tooltip pipelines¶
Two independent code paths render a hover tooltip for a world object, and they gate on DIFFERENT fields of the same PassiveTooltip struct. The crosshair pipeline (normal first-person play) displays a passive tooltip only when Title is non-empty; the mouse-control pipeline (the ALT free-cursor mode) renders whenever any populated field exists, Extended alone included. A mod appending hover text must know which pipeline it is feeding. (The PassiveTooltip struct body, the Device -> Structure -> Thing dispatch chain, and the TextMeshPro render path are on ElectricalInputOutput, "GetPassiveTooltip: body-hover tooltip resolution chain".)
The law¶
- An
Extended-only tooltip renders under mouse control (ALT) but NEVER on a crosshair hover. - A mod appending hover text to a passive tooltip must fill
Titlefor crosshair visibility;Extendedalone is invisible there. - Crosshair hovers that resolve to a tool action bypass
GetPassiveTooltipoutput entirely: the displayed tooltip is built from theDelayedActionInstance, so aGetPassiveTooltippatch never affects those hovers. - An undamaged
Structurewith no ExtendedTooltips gate passing produces the all-empty base tooltip, which neither pipeline renders: crosshair fails theTitlegate, mouse control has every_has*flag false.
Crosshair pipeline: InventoryManager.NormalModeThing requires a non-empty Title¶
InventoryManager.NormalModeThing() (declared at decompile 287864 at 0.2.6403.27689) is the per-frame crosshair-hover handler in normal play. It asks the hovered thing for its passive tooltip once (287869):
PassiveTooltip cursorPassiveTooltip2 = ((CursorManager.CursorThing != null) ? CursorManager.CursorThing.GetPassiveTooltip(cursorTargetCollider) : default(PassiveTooltip));
When the hover carries a tool action (a DelayedActionInstance from AttackWith / InteractWith previews), the displayed tooltip is constructed from the action instance instead, bypassing cursorPassiveTooltip2. The two action-built sites (287917-287923 and 287947-287953), verbatim:
if (delayedActionInstance3 != null)
{
color = ((!delayedActionInstance3.IsDisabled) ? ((delayedActionInstance3.Duration > 0f) ? UnityEngine.Color.yellow : UnityEngine.Color.green) : UnityEngine.Color.red);
PassiveTooltip cursorPassiveTooltip3 = new PassiveTooltip(delayedActionInstance3, string.Empty, CursorManager.CursorThing);
cursorPassiveTooltip3.color = color;
TooltipRef.HandleToolTipDisplay(cursorPassiveTooltip3);
}
else if (delayedActionInstance != null)
{
color = ((!delayedActionInstance.IsDisabled) ? ((delayedActionInstance.Duration > 0f) ? UnityEngine.Color.yellow : UnityEngine.Color.green) : UnityEngine.Color.red);
PassiveTooltip cursorPassiveTooltip5 = new PassiveTooltip(delayedActionInstance, string.Empty, CursorManager.CursorThing);
cursorPassiveTooltip5.color = color;
TooltipRef.HandleToolTipDisplay(cursorPassiveTooltip5);
}
The GetPassiveTooltip result reaches the renderer through exactly one branch, and that branch is gated on a non-empty Title (287963-287967), verbatim:
else if (!cursorPassiveTooltip2.Title.Equals(string.Empty))
{
TooltipRef.HandleToolTipDisplay(cursorPassiveTooltip2);
CursorManager.ClearLastSelectionId();
}
A passive tooltip whose Title is empty never reaches HandleToolTipDisplay on this pipeline, no matter how much Extended text it carries.
Mouse-control pipeline: InputMouse.Idle hands over unconditionally¶
InputMouse (public class InputMouse : UserInterfaceBase, 239369) drives the ALT free-cursor mode. Its Idle() handler (239679) sources the tooltip from the same producer (239691: passiveTooltip = CursorThing.GetPassiveTooltip(hitInfo.collider);) but hands it to the renderer with NO field gate at all (239716-239721), verbatim:
if (interactable != null)
{
Tooltip.SetValuesForInteractable(ref passiveTooltip, CursorThing, interactable);
}
passiveTooltip.FollowMouseMovement = true;
InventoryManager.Instance.TooltipRef.HandleToolTipDisplay(passiveTooltip);
The visibility decision happens inside Tooltip.HandleToolTipDisplay (public class Tooltip : UserInterfaceBase, 253966; method at 254322), where a non-empty Extended is sufficient on its own: the panel-visible flag flag2 includes _hasExtended, and the extended block toggles on its own flag. Verbatim (254322-254351, trimmed after the per-block toggles):
public void HandleToolTipDisplay(PassiveTooltip cursorPassiveTooltip)
{
WantDraw = true;
if ((bool)InventoryManager.ParentHuman && (bool)InventoryManager.Instance.ActiveHand.Slot.Occupant && InventoryManager.Instance.ActiveHand.Slot.Occupant is Tablet)
{
return;
}
SetUpToolTip(cursorPassiveTooltip.Action, cursorPassiveTooltip);
bool flag = !InventoryManager.Instance.UIProgressionBar.IsVisible && (cursorPassiveTooltip.ShowRotate || cursorPassiveTooltip.ShowScroll || (cursorPassiveTooltip.ShowAction && _hasAction));
bool flag2 = !InventoryManager.Instance.UIProgressionBar.IsVisible && (_hasAction || _hasConstruction || _hasDeconstruction || _hasPlacement || _hasState || _hasTitle || _hasRepair || _hasExtended);
if (!flag2 && !flag)
{
UiComponentRenderer.SetVisible(isVisble: false);
return;
}
if (!UiComponentRenderer.IsVisible)
{
UiComponentRenderer.SetVisible(isVisble: true);
}
FullToolTipHotKeys.SetVisible(flag);
FullToolTipPanel.SetVisible(flag2);
if (flag2)
{
InfoConstructionGameObject.SetVisible(_hasConstruction);
InfoRepairGameObject?.SetVisible(_hasRepair);
InfoDeconstructGameObject.SetVisible(_hasDeconstruction);
InfoPlacementGameObject.SetVisible(_hasPlacement);
StateRenderer.SetVisible(_hasState);
ExtendedRenderer.SetVisible(_hasExtended);
TitleRenderer.SetVisible(_hasTitle);
(_hasExtended = !string.IsNullOrEmpty(_extended) && _extended.Length > 0; is computed inside SetUpToolTip at 254314.) So under mouse control an Extended-only tooltip renders: the panel shows with only the extended block visible. The crosshair pipeline funnels through this same method, but only after its own Title gate upstream; the asymmetry lives in the callers, not here.
Producer gates: Structure.GetPassiveTooltip returns the all-empty tooltip unless an ExtendedTooltips gate passes¶
The base producer is Thing.GetPassiveTooltip (319731-319734), which returns a defaulted struct whose every string is empty. Verbatim, plus the head of the PassiveTooltip(bool) constructor (307089-307104; the body continues past the excerpt):
public virtual PassiveTooltip GetPassiveTooltip(Collider hitCollider)
{
return new PassiveTooltip(true);
}
public PassiveTooltip(bool toDefault = true)
{
Title = string.Empty;
Action = string.Empty;
State = string.Empty;
Extended = string.Empty;
RepairString = string.Empty;
DeconstructString = string.Empty;
ConstructString = string.Empty;
PlacementString = string.Empty;
ShowRotate = false;
ShowScroll = false;
ShowConstructionRotate = false;
ShowAction = true;
BuildStateIndexMessage = string.Empty;
color = UnityEngine.Color.white;
Structure.GetPassiveTooltip (314440-314471) falls through to that empty base for every undamaged structure unless one of three gates passes. ALL three require the vanilla ExtendedTooltips game setting (Settings.CurrentData.ExtendedTooltips); the build and deconstruct gates additionally require an IShowBuildStateTooltip item in the active hand (the build gate waives the hand requirement in the upgrade-state case). Verbatim (gates 314388-314438, method 314440-314471):
private bool ShowBuildTooltip()
{
bool flag = IsUpgradeState();
if (Settings.CurrentData.ExtendedTooltips && (flag || !IsStructureCompleted) && NextBuildState != null)
{
if (!flag)
{
Slot activeHandSlot = InventoryManager.ActiveHandSlot;
if (activeHandSlot != null)
{
return activeHandSlot.Occupant is IShowBuildStateTooltip;
}
return false;
}
return true;
}
return false;
}
private bool ShowDeconstructTooltip()
{
if (Settings.CurrentData.ExtendedTooltips && (bool)InventoryManager.Parent)
{
Slot activeHandSlot = InventoryManager.ActiveHandSlot;
if (activeHandSlot != null)
{
return activeHandSlot.Occupant is IShowBuildStateTooltip;
}
return false;
}
return false;
}
private bool ShowRepairTooltip()
{
if (Settings.CurrentData.ExtendedTooltips && RepairTools.IsValid())
{
return DamageState.Total > 0f;
}
return false;
}
private bool IsUpgradeState()
{
ToolUseType? toolUseType = NextBuildState?.Tool?.ToolUseType;
if (toolUseType.HasValue)
{
return toolUseType == ToolUseType.Upgrade;
}
return false;
}
public override PassiveTooltip GetPassiveTooltip(Collider hitCollider)
{
bool flag = ShowBuildTooltip();
bool flag2 = ShowDeconstructTooltip();
bool flag3 = ShowRepairTooltip();
if (DamageState.Total <= 0f && !flag && !flag2 && !flag3)
{
return base.GetPassiveTooltip(hitCollider);
}
PassiveTooltip passiveTooltip = new PassiveTooltip(true);
passiveTooltip.Title = DisplayName;
passiveTooltip.Extended = GetExtendedText().ToString();
PassiveTooltip result = passiveTooltip;
if (flag)
{
result.ConstructString = NextBuildState.Tool.GetToolsAsString();
}
if (IsBroken && BrokenBuildStates.Count > 0)
{
int index = Mathf.Clamp(Mathf.Abs(CurrentBuildStateIndex) - 1, 0, BrokenBuildStates.Count - 1);
result.DeconstructString = BrokenBuildStates[index].BuildState.Tool.GetExitToolAsString();
}
else
{
result.DeconstructString = CurrentBuildState.Tool.GetExitToolAsString();
}
if (flag3)
{
result.RepairString = RepairTools.GetRepairsAsString();
}
return result;
}
Gate census:
| Gate | Requires |
|---|---|
ShowBuildTooltip (314388-314405) |
ExtendedTooltips AND (IsUpgradeState() OR not IsStructureCompleted) AND NextBuildState != null; when not upgrade-state, ALSO active-hand occupant is IShowBuildStateTooltip |
ShowDeconstructTooltip (314407-314419) |
ExtendedTooltips AND InventoryManager.Parent AND active-hand occupant is IShowBuildStateTooltip |
ShowRepairTooltip (314421-314428) |
ExtendedTooltips AND RepairTools.IsValid() AND DamageState.Total > 0f |
When DamageState.Total <= 0f and no gate passes, the return is the all-empty base tooltip: invisible on both pipelines. When any gate passes (or the structure is damaged), the struct gets Title = DisplayName and Extended = GetExtendedText().ToString() plus the construct / deconstruct / repair strings.
Mod consequences¶
- A
Structure.GetPassiveTooltippostfix that only appends to__result.Extendedis invisible to crosshair players whenever the vanilla body fell through to the empty base (undamaged structure, gates closed):Titlestayed empty andNormalModeThing's gate at 287963 drops the tooltip. Set__result.Title(typically__instance.DisplayName) in the same postfix. - Concrete case:
CableRupturedwreckage spawns undamaged (DamageState.Total == 0, see Cable, "The wreckage spawns undamaged"), so a burn-reason annotation on the wreckage must fillTitleto show on a crosshair hover;Extended-only text appears solely in the ALT mouse-control hover. - Tool-action hovers (crosshair with a tool in hand producing a
DelayedActionInstance) never consultGetPassiveTooltipoutput; annotating those requires touching the action-instance path (287920 / 287950), not the passive-tooltip producer.
Per-class Title filling: which device bodies render Extended-only text on a crosshair hover¶
The crosshair Title gate makes the practical visibility of an appended Extended line depend on whether the hovered class's own GetPassiveTooltip override fills Title. Census at 0.2.6403.27689:
Title-FILLING bodies (Extended-only annotations DO render on a bare crosshair hover):
SolarPanel.GetPassiveTooltip(421384-421397): when fully built, always setsTitle = DisplayNameplusState = SolarInfo().PowerGeneratorPipe.GetPassiveTooltip(396655-396675): fillsTitle = DisplayNamewhenever the base chain left it empty.StirlingEngine.GetPassiveTooltip(424241-424269): same pattern.AreaPowerControl.GetPassiveTooltip(390800-390810): the null-collider body poll getsTitle = DisplayNameplus the charge readout in Extended.- Any DAMAGED structure (the damage gate fills a repair tooltip) and any Input / Output port collider on the
ElectricalInputOutputfamily (395008-395023 titles the port colliders).
Title-LESS bodies (Extended-only annotations render ONLY under ALT):
- The
ElectricalInputOutputfamily body hover (Transformer, Battery, PowerTransmitter, PowerReceiver, both umbilical halves): only port colliders are titled. PowerConnector(408050-408059): only its input-port collider.- Every class that falls through to the empty base tooltip (Thing 319731-319734), including undamaged wreckage.
The cursor-target accessor¶
The Thing under the crosshair or mouse resolves through CursorManager.CursorThing (class CursorManager : ManagerBase in namespace Assets.Scripts, decompile 200808; example vanilla consumer at 115374). A mod patch bracketing InventoryManager.NormalModeThing with a flag and completing the tooltip at Tooltip.HandleToolTipDisplay (Assets.Scripts.UI.Tooltip) can therefore attribute the displayed tooltip to the cursor target and append or Title-fill additively, covering the tool-in-hand branches that bypass GetPassiveTooltip; PowerGridPlus's CursorTooltipChokePatches is the working example.
Verification history¶
- 2026-07-18: added the cursor-target accessor section (CursorManager.CursorThing, Assets.Scripts namespace, decompile 200808) with the choke-point pattern note.
- 2026-07-18: added the per-class Title-filling census (SolarPanel / PowerGeneratorPipe / StirlingEngine / AreaPowerControl fill Title on the body; the ElectricalInputOutput family and PowerConnector title only port colliders), read from the 0.2.6403.27689 decompile during the hover-surface matrix review.
- 2026-07-18: page created (game version 0.2.6403.27689). All quoted lines read directly from
.work/decomp/0.2.6403.27689/Assembly-CSharp.decompiled.cs; the findings were produced and independently adversarially verified against the decompile this session. Crosshair pipeline:InventoryManager.NormalModeThing(287864) builds the hover tooltip fromGetPassiveTooltip(287869) and displays it only whenTitleis non-empty (287963); tool-action hovers buildPassiveTooltip(DelayedActionInstance, ...)directly (287920, 287950). Mouse-control pipeline:InputMouse.Idle(239679, tooltip sourced at 239691) hands the tooltip toHandleToolTipDisplayunconditionally (239720-239721);Tooltip.HandleToolTipDisplay(254322) treats a non-emptyExtendedas sufficient (flag2includes_hasExtendedat 254331;ExtendedRenderer.SetVisible(_hasExtended)at 254350). Producer gates:Structure.GetPassiveTooltip(314440-314471) with the three ExtendedTooltips-gated helpers (314388-314428) and the all-emptyThingbase (319731-319734;PassiveTooltip(bool)zeroesTitleat 307091).
Open questions¶
None.