WirelessPower¶
Vanilla base class at Assets.Scripts.Objects.Electrical.WirelessPower : ElectricalInputOutput, IRotatable (decompile line 426779 in the 0.2.6403.27689 decompile). The shared servo / dish-aim machinery used by PowerTransmitter and PowerReceiver; those two are the ONLY subclasses. PowerTransmitterOmni does NOT derive from this class (it is PowerTransmitterOmni : Electrical, a sibling branch under Device; see PowerTransmitterOmni). Holds Horizontal / Vertical properties (servo state in normalized [0..1] units), the dish transform tree, and the network-sync hook that ships H/V to clients.
Servo math¶
Source: Assets/Scripts/Objects/Electrical/WirelessPower.cs:66-100.
public double Vertical
{
get { return _vertical; }
set
{
if (_vertical != value)
{
_vertical = value;
if ((bool)DishTransform)
{
DishTransform.localRotation = Quaternion.Euler(Mathf.Lerp(90f, -90f, (float)_vertical), 0f, 0f);
}
DishForward = DishTransform.up;
RayPosition = RayTransform.position;
IsDirty = true;
}
}
}
public double Horizontal
{
get { return _horizontal; }
set
{
if (_horizontal != value)
{
_horizontal = value;
if ((bool)AxleTransform)
{
AxleTransform.localRotation = Quaternion.Euler(0f, (float)(_horizontal * MaximumHorizontal), 0f);
}
AxleForward = AxleTransform.up;
RayPosition = RayTransform.position;
IsDirty = true;
}
}
}
public double MaximumVertical => 180.0;
public double MaximumHorizontal => 360.0;
Both setters write localRotation (parent-relative). The math is therefore frame-invariant: if the dish's root transform is rotated (sideways on a wall, upside-down on a ceiling), the same Vertical and Horizontal ratios still produce the dish-local Euler rotations the geometry expects. The sphere of reachable directions rotates with the parent.
MaximumHorizontal = 360.0 and MaximumVertical = 180.0 are degree-space constants; _vertical and _horizontal are normalized [0..1] ratios. Lerp(90f, -90f, V) maps V=0 -> +90 deg pitch (nadir of the dish-local frame) and V=1 -> -90 deg pitch (zenith of the dish-local frame).
Vertical clamping in input paths¶
Source: WirelessPower.cs:273-293, 389-395.
The setter itself does not clamp, but every public consumer of Vertical does:
- Logic-write (
SetLogicValueforLogicType.Vertical): clampsvalue < 0.0to 0 andvalue > MaximumVerticaltoMaximumVertical, then divides byMaximumVerticalto produce the normalized ratio. - Interaction button 3 (decrement vertical):
if (num < 0.0) num = 0.0;. - Interaction button 4 (increment vertical):
if (num > 1.0) num = 1.0;.
The dish servo's reachable space is the full hemisphere from nadir (V=0) to zenith (V=1) in the dish-local frame. Mods that need finer-grained control (e.g. clamping to 7 deg above horizon for visual realism, or removing the clamp to allow wrap-around) can patch WirelessPower.SetLogicValue or the interaction handlers.
Network sync (H/V via flag 256)¶
Source: WirelessPower.cs:120-168.
public override void BuildUpdate(RocketBinaryWriter writer, ushort networkUpdateType)
{
base.BuildUpdate(writer, networkUpdateType);
if (Thing.IsNetworkUpdateRequired(256u, networkUpdateType))
{
writer.WriteFloatHalf((float)(RotatableBehaviour?.TargetVertical ?? 0.0));
writer.WriteFloatHalf((float)(RotatableBehaviour?.TargetHorizontal ?? 0.0));
}
if (Thing.IsNetworkUpdateRequired(512u, networkUpdateType))
{
writer.WriteFloatHalf(VisualizerIntensity);
}
}
public override void ProcessUpdate(RocketBinaryReader reader, ushort networkUpdateType)
{
base.ProcessUpdate(reader, networkUpdateType);
if (Thing.IsNetworkUpdateRequired(256u, networkUpdateType))
{
float num = reader.ReadFloatHalf();
float num2 = reader.ReadFloatHalf();
if (RotatableBehaviour != null)
{
RotatableBehaviour.TargetVertical = num;
RotatableBehaviour.TargetHorizontal = num2;
}
}
if (Thing.IsNetworkUpdateRequired(512u, networkUpdateType))
{
VisualizerIntensity = reader.ReadFloatHalf();
}
}
public override void SerializeOnJoin(RocketBinaryWriter writer)
{
base.SerializeOnJoin(writer);
writer.WriteDouble(RotatableBehaviour?.TargetVertical ?? 0.0);
writer.WriteDouble(RotatableBehaviour?.TargetHorizontal ?? 0.0);
}
public override void DeserializeOnJoin(RocketBinaryReader reader)
{
base.DeserializeOnJoin(reader);
double targetVertical = reader.ReadDouble();
double targetHorizontal = reader.ReadDouble();
if (RotatableBehaviour != null)
{
RotatableBehaviour.TargetVertical = targetVertical;
RotatableBehaviour.TargetHorizontal = targetHorizontal;
}
}
Two flags:
256(0x100): per-tick delta carrying half-precisionTargetVertical/TargetHorizontal(4 bytes total). Triggered on the host-side setters ofRotatableBehaviour.TargetHorizontal/TargetVertical.512(0x200): per-tick delta carrying half-precisionVisualizerIntensity(2 bytes).
Late-join (SerializeOnJoin / DeserializeOnJoin) ships full-precision double values for both targets so a joining client picks up an exact slew destination.
Save data¶
public class WirelessPowerSaveData : StructureSaveData
{
[XmlElement] public double Horizontal;
[XmlElement] public double Vertical;
[XmlElement] public double TargetHorizontal;
[XmlElement] public double TargetVertical;
}
PowerTransmitterSaveData : WirelessPowerSaveData adds OutputNetworkReferenceId. PowerReceiverSaveData : WirelessPowerSaveData adds nothing. Both H/V live values and slew targets persist across save/load. Re-verified verbatim at 0.2.6403.27689 (WirelessPowerSaveData decompile lines 426765-426778, PowerTransmitterSaveData 408264-408268, PowerReceiverSaveData 408062-408064).
The four rotation doubles (plus the TX's network reference id) are ALL the wireless-power state that saves. In particular the subclasses' private _powerProvided debt accumulators (PowerTransmitter 408287, PowerReceiver 408071) have no save field and no join-stream field; they restart at 0 on save load and client join. See Device, "Two per-device draw-state fields", for the full non-serialization census.
OnRegistered: H/V reset to upright¶
Source: PowerTransmitter.cs:308-327, PowerReceiver.cs:229-236.
// PowerTransmitter
public override void OnRegistered(Cell cell)
{
if (GameManager.GameState != GameState.Loading && GameManager.RunSimulation)
{
OutputNetwork = WirelessNetwork.GetWirelessNetwork(this);
OutputNetwork.AddDevice(this);
}
base.OnRegistered(cell);
if (GameManager.GameState == GameState.Running)
{
base.Horizontal = 0.0;
base.Vertical = 1.0;
base.RotatableBehaviour.TargetHorizontal = base.Horizontal;
base.RotatableBehaviour.TargetVertical = base.Vertical;
}
if (GameManager.GameState != GameState.Loading)
{
TryContactReceiver();
}
}
When a fresh dish is placed (GameState.Running), H is reset to 0 and V is reset to 1 (zenith). At save-load (GameState.Loading) this branch is skipped so the saved H/V values persist.
V=1 corresponds to "the dish points along its dish-local +Y axis" (zenith of the dish-local frame). For a floor-mounted dish that aligns with world up. For a ceiling-mounted dish, V=1 would point world-down. Mod authors enabling non-floor placement should consider either (a) leaving V=1 alone (the dish points away from the mount surface, intuitive) or (b) re-deriving the post-place default H/V from the prefab's mount surface.
Verification history¶
- 2026-07-07: re-confirmed
WirelessNetwork : CableNetwork(272453) and its membership shape (the carrier network's power members are exactly the two dish halves, both registered segmenters) against the 0.2.6403.27689 decompile. Occasion: the PowerGridPlus partial-power sentinel's carrier-network exclusion (bridge-only membership means a sub-1 vanilla ratio on a carrier deprives no device; under the billing handshake the carrier's demand mirror is the receiver's unclamped drain while its supply mirror is the delivery-gate-clamped advertise, so ratio below 1 is its normal conducting state). No content change. - 2026-07-06: re-verified the "Save data" section against the 0.2.6403.27689 decompile (
WirelessPowerSaveData426765-426778 unchanged;PowerTransmitterSaveData408264-408268 still adds onlyOutputNetworkReferenceId;PowerReceiverSaveData408062-408064 still empty) and restamped it. Added the explicit scoping note that the rotation doubles are the only wireless-power state that saves and that the subclasses'_powerProvidedledgers are runtime-only (no save field, no join field; whole-decompile census on Device). Additive; the section's existing claims were confirmed, not changed. - 2026-07-02: conflict resolution on the subclass list (game version 0.2.6403.27689). Contradicted claim: the intro stated the servo machinery is "used by
PowerTransmitter,PowerReceiver, andPowerTransmitterOmni". Fresh validator verdict (binding): onlyPowerTransmitterandPowerReceiverderive fromWirelessPower;PowerTransmitterOmni : Electrical(decompile line 408582) sits on a sibling branch underDeviceand carries none of the servo members.WirelessPower : ElectricalInputOutput, IRotatableconfirmed at decompile line 426779. Resulting change: removedPowerTransmitterOmnifrom the intro's subclass list and pointed at the new PowerTransmitterOmni page. Section bodies below (servo math, clamping, sync, save data, OnRegistered) were not re-read this pass and keep their 0.2.6228.27061 stamps. - 2026-04-25: page created during a deep decompile of the placement-orientation system. Verbatim source extracts from
WirelessPower.cs,PowerTransmitter.cs,PowerReceiver.cs,WirelessPowerSaveData.cs. No conflicts withPowerTransmitter.md; this page documents the BASE class.
Open questions¶
None.