TegSystem.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401
  1. using Content.Server.Atmos;
  2. using Content.Server.Atmos.EntitySystems;
  3. using Content.Server.Atmos.Piping.Components;
  4. using Content.Server.Audio;
  5. using Content.Server.DeviceNetwork;
  6. using Content.Server.DeviceNetwork.Systems;
  7. using Content.Server.NodeContainer;
  8. using Content.Server.NodeContainer.Nodes;
  9. using Content.Server.Power.Components;
  10. using Content.Shared.Atmos;
  11. using Content.Shared.DeviceNetwork;
  12. using Content.Shared.Examine;
  13. using Content.Shared.Power;
  14. using Content.Shared.Power.EntitySystems;
  15. using Content.Shared.Power.Generation.Teg;
  16. using Content.Shared.Rounding;
  17. using Robust.Server.GameObjects;
  18. namespace Content.Server.Power.Generation.Teg;
  19. /// <summary>
  20. /// Handles processing logic for the thermo-electric generator (TEG).
  21. /// </summary>
  22. /// <remarks>
  23. /// <para>
  24. /// The TEG generates power by exchanging heat between gases flowing through its two sides.
  25. /// The gas flows through a "circulator" entity on each side, which have both an inlet and an outlet port.
  26. /// </para>
  27. /// <remarks>
  28. /// Connecting the TEG core to its circulators is implemented via a node group. See <see cref="TegNodeGroup"/>.
  29. /// </remarks>
  30. /// <para>
  31. /// The TEG center does HV power output, and must also be connected to an LV wire for the TEG to function.
  32. /// </para>
  33. /// <para>
  34. /// Unlike in SS13, the TEG actually adjusts gas heat exchange to match the energy demand of the power network.
  35. /// To achieve this, the TEG implements its own ramping logic instead of using the built-in Pow3r ramping.
  36. /// The TEG actually has a maximum output of +n% more than was really generated,
  37. /// which allows Pow3r to draw more power to "signal" that there is more network load.
  38. /// The ramping is also exponential instead of linear like in normal Pow3r.
  39. /// This system does mean a fully-loaded TEG creates +n% power out of thin air, but this is considered acceptable.
  40. /// </para>
  41. /// </remarks>
  42. /// <seealso cref="TegGeneratorComponent"/>
  43. /// <seealso cref="TegCirculatorComponent"/>
  44. /// <seealso cref="TegNodeGroup"/>
  45. /// <seealso cref="TegSensorData"/>
  46. public sealed class TegSystem : EntitySystem
  47. {
  48. /// <summary>
  49. /// Node name for the TEG part connection nodes (<see cref="TegNodeGroup"/>).
  50. /// </summary>
  51. private const string NodeNameTeg = "teg";
  52. /// <summary>
  53. /// Node name for the inlet pipe of a circulator.
  54. /// </summary>
  55. private const string NodeNameInlet = "inlet";
  56. /// <summary>
  57. /// Node name for the outlet pipe of a circulator.
  58. /// </summary>
  59. private const string NodeNameOutlet = "outlet";
  60. /// <summary>
  61. /// Device network command to have the TEG output a <see cref="TegSensorData"/> object for its last statistics.
  62. /// </summary>
  63. public const string DeviceNetworkCommandSyncData = "teg_sync_data";
  64. [Dependency] private readonly AmbientSoundSystem _ambientSound = default!;
  65. [Dependency] private readonly AppearanceSystem _appearance = default!;
  66. [Dependency] private readonly AtmosphereSystem _atmosphere = default!;
  67. [Dependency] private readonly DeviceNetworkSystem _deviceNetwork = default!;
  68. [Dependency] private readonly PointLightSystem _pointLight = default!;
  69. [Dependency] private readonly SharedPowerReceiverSystem _receiver = default!;
  70. private EntityQuery<NodeContainerComponent> _nodeContainerQuery;
  71. public override void Initialize()
  72. {
  73. base.Initialize();
  74. SubscribeLocalEvent<TegGeneratorComponent, AtmosDeviceUpdateEvent>(GeneratorUpdate);
  75. SubscribeLocalEvent<TegGeneratorComponent, PowerChangedEvent>(GeneratorPowerChange);
  76. SubscribeLocalEvent<TegGeneratorComponent, DeviceNetworkPacketEvent>(DeviceNetworkPacketReceived);
  77. SubscribeLocalEvent<TegGeneratorComponent, ExaminedEvent>(GeneratorExamined);
  78. _nodeContainerQuery = GetEntityQuery<NodeContainerComponent>();
  79. }
  80. private void GeneratorExamined(EntityUid uid, TegGeneratorComponent component, ExaminedEvent args)
  81. {
  82. if (GetNodeGroup(uid) is not { IsFullyBuilt: true })
  83. {
  84. args.PushMarkup(Loc.GetString("teg-generator-examine-connection"));
  85. }
  86. else
  87. {
  88. var supplier = Comp<PowerSupplierComponent>(uid);
  89. args.PushMarkup(Loc.GetString("teg-generator-examine-power", ("power", supplier.CurrentSupply)));
  90. }
  91. }
  92. private void GeneratorUpdate(EntityUid uid, TegGeneratorComponent component, ref AtmosDeviceUpdateEvent args)
  93. {
  94. var supplier = Comp<PowerSupplierComponent>(uid);
  95. var powerReceiver = Comp<ApcPowerReceiverComponent>(uid);
  96. if (!powerReceiver.Powered)
  97. {
  98. supplier.MaxSupply = 0;
  99. return;
  100. }
  101. var tegGroup = GetNodeGroup(uid);
  102. if (tegGroup is not { IsFullyBuilt: true })
  103. return;
  104. var circA = tegGroup.CirculatorA!.Owner;
  105. var circB = tegGroup.CirculatorB!.Owner;
  106. var (inletA, outletA) = GetPipes(circA);
  107. var (inletB, outletB) = GetPipes(circB);
  108. var (airA, δpA) = GetCirculatorAirTransfer(inletA.Air, outletA.Air);
  109. var (airB, δpB) = GetCirculatorAirTransfer(inletB.Air, outletB.Air);
  110. var cA = _atmosphere.GetHeatCapacity(airA, true);
  111. var cB = _atmosphere.GetHeatCapacity(airB, true);
  112. // Shift ramp position based on demand and generation from previous tick.
  113. var curRamp = component.RampPosition;
  114. var lastDraw = supplier.CurrentSupply;
  115. curRamp = MathHelper.Clamp(lastDraw, curRamp / component.RampFactor, curRamp * component.RampFactor);
  116. curRamp = MathF.Max(curRamp, component.RampMinimum);
  117. component.RampPosition = curRamp;
  118. var electricalEnergy = 0f;
  119. if (airA.Pressure > 0 && airB.Pressure > 0)
  120. {
  121. var hotA = airA.Temperature > airB.Temperature;
  122. // Calculate thermal and electrical energy transfer between the two sides.
  123. // Assume temperature equalizes, i.e. Ta*cA + Tb*cB = Tf*(cA+cB)
  124. var Tf = (airA.Temperature * cA + airB.Temperature * cB) / (cA + cB);
  125. // The maximum energy we can extract is (Ta - Tf)*cA, which is equal to (Tf - Tb)*cB
  126. var Wmax = MathF.Abs(airA.Temperature - Tf) * cA;
  127. var N = component.ThermalEfficiency;
  128. // Calculate Carnot efficiency
  129. var Thot = hotA ? airA.Temperature : airB.Temperature;
  130. var Tcold = hotA ? airB.Temperature : airA.Temperature;
  131. var Nmax = 1 - Tcold / Thot;
  132. N = MathF.Min(N, Nmax); // clamp by Carnot efficiency
  133. // Reduce efficiency at low temperature differences to encourage burn chambers (instead
  134. // of just feeding the TEG room temperature gas from an infinite gas miner).
  135. var dT = Thot - Tcold;
  136. N *= MathF.Tanh(dT/700); // https://www.wolframalpha.com/input?i=tanh(x/700)+from+0+to+1000
  137. var transfer = Wmax * N;
  138. electricalEnergy = transfer * component.PowerFactor;
  139. var outTransfer = transfer * (1 - component.ThermalEfficiency);
  140. // Adjust thermal energy in transferred gas mixtures.
  141. if (hotA)
  142. {
  143. // A -> B
  144. airA.Temperature -= transfer / cA;
  145. airB.Temperature += outTransfer / cB;
  146. }
  147. else
  148. {
  149. // B -> A
  150. airA.Temperature += outTransfer / cA;
  151. airB.Temperature -= transfer / cB;
  152. }
  153. }
  154. component.LastGeneration = electricalEnergy;
  155. // Turn energy (at atmos tick rate) into wattage.
  156. var power = electricalEnergy / args.dt;
  157. // Add ramp factor. This magics slight power into existence, but allows us to ramp up.
  158. supplier.MaxSupply = power * component.RampFactor;
  159. var circAComp = Comp<TegCirculatorComponent>(circA);
  160. var circBComp = Comp<TegCirculatorComponent>(circB);
  161. circAComp.LastPressureDelta = δpA;
  162. circAComp.LastMolesTransferred = airA.TotalMoles;
  163. circBComp.LastPressureDelta = δpB;
  164. circBComp.LastMolesTransferred = airB.TotalMoles;
  165. _atmosphere.Merge(outletA.Air, airA);
  166. _atmosphere.Merge(outletB.Air, airB);
  167. UpdateAppearance(uid, component, powerReceiver, tegGroup);
  168. }
  169. private void UpdateAppearance(
  170. EntityUid uid,
  171. TegGeneratorComponent component,
  172. ApcPowerReceiverComponent powerReceiver,
  173. TegNodeGroup nodeGroup)
  174. {
  175. int powerLevel;
  176. if (powerReceiver.Powered)
  177. {
  178. powerLevel = ContentHelpers.RoundToLevels(
  179. component.RampPosition - component.RampMinimum,
  180. component.MaxVisualPower - component.RampMinimum,
  181. 12);
  182. }
  183. else
  184. {
  185. powerLevel = 0;
  186. }
  187. _ambientSound.SetAmbience(uid, powerLevel >= 1);
  188. // TODO: Ok so this introduces popping which is a major shame big rip.
  189. // _ambientSound.SetVolume(uid, MathHelper.Lerp(component.VolumeMin, component.VolumeMax, MathHelper.Clamp01(component.RampPosition / component.MaxVisualPower)));
  190. _appearance.SetData(uid, TegVisuals.PowerOutput, powerLevel);
  191. if (nodeGroup.IsFullyBuilt)
  192. {
  193. UpdateCirculatorAppearance(nodeGroup.CirculatorA!.Owner, powerReceiver.Powered);
  194. UpdateCirculatorAppearance(nodeGroup.CirculatorB!.Owner, powerReceiver.Powered);
  195. }
  196. }
  197. [Access(typeof(TegNodeGroup))]
  198. public void UpdateGeneratorConnectivity(
  199. EntityUid uid,
  200. TegNodeGroup group,
  201. TegGeneratorComponent? component = null)
  202. {
  203. if (!Resolve(uid, ref component))
  204. return;
  205. var powerReceiver = Comp<ApcPowerReceiverComponent>(uid);
  206. _receiver.SetPowerDisabled(uid, !group.IsFullyBuilt, powerReceiver);
  207. UpdateAppearance(uid, component, powerReceiver, group);
  208. }
  209. [Access(typeof(TegNodeGroup))]
  210. public void UpdateCirculatorConnectivity(
  211. EntityUid uid,
  212. TegNodeGroup group,
  213. TegCirculatorComponent? component = null)
  214. {
  215. if (!Resolve(uid, ref component))
  216. return;
  217. // If the group IS fully built, the generator will update its circulators.
  218. // Otherwise, make sure circulator is set to nothing.
  219. if (!group.IsFullyBuilt)
  220. {
  221. UpdateCirculatorAppearance(uid, false);
  222. }
  223. }
  224. private void UpdateCirculatorAppearance(EntityUid uid, bool powered)
  225. {
  226. var circ = Comp<TegCirculatorComponent>(uid);
  227. TegCirculatorSpeed speed;
  228. if (powered && circ.LastPressureDelta > 0 && circ.LastMolesTransferred > 0)
  229. {
  230. if (circ.LastPressureDelta > circ.VisualSpeedDelta)
  231. speed = TegCirculatorSpeed.SpeedFast;
  232. else
  233. speed = TegCirculatorSpeed.SpeedSlow;
  234. }
  235. else
  236. {
  237. speed = TegCirculatorSpeed.SpeedStill;
  238. }
  239. _appearance.SetData(uid, TegVisuals.CirculatorSpeed, speed);
  240. _appearance.SetData(uid, TegVisuals.CirculatorPower, powered);
  241. if (_pointLight.TryGetLight(uid, out var pointLight))
  242. {
  243. _pointLight.SetEnabled(uid, powered, pointLight);
  244. _pointLight.SetColor(uid, speed == TegCirculatorSpeed.SpeedFast ? circ.LightColorFast : circ.LightColorSlow, pointLight);
  245. }
  246. }
  247. private void GeneratorPowerChange(EntityUid uid, TegGeneratorComponent component, ref PowerChangedEvent args)
  248. {
  249. // TODO: I wish power events didn't go out on shutdown.
  250. if (TerminatingOrDeleted(uid))
  251. return;
  252. var nodeGroup = GetNodeGroup(uid);
  253. if (nodeGroup == null)
  254. return;
  255. UpdateAppearance(uid, component, Comp<ApcPowerReceiverComponent>(uid), nodeGroup);
  256. }
  257. /// <returns>Null if the node group is not yet available. This can happen during initialization.</returns>
  258. private TegNodeGroup? GetNodeGroup(EntityUid uidGenerator)
  259. {
  260. NodeContainerComponent? nodeContainer = null;
  261. if (!_nodeContainerQuery.Resolve(uidGenerator, ref nodeContainer))
  262. return null;
  263. if (!nodeContainer.Nodes.TryGetValue(NodeNameTeg, out var tegNode))
  264. return null;
  265. if (tegNode.NodeGroup is not TegNodeGroup tegGroup)
  266. return null;
  267. return tegGroup;
  268. }
  269. private static (GasMixture, float δp) GetCirculatorAirTransfer(GasMixture airInlet, GasMixture airOutlet)
  270. {
  271. var n1 = airInlet.TotalMoles;
  272. var n2 = airOutlet.TotalMoles;
  273. var p1 = airInlet.Pressure;
  274. var p2 = airOutlet.Pressure;
  275. var V1 = airInlet.Volume;
  276. var V2 = airOutlet.Volume;
  277. var T1 = airInlet.Temperature;
  278. var T2 = airOutlet.Temperature;
  279. var δp = p1 - p2;
  280. var denom = T1 * V2 + T2 * V1;
  281. if (δp > 0 && p1 > 0 && denom > 0)
  282. {
  283. var transferMoles = n1 - (n1 + n2) * T2 * V1 / denom;
  284. return (airInlet.Remove(transferMoles), δp);
  285. }
  286. return (new GasMixture(), δp);
  287. }
  288. private (PipeNode inlet, PipeNode outlet) GetPipes(EntityUid uidCirculator)
  289. {
  290. var nodeContainer = _nodeContainerQuery.GetComponent(uidCirculator);
  291. var inlet = (PipeNode) nodeContainer.Nodes[NodeNameInlet];
  292. var outlet = (PipeNode) nodeContainer.Nodes[NodeNameOutlet];
  293. return (inlet, outlet);
  294. }
  295. private void DeviceNetworkPacketReceived(
  296. EntityUid uid,
  297. TegGeneratorComponent component,
  298. DeviceNetworkPacketEvent args)
  299. {
  300. if (!args.Data.TryGetValue(DeviceNetworkConstants.Command, out string? cmd))
  301. return;
  302. switch (cmd)
  303. {
  304. case DeviceNetworkCommandSyncData:
  305. var group = GetNodeGroup(uid);
  306. if (group is not { IsFullyBuilt: true })
  307. return;
  308. var supplier = Comp<PowerSupplierComponent>(uid);
  309. var payload = new NetworkPayload
  310. {
  311. [DeviceNetworkConstants.Command] = DeviceNetworkCommandSyncData,
  312. [DeviceNetworkCommandSyncData] = new TegSensorData
  313. {
  314. CirculatorA = GetCirculatorSensorData(group.CirculatorA!.Owner),
  315. CirculatorB = GetCirculatorSensorData(group.CirculatorB!.Owner),
  316. LastGeneration = component.LastGeneration,
  317. PowerOutput = supplier.CurrentSupply,
  318. RampPosition = component.RampPosition
  319. }
  320. };
  321. _deviceNetwork.QueuePacket(uid, args.SenderAddress, payload);
  322. break;
  323. }
  324. }
  325. private TegSensorData.Circulator GetCirculatorSensorData(EntityUid circulator)
  326. {
  327. var (inlet, outlet) = GetPipes(circulator);
  328. return new TegSensorData.Circulator(
  329. inlet.Air.Pressure,
  330. outlet.Air.Pressure,
  331. inlet.Air.Temperature,
  332. outlet.Air.Temperature);
  333. }
  334. }