PowerTransmitter¶
Vanilla game class at Assets.Scripts.Objects.Electrical.PowerTransmitter. Transmitter half of the dish-to-dish wireless power pair. Paired at runtime with a PowerReceiver via raycast and drives a private _powerProvided debt accumulator across two cable-network ticks.
Class hierarchy¶
Source: F0035; re-verified against the 0.2.6403.27689 decompile (class declarations at lines 394786 Electrical, 394813 ElectricalInputOutput, 426779 WirelessPower, 408269 PowerTransmitter, 408065 PowerReceiver, 408582 PowerTransmitterOmni).
MonoBehaviour
Thing
...
Device
Electrical ← sibling branch of ElectricalInputOutput; carries only SmartRotate
members (line 394786)
PowerTransmitterOmni ← single-network omnidirectional charger (line 408582); NOT a
WirelessPower subclass, no Horizontal/Vertical/RayTransform/
DishTransform servo machinery; see PowerTransmitterOmni.md
ElectricalInputOutput ← public CableNetwork InputNetwork; public CableNetwork OutputNetwork; (394813)
WirelessPower ← public Transform RayTransform; AxleTransform; DishTransform;
double Horizontal { get; set; }; double Vertical { get; set; };
protected PowerTransmitterVisualiser PowerTransmitterVisualiser; (426779)
PowerTransmitter ← public PowerReceiver LinkedReceiver; private PowerReceiver _linkedReceiver;
private float _linkedReceiverDistance; private float _powerProvided;
public static float MaxPowerTransmission = 5000f;
private static readonly float _MaxTransmitterDistance = 500f;
public AnimationCurve PowerLossOverDistance; (408269)
PowerReceiver ← public PowerTransmitter LinkedPowerTransmitter;
private PowerTransmitter _linkedPowerTransmitter;
public Transform DishTarget; private float _powerProvided (408065)
PowerTransmitterVisualiser lives in global namespace (no Assets.Scripts... prefix). Thing.EMISSION_COLOR = Shader.PropertyToID("_EmissionColor").
Constants¶
Source: F0038. Values read from vanilla fields on PowerTransmitter and related electrical classes. Re-verified against the 0.2.6403.27689 decompile: MaxPowerTransmission = 5000f (line 408275), _MaxTransmitterDistance = 500f (408273), PowerLossOverDistance keyframes (0,0)/(1,1)/(2,1) (408277), APC BatteryChargeRate = 1000f (390586), BatteryCell.PowerMaximum = 36000f (340493), Cable.MaxVoltage = 5000f C# default (392341).
| Constant | Class | Type | Value | Notes |
|---|---|---|---|---|
MaxPowerTransmission |
PowerTransmitter |
public static float |
5000f |
Mutable at runtime |
_MaxTransmitterDistance |
PowerTransmitter |
private static readonly float |
500f |
Only used as loss-curve denominator |
PowerLossOverDistance |
PowerTransmitter |
AnimationCurve |
(0,0)→(1,1)→(2,1) linear |
loss = distance × 10 W capped at 5000 W |
BatteryChargeRate |
AreaPowerControl |
[SerializeField] float |
1000f |
APC's max battery-slot charge rate |
PowerMaximum |
BatteryCell |
public float |
36000f |
Joule capacity, NOT a rate cap |
MaxVoltage |
Cable |
float |
5000f |
Rupture threshold in watts (despite the name) |
Method semantics¶
Source: F0036. Primary source for method bodies and the TryContactReceiver raycast condition. All bodies re-verified verbatim against the 0.2.6403.27689 decompile: GetGeneratedPower 408472-408484, UsePower 408424-408430, GetUsedPower 408446-408469, ReceivePower 408432-408444, TryContactReceiver 408492-408508 (raycast + dual anti-parallel 7 deg checks + GameManager.RunSimulation && GameState == Running host gate).
PowerTransmitter.GetGeneratedPower:
public override float GetGeneratedPower(CableNetwork cableNetwork)
{
if (OutputNetwork == null || Error == 1 || cableNetwork != OutputNetwork) return 0f;
float num = PowerLossOverDistance.Evaluate(
Mathf.Clamp01(_linkedReceiverDistance / _MaxTransmitterDistance)) * MaxPowerTransmission;
if (!OnOff || InputNetwork == null) return 0f;
return Mathf.Min(MaxPowerTransmission, InputNetwork.PotentialLoad) - num;
}
PowerTransmitter.UsePower:
public override void UsePower(CableNetwork cableNetwork, float powerUsed)
{
if (Error != 1 && OnOff && cableNetwork == WirelessOutputNetwork)
_powerProvided += powerUsed;
}
PowerTransmitter.GetUsedPower:
public override float GetUsedPower([NotNull] CableNetwork cableNetwork)
{
if (InputNetwork == null) base.VisualizerIntensity = 0f;
if (InputNetwork == null || cableNetwork != InputNetwork) return 0f;
if (Error == 1) {
base.VisualizerIntensity = 0f;
if (!OnOff) return 0f;
return UsedPower;
}
if (!OnOff) return 0f;
return Mathf.Min(MaxPowerTransmission, _powerProvided);
}
PowerTransmitter.ReceivePower:
public override void ReceivePower(CableNetwork cableNetwork, float powerAdded)
{
if (InputNetwork == null || cableNetwork == InputNetwork) {
if (!OnOff || InputNetwork == null) { base.VisualizerIntensity = 0f; return; }
base.VisualizerIntensity = RocketMath.MapToScale(0f, MaxPowerTransmission, 0f, 1f, powerAdded);
_powerProvided -= powerAdded;
}
}
TryContactReceiver core condition (from PowerTransmitter.cs):
Physics.Raycast(RayTransform.position, RayTransform.TransformDirection(Vector3.forward), out hit, float.PositiveInfinity)
&& Thing._colliderLookup.TryGetValue(hit.collider, out value) && value is PowerReceiver rx
&& hit.transform == rx.DishTarget
&& RocketMath.Approximately(Vector3.Angle(RayTransform.forward, rx.RayTransform.forward), 180f, 7f)
&& RocketMath.Approximately(Vector3.Angle(RayTransform.right, rx.RayTransform.right), 180f, 7f)
Link requires: raycast hit lands on the receiver's DishTarget collider AND both dishes' forward axes anti-parallel AND both right axes anti-parallel, within 7° on each.
Unlinked-TX retry: PowerTransmitter.OnPowerTick (decompile 408396-408413) increments _tryTargetCount while LinkedReceiver == null && OnOff; once the count passes ReTargetWait = 10 (private static readonly int, line 408285) it resets the counter and retries the link. New at 0.2.6403.27689: the retry goes through WaitTryContactReceiverMainThread() (408486-408490), an async UniTask that does await UniTask.SwitchToMainThread() before calling TryContactReceiver(), so the Physics.Raycast never runs on the power-tick ThreadPool worker.
_powerProvided is runtime-only: not saved, not synced¶
The debt accumulator (declaration 408287) has no persistence path at 0.2.6403.27689. PowerTransmitterSaveData : WirelessPowerSaveData (408264-408268) adds only long OutputNetworkReferenceId to the base record's four dish-rotation doubles (426765-426778); the TX join stream (SerializeOnJoin 408352) carries only OutputNetwork.ReferenceId and the linked receiver id, and the flag-driven BuildUpdate / ProcessUpdate carry only the receiver link. The whole-decompile census confirms the field is touched exclusively by UsePower (408428), ReceivePower (408442), and GetUsedPower (408469). It restarts at 0 on save load and on client join, so a loaded or late-joining dish always begins with an empty ledger; the one-tick input-side draw lag rebuilds from zero. Cross-class census and consequences on Device, "Two per-device draw-state fields".
Transforms and geometry¶
Sources: F0037, F0053.
Both StructurePowerTransmitter and StructurePowerTransmitterReceiver share the same rig:
root GameObject pos (0, 1, 0) rot identity
inner StructureXxx pos (0, 0, 0) rot identity
Rotation (= AxleTransform) pos (0, 0.27, 0) rot identity → runtime Euler(0, H·360°, 0)
Arm pos (0, 0.19, 0) rot identity
Head (= DishTransform) pos (0, 0.65, 0) rot identity → runtime Euler(Lerp(90°, -90°, V), 0, 0)
Line (= RayTransform) pos (0, 0.34, 0.03) rot identity ← ray origin, moves with H/V
DishTarget (RX only) pos (0, 0.33, 0.54) rot identity ← link raycast target, moves with H/V
Transmitter (TX, dish mesh) pos (0, 0.34, 0.76) rot identity
RayTransform, DishTarget, and Transmitter are all children of Head and their world positions change as the dish rotates. The only positions invariant under H/V rotation are the root GameObject's transform.position and the Rotation / Arm nodes up to Head.
Dish reachable space: full sphere via V ∈ [0, 1] (nadir → horizon → zenith) and H ∈ [0, 1) (full azimuth).
- V=0 (Euler X = +90°):
RayTransform.forward=(0, -1, 0)in root-local = straight down - V=0.5 (Euler X = 0°):
(0, 0, 1)= horizon forward - V=1 (Euler X = -90°):
(0, 1, 0)= straight up (the vanilla rest state)
Inverse formula (world direction → (H, V)):
d_local = dish.transform.InverseTransformDirection((targetPos - dishPos).normalized)
V = 0.5 + asin(d_local.y) / π
H = (atan2(d_local.x, d_local.z) / (2π) + 1) mod 1
|d_local.y| ≈ 1), H is undefined. Keep the current value.
Placement: floor-only in vanilla (the dish prefab's serialized AllowedRotations value gates the cursor; see ./AllowedRotations.md), four cardinal rotations, root.up = world.up always. The aim math is frame-agnostic via InverseTransformDirection, so a non-floor placement (achieved by mutating AllowedRotations on the prefab or the construction cursor) continues to compute correct (H, V) without changes - the dish-local sphere of reachable directions rotates with the parent.
Caveats for non-floor placement:
- The
OnRegisteredreset to(H=0, V=1)(zenith of the dish-local frame) is a fresh-place default. For a ceiling-mounted dish, V=1 points world-down, which is the side facing away from the mount and therefore the natural "default" orientation. For a wall-mounted dish, V=1 points horizontally outward, also natural. - The TX-RX link raycast checks
Vector3.Angle(RayTransform.forward, rx.RayTransform.forward) ~ 180ANDVector3.Angle(RayTransform.right, rx.RayTransform.right) ~ 180within 7 deg. Both vectors are dish-local; the check works regardless of root orientation as long as the two dishes face each other alongRayTransform.forward. - The vertical-input clamps (
SetLogicValueand the interaction increment/decrement handlers) keep V in [0..1] = [+90 deg, -90 deg] dish-local pitch. The reachable hemisphere is always the dish-local upper hemisphere; "world directions" reachable depends entirely on root orientation.
Head-child drift trap (F0053)¶
Line (RayTransform), DishTarget, and Transmitter are all children of Head, which rotates via DishTransform.localRotation = Euler(Lerp(90°, -90°, V), 0, 0). Their world positions therefore change as the dish rotates. Any aim algorithm that treats them as fixed produces:
- Self-referential error when used as RAY ORIGIN: aim computed from current RayTransform position goes stale once the dish rotates and the RayTransform moves. Observed as ~0.3° drift that prevented link-raycast hits.
- Pose-lock-in when used as RAY TARGET: aiming at the other dish's RayTransform / DishTarget locks onto that dish's CURRENT pose. When the target later rotates to a correct aim, your aim is pointing at empty space.
Rule: use dish.transform.position (the placement-anchored root) as both origin and target for aim computation. Invariant under all dish rotation.
Prefab extraction¶
Source: F0039. Extracted via UnityPy from rocketstation_Data/sharedassets0.assets:
PowerTransmitterVisualiser MonoBehaviour serialized values:
- InnerColor = (1, 1, 1, 1) white
- EmissionColor = (0, 0.4915, 10, 10) HDR cyan-blue ([ColorUsage(false, true)]); alpha animated 0..1 by Activate/Deactivate.
LineRenderer on child GO Line under .../Rotation/Arm/Head/Line:
- widthMultiplier = 0.1
- alignment = local
- colorGradient baked to semi-transparent red; never touched at runtime
Material Custom_PowerTransmission on the LineRenderer:
- Shader: name stripped from build; Shader.Find("Custom_PowerTransmission") will NOT find it.
- _EmissionColor = (5.992, 0.188, 0, 1) HDR orange-red baked. Overwritten at runtime by DOColor with the MonoBehaviour's cyan-blue EmissionColor. The baked orange-red is a vestige.
Net result: the visible in-game beam color comes from the MonoBehaviour field, not the baked material.
Pitfalls¶
Sources: F0052, F0053.
DishForward is a lie for aim (F0052)¶
WirelessPower.Vertical and Horizontal setters both update DishForward = DishTransform.up, which reads naturally when inferring aim direction. It is wrong for the raycast. The base-game link raycast uses RayTransform.forward, NOT DishTransform.up. These two vectors are ORTHOGONAL in the local Head frame (forward is local +Z, up is local +Y). Using DishForward restricts aim to the upper hemisphere only, contradicting observed in-game behavior.
Rule: the dish's true aim direction is the raycast's direction vector. Check the actual raycast call site before deriving aim math.
Inherited Logic methods¶
CanLogicRead / GetLogicValue / SetLogicValue / CanLogicWrite are declared on WirelessPower, NOT on PowerTransmitter. [HarmonyPatch(typeof(PowerTransmitter), ...)] fails via AccessTools.DeclaredMethod. Target the declaring class (WirelessPower) instead. See ../Patterns/HarmonyInheritedMethods.md.
PowerTick threading¶
The VisualizerIntensity setter on WirelessPower fires from a ThreadPool worker during PowerTick. Any Unity API from that call path hard-crashes the player. Route work to the main thread via MainThreadDispatcher. See ../GameSystems/PowerTickThreading.md.
Auto-aim accuracy under non-floor mounts¶
A mod that auto-aims the dish at a target by setting Horizontal / Vertical from the world direction between two transforms inherits the offset between those transforms and the actual ray endpoints. Specifically, the link raycast in TryContactReceiver originates from RayTransform.position (a child of the rotating Head) and tests against rx.DishTarget (also a child of the receiver's rotating Head). Pivot-to-pivot aim (TX root -> RX root) is convenient because both pivots are rotation-invariant, but it leaves a residual angular error equal to the perpendicular component of the root-to-RayTransform offset against the aim direction.
For floor-mounted dishes the offset is mostly along world up, which is mostly perpendicular to the (mostly horizontal) aim direction; at link distances of several tens of meters that perpendicular component is small in angular terms (a fraction of a degree). For non-floor mounts (wall, ceiling, sideways) the offset maps to non-vertical world directions and the perpendicular component can dominate: empirically, a wall-mounted TX with offset magnitude 1.937 m aimed at an RX 42 m away showed a 2.51 deg residual aim error after one slew (1.84 m off-axis at the receiver), enough to make a narrow Physics.Raycast miss the small DishTarget collider entirely.
The fix is a fixed-point iteration that converges to a pose where the predicted RayTransform.position and the chosen (Horizontal, Vertical) are mutually consistent:
origin = current RayTransform.position
for i in 1..N:
direction = (DishTarget - origin).normalized
(H, V) = solve_dish_local(direction)
origin = predict(H, V) # apply candidate H / V to AxleTransform /
# DishTransform localRotation, read
# RayTransform.position, restore
if step delta < 1 cm: break
set TargetHorizontal = H, TargetVertical = V
The contraction factor is approximately k = |root-to-RayTransform offset| / link_distance. For our 42 m wall + ceiling case k = 1.94 / 42 = 0.046, which converges to mm precision in 2-3 iterations. The contraction degrades at short range: at D = |offset| (~ 2 m) the iteration does not converge meaningfully, but a 1.9 m dish-to-dish placement is pathological. PowerTransmitterPlus caps at 10 iterations as a safety net and breaks early at the 1 cm tolerance, with the cache check on the public auto-aim entry preventing redundant solves on rewrites of the same target.
TX/RX wireless link topology (network bridging)¶
PowerTransmitter (decompile line 408269) and PowerReceiver (line 408065) inherit from WirelessPower. The wireless pair is bridged through a shared WirelessNetwork instance:
WirelessPower : ElectricalInputOutput, IRotatable(decompile line 426779). BothPowerTransmitterandPowerReceiverinherit twoCableNetwork-typed slots fromElectricalInputOutput:InputNetworkandOutputNetwork. Directional usage in vanilla:- On a
PowerTransmitter:InputNetworkis the physical cable network the dish is wired to (the power source);OutputNetworkis theWirelessNetwork(the dish's broadcast). - On a
PowerReceiver:InputNetworkis theWirelessNetwork(the dish's reception, assigned fromLinkedPowerTransmitter.OutputNetwork);OutputNetworkis the physical cable network the dish is wired to (the power destination). PowerTransmitter.OutputNetworkis aWirelessNetwork(WirelessOutputNetwork => OutputNetwork as WirelessNetwork). The TX'sLinkedReceiversetter (line 408293) callsWirelessOutputNetwork.RemoveDevice(_linkedReceiver)(408305) thenWirelessOutputNetwork.AddDevice(value)-- the RX is registered as a device on the TX's wireless network.PowerReceiver.LinkedPowerTransmittersetter (line 408081) executesInputNetwork = value.OutputNetwork;-- the RX'sInputNetworkis assigned the TX'sOutputNetworkreference, sharing the same WirelessNetwork instance.- The fields backing the link are
PowerTransmitter._linkedReceiver(privatePowerReceiver) andPowerReceiver._linkedPowerTransmitter(privatePowerTransmitter). - For wire-side serialization,
PowerTransmitter.SerializeOnJoin(line 408352) writesOutputNetwork.ReferenceIdthenLinkedReceiver?.ReferenceId ?? 0;DeserializeOnJoin(line 408359) reads them in the same order and rehydrates the wireless network reference viaReferencable.Find<WirelessNetwork>. The runtime link is rebuilt via_savedRecieverId(note vanilla typoReciever). PowerTransmitter.OnDestroy(line 408387) andPowerReceiver.OnDestroy(line 408108) both null-out the partner side.
WirelessNetwork internals¶
WirelessNetwork : CableNetwork (decompile lines 272453-272550) is the network type both dishes bridge through. Facts a mod touching the wireless side needs:
GetWirelessNetwork(WirelessPower parentTransmitter)(272472-272484) first scansCableNetwork.AllCableNetworksfor an existingWirelessNetworkwhoseDeviceListcontains the caller; if none is found it creates one ONLY whenGameManager.RunSimulationis true (host / single-player) and returnsnullon a client. Wireless network creation is host-only.RefreshPowerAndDataDeviceLists()override (272486-272492) clears both typed lists, then copies the WHOLEDeviceListinto_powerDeviceListand leaves_dataDeviceListpermanently empty. Every device on a wireless network is a power device; no data traversal ever crosses it.OnPowerTick()(272506-272512) runs the baseCableNetwork.OnPowerTickonly whenIsNetworkValid()is true, andIsNetworkValid()(272514-272517) isDeviceList.Count > 0(a plainCableNetworkkeys validity onCableList.Countinstead; a wireless network has no cables).
Cable-side networks are separate from the wireless network: the TX's cable port is on its own physical CableNetwork (the cable network ID seen as OutputNetworkReferenceId in PowerTransmitterSaveData is the cable-side network). Same for the RX. So a TX-RX pair touches four networks total: two wireless (one shared instance bridging both sides) and two physical cable networks (one wired to the TX side, one wired to the RX side). To bridge cable-side logic across a TX-RX link you traverse: caller cable network -> caller's TX or RX -> partner via _linkedReceiver / _linkedPowerTransmitter -> partner's cable network.
Link establishment is host-authoritative; only the TX side replicates¶
PowerTransmitter.TryContactReceiver (the raycast that sets LinkedReceiver and mirrors LinkedReceiver.LinkedPowerTransmitter = this) runs only on the host: its body is wrapped in if (GameManager.RunSimulation && GameManager.GameState == GameState.Running) (decompile 408492-408508). Clients never establish the link locally; they receive it by replication. The two halves are asymmetric:
- TX side replicates. The
PowerTransmitter.LinkedReceiversetter (line 408293) setsNetworkUpdateFlags |= NetworkUpdateType.Thing.WirelessPower.Receiver(line 408318) on the server.PowerTransmitter.BuildUpdate(line 408334) writesLinkedReceiver?.ReferenceId ?? 0under that flag;ProcessUpdate(line 408343) reads it back asLinkedReceiver = Thing.Find<PowerReceiver>(reader.ReadInt64()).SerializeOnJoin(408352) carries it too. So a client'stx.LinkedReceiverstays current. - RX back-reference does NOT replicate.
PowerReceiver.LinkedPowerTransmitter's setter (line 408081) sets noNetworkUpdateFlagsand is assigned only host-side, insideTryContactReceiver. There is noBuildUpdate/ProcessUpdatefield carrying it. So on a clientrx.LinkedPowerTransmitterstays null even whiletx.LinkedReceiverpoints at that RX.
Consequence for cross-link cable-side logic computed per-peer: a traversal that starts from the TX side (tx.LinkedReceiver) resolves on a client, but one that starts from the RX side (rx.LinkedPowerTransmitter) does not. A logic-passthrough merge is therefore symmetric on the host (both sides set in TryContactReceiver) but one-directional on a client (TX-cable side sees across the link; RX-cable side does not), unless the mod mirrors the back-reference on the client itself.
Mod extensions¶
- PowerTransmitterPlus extends this class; see ../../Mods/PowerTransmitterPlus/RESEARCH.md for distance-cost patches, AutoAim, and related extensions.
Verification history¶
- 2026-07-06: added the "
_powerProvidedis runtime-only" subsection to Method semantics (game version 0.2.6403.27689). Evidence:PowerTransmitterSaveDatabody (408264-408268, onlyOutputNetworkReferenceId),WirelessPowerSaveDatabody (426765-426778, only H/V + slew targets), and the whole-decompile_powerProvidedcensus (TX sites: declaration 408287, 408428 / 408442 / 408469 only; no serialization member). Additive; the existing serialization notes on this page (SerializeOnJoin carrying network + receiver ids) already implied it, no conflict, no fresh validator. - 2026-07-02: conflict resolution on the class-hierarchy diagram (game version 0.2.6403.27689). Contradicted claim: the diagram placed
PowerTransmitterOmniunder theWirelessPowersubtree (as a third sibling ofPowerTransmitter/PowerReceiver), implying it inherits the servo machinery. Fresh validator verdict (binding):public class PowerTransmitterOmni : Electricalat decompile line 408582;Electrical(394786) andElectricalInputOutput(394813) are SIBLINGS underDevice; the Omni does NOT inheritWirelessPowerand has noHorizontal/Vertical/RayTransform/DishTransformmembers. Resulting change: movedPowerTransmitterOmniout of theWirelessPowersubtree into a siblingElectricalbranch with a pointer to the new PowerTransmitterOmni page. - 2026-07-02: re-verification pass against the 0.2.6403.27689 decompile after the game update from 0.2.6228.27061. Confirmed unchanged:
PowerTransmitter : WirelessPower(408269),MaxPowerTransmission = 5000f(408275),_MaxTransmitterDistance = 500f(408273),GetGeneratedPower=Min(5000, InputNetwork.PotentialLoad) - PowerLossOverDistance.Evaluate(clamp01(dist/500)) * 5000(408472-408484),GetUsedPower=Min(5000, _powerProvided)(408446-408469),UsePower/ReceivePowerledger (408424-408444),TryContactReceiverraycast + dual anti-parallel 7 deg checks + host gate (408492-408508),ReTargetWait = 10(408285). New at this version: the unlinked-TX retry inOnPowerTick(408396-408413) marshals to the main thread viaWaitTryContactReceiverMainThread(408486-408490) before the raycast; documented in Method semantics. Added the "WirelessNetwork internals" subsection (WirelessNetwork : CableNetwork272453-272550; host-onlyGetWirelessNetworkcreation 272472-272484;RefreshPowerAndDataDeviceListsoverride makes the power list the wholeDeviceListand the data list permanently empty 272486-272492;OnPowerTickgated onIsNetworkValid()=DeviceList.Count > 0272506-272517). Updated the TX/RX topology and host-authoritative sections' line refs from the 0.2.6228 decompile to the 0.2.6403 one (setter 408293, flag 408318, BuildUpdate 408334, ProcessUpdate 408343, SerializeOnJoin 408352, OnDestroy 408387 / 408108). - 2026-05-28: added "Link establishment is host-authoritative; only the TX side replicates". Confirmed
TryContactReceiveris gated onGameManager.RunSimulation(host-only) in both vanilla and PowerTransmitterPlus (PTP prefix-replaces the body but keeps the gate); thePowerTransmitter.LinkedReceiversetter (387089) flagsNetworkUpdateType.Thing.WirelessPower.Receiver(387114) andBuildUpdate/ProcessUpdate(387130-387146) carry it by ReferenceId;PowerReceiver.LinkedPowerTransmittersetter (386871) sets no flag and is assigned only host-side, so the back-reference does not replicate. Driving work: PowerGridPlus dish-link passthrough refresh, where this asymmetry makes the client merge one-directional. Additive; no prior claim contradicted, no fresh validator. - 2026-05-17: added "TX/RX wireless link topology (network bridging)" section, sourced from decompile lines 387065-387162 (PowerTransmitter), 386861-386911 (PowerReceiver), and 405441 (
WirelessPower : ElectricalInputOutput). Documents the sharedWirelessNetworkbetween linked TX and RX (RX.InputNetwork = TX.OutputNetworkassignment in theLinkedPowerTransmittersetter), the private fields backing the link (_linkedReceiver,_linkedPowerTransmitter,_savedRecieverIdvanilla typo), the wire format used bySerializeOnJoin/DeserializeOnJoin, the topology fact that a TX/RX pair touches four networks total (one shared wireless + two separate cable networks on each end), and the directionalInputNetwork/OutputNetworkassignment on each side (TX:InputNetwork=cable,OutputNetwork=wireless; RX:InputNetwork=wireless,OutputNetwork=cable). Driving question: how would a logic-passthrough mod bridge cable-side logic across a TX-RX wireless link. Answer: in aRefreshPowerAndDataDeviceListspostfix, when iterating a Cable network's DeviceList and encountering a TX, the "other side" cable network istx.LinkedReceiver?.OutputNetwork; encountering an RX, the other side isrx.LinkedPowerTransmitter?.InputNetwork. - 2026-04-20: page created from the Research migration; verbatim content lifted from F0035, F0036, F0037, F0038, F0039, F0042, F0044, F0050, F0052, F0053, F0219z, F0300, F0301. No conflicts.
- 2026-04-20: removed PowerTransmitterPlus-specific subsections per Phase 6 Pass B editorial decision (GameClasses pages are strictly vanilla). Added mod-extensions pointer.
- 2026-04-25: refined the "Placement" line in "Transforms and geometry" to clarify the floor-only restriction comes from the prefab's serialized
AllowedRotationsvalue (the gate is inInventoryManager.UpdatePlacement, not hardcoded toPowerTransmitter), and added caveats coveringOnRegisteredH/V reset, TX-RX link raycast frame-invariance, and vertical clamp behavior under non-upright placement. Source: deep decompile pass producingResearch/GameClasses/AllowedRotations.md,Research/GameSystems/PlacementOrientation.md,Research/GameClasses/WirelessPower.md. No existing factual claim was contradicted; the new wording disambiguates the previously implicit assumption. - 2026-04-26: added "Auto-aim accuracy under non-floor mounts" section after empirical testing of wall TX + ceiling RX showed pivot-to-pivot aim leaves a 2.51 deg residual at 42 m (1.84 m off-axis at the receiver), enough to miss the narrow
Physics.RaycastagainstDishTarget. Documents the contraction-factor analysis (k ~ |root-to-RayTransform offset| / link_distance) and the iterativeRayTransform->DishTargetsolve template that PowerTransmitterPlus now uses. Source: empirical InspectorPlus snapshots ofRayTransform.position/DishTarget.position/ forwards / rights vectors during the wall+ceiling test, plus direct reads ofPowerTransmitter.TryContactReceiverandWirelessPower.Vertical/Horizontalsetters. Additive: previously the page documented only the floor-only frame-invariance argument; this section covers what changes when one or both endpoints are mounted on non-floor surfaces. - 2026-04-26: noted in mod-extensions context that the vanilla
TryContactReceiverright-axis antiparallel check (condition 5) is geometrically unsatisfiable for non-floor pairs because H/V control aim direction, not roll around the forward axis. Empirically on wall TX + ceiling RX: forwards angle 178.92 deg (within 7 deg of 180), rights angle 56.01 deg (124 deg outside tolerance). For floor-only pairs the rights are GEOMETRICALLY FORCED antiparallel once forwards are antiparallel (both axles spin around world up), making the check a redundant tautology. Documented in the mod'sLinkPatch.csrationale; no change to the central page's vanilla description, since vanilla is unchanged.
Open questions¶
None at creation.