| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401 |
- using Content.Server.Atmos;
- using Content.Server.Atmos.EntitySystems;
- using Content.Server.Atmos.Piping.Components;
- using Content.Server.Audio;
- using Content.Server.DeviceNetwork;
- using Content.Server.DeviceNetwork.Systems;
- using Content.Server.NodeContainer;
- using Content.Server.NodeContainer.Nodes;
- using Content.Server.Power.Components;
- using Content.Shared.Atmos;
- using Content.Shared.DeviceNetwork;
- using Content.Shared.Examine;
- using Content.Shared.Power;
- using Content.Shared.Power.EntitySystems;
- using Content.Shared.Power.Generation.Teg;
- using Content.Shared.Rounding;
- using Robust.Server.GameObjects;
- namespace Content.Server.Power.Generation.Teg;
- /// <summary>
- /// Handles processing logic for the thermo-electric generator (TEG).
- /// </summary>
- /// <remarks>
- /// <para>
- /// The TEG generates power by exchanging heat between gases flowing through its two sides.
- /// The gas flows through a "circulator" entity on each side, which have both an inlet and an outlet port.
- /// </para>
- /// <remarks>
- /// Connecting the TEG core to its circulators is implemented via a node group. See <see cref="TegNodeGroup"/>.
- /// </remarks>
- /// <para>
- /// The TEG center does HV power output, and must also be connected to an LV wire for the TEG to function.
- /// </para>
- /// <para>
- /// Unlike in SS13, the TEG actually adjusts gas heat exchange to match the energy demand of the power network.
- /// To achieve this, the TEG implements its own ramping logic instead of using the built-in Pow3r ramping.
- /// The TEG actually has a maximum output of +n% more than was really generated,
- /// which allows Pow3r to draw more power to "signal" that there is more network load.
- /// The ramping is also exponential instead of linear like in normal Pow3r.
- /// This system does mean a fully-loaded TEG creates +n% power out of thin air, but this is considered acceptable.
- /// </para>
- /// </remarks>
- /// <seealso cref="TegGeneratorComponent"/>
- /// <seealso cref="TegCirculatorComponent"/>
- /// <seealso cref="TegNodeGroup"/>
- /// <seealso cref="TegSensorData"/>
- public sealed class TegSystem : EntitySystem
- {
- /// <summary>
- /// Node name for the TEG part connection nodes (<see cref="TegNodeGroup"/>).
- /// </summary>
- private const string NodeNameTeg = "teg";
- /// <summary>
- /// Node name for the inlet pipe of a circulator.
- /// </summary>
- private const string NodeNameInlet = "inlet";
- /// <summary>
- /// Node name for the outlet pipe of a circulator.
- /// </summary>
- private const string NodeNameOutlet = "outlet";
- /// <summary>
- /// Device network command to have the TEG output a <see cref="TegSensorData"/> object for its last statistics.
- /// </summary>
- public const string DeviceNetworkCommandSyncData = "teg_sync_data";
- [Dependency] private readonly AmbientSoundSystem _ambientSound = default!;
- [Dependency] private readonly AppearanceSystem _appearance = default!;
- [Dependency] private readonly AtmosphereSystem _atmosphere = default!;
- [Dependency] private readonly DeviceNetworkSystem _deviceNetwork = default!;
- [Dependency] private readonly PointLightSystem _pointLight = default!;
- [Dependency] private readonly SharedPowerReceiverSystem _receiver = default!;
- private EntityQuery<NodeContainerComponent> _nodeContainerQuery;
- public override void Initialize()
- {
- base.Initialize();
- SubscribeLocalEvent<TegGeneratorComponent, AtmosDeviceUpdateEvent>(GeneratorUpdate);
- SubscribeLocalEvent<TegGeneratorComponent, PowerChangedEvent>(GeneratorPowerChange);
- SubscribeLocalEvent<TegGeneratorComponent, DeviceNetworkPacketEvent>(DeviceNetworkPacketReceived);
- SubscribeLocalEvent<TegGeneratorComponent, ExaminedEvent>(GeneratorExamined);
- _nodeContainerQuery = GetEntityQuery<NodeContainerComponent>();
- }
- private void GeneratorExamined(EntityUid uid, TegGeneratorComponent component, ExaminedEvent args)
- {
- if (GetNodeGroup(uid) is not { IsFullyBuilt: true })
- {
- args.PushMarkup(Loc.GetString("teg-generator-examine-connection"));
- }
- else
- {
- var supplier = Comp<PowerSupplierComponent>(uid);
- args.PushMarkup(Loc.GetString("teg-generator-examine-power", ("power", supplier.CurrentSupply)));
- }
- }
- private void GeneratorUpdate(EntityUid uid, TegGeneratorComponent component, ref AtmosDeviceUpdateEvent args)
- {
- var supplier = Comp<PowerSupplierComponent>(uid);
- var powerReceiver = Comp<ApcPowerReceiverComponent>(uid);
- if (!powerReceiver.Powered)
- {
- supplier.MaxSupply = 0;
- return;
- }
- var tegGroup = GetNodeGroup(uid);
- if (tegGroup is not { IsFullyBuilt: true })
- return;
- var circA = tegGroup.CirculatorA!.Owner;
- var circB = tegGroup.CirculatorB!.Owner;
- var (inletA, outletA) = GetPipes(circA);
- var (inletB, outletB) = GetPipes(circB);
- var (airA, δpA) = GetCirculatorAirTransfer(inletA.Air, outletA.Air);
- var (airB, δpB) = GetCirculatorAirTransfer(inletB.Air, outletB.Air);
- var cA = _atmosphere.GetHeatCapacity(airA, true);
- var cB = _atmosphere.GetHeatCapacity(airB, true);
- // Shift ramp position based on demand and generation from previous tick.
- var curRamp = component.RampPosition;
- var lastDraw = supplier.CurrentSupply;
- curRamp = MathHelper.Clamp(lastDraw, curRamp / component.RampFactor, curRamp * component.RampFactor);
- curRamp = MathF.Max(curRamp, component.RampMinimum);
- component.RampPosition = curRamp;
- var electricalEnergy = 0f;
- if (airA.Pressure > 0 && airB.Pressure > 0)
- {
- var hotA = airA.Temperature > airB.Temperature;
- // Calculate thermal and electrical energy transfer between the two sides.
- // Assume temperature equalizes, i.e. Ta*cA + Tb*cB = Tf*(cA+cB)
- var Tf = (airA.Temperature * cA + airB.Temperature * cB) / (cA + cB);
- // The maximum energy we can extract is (Ta - Tf)*cA, which is equal to (Tf - Tb)*cB
- var Wmax = MathF.Abs(airA.Temperature - Tf) * cA;
- var N = component.ThermalEfficiency;
- // Calculate Carnot efficiency
- var Thot = hotA ? airA.Temperature : airB.Temperature;
- var Tcold = hotA ? airB.Temperature : airA.Temperature;
- var Nmax = 1 - Tcold / Thot;
- N = MathF.Min(N, Nmax); // clamp by Carnot efficiency
- // Reduce efficiency at low temperature differences to encourage burn chambers (instead
- // of just feeding the TEG room temperature gas from an infinite gas miner).
- var dT = Thot - Tcold;
- N *= MathF.Tanh(dT/700); // https://www.wolframalpha.com/input?i=tanh(x/700)+from+0+to+1000
- var transfer = Wmax * N;
- electricalEnergy = transfer * component.PowerFactor;
- var outTransfer = transfer * (1 - component.ThermalEfficiency);
- // Adjust thermal energy in transferred gas mixtures.
- if (hotA)
- {
- // A -> B
- airA.Temperature -= transfer / cA;
- airB.Temperature += outTransfer / cB;
- }
- else
- {
- // B -> A
- airA.Temperature += outTransfer / cA;
- airB.Temperature -= transfer / cB;
- }
- }
- component.LastGeneration = electricalEnergy;
- // Turn energy (at atmos tick rate) into wattage.
- var power = electricalEnergy / args.dt;
- // Add ramp factor. This magics slight power into existence, but allows us to ramp up.
- supplier.MaxSupply = power * component.RampFactor;
- var circAComp = Comp<TegCirculatorComponent>(circA);
- var circBComp = Comp<TegCirculatorComponent>(circB);
- circAComp.LastPressureDelta = δpA;
- circAComp.LastMolesTransferred = airA.TotalMoles;
- circBComp.LastPressureDelta = δpB;
- circBComp.LastMolesTransferred = airB.TotalMoles;
- _atmosphere.Merge(outletA.Air, airA);
- _atmosphere.Merge(outletB.Air, airB);
- UpdateAppearance(uid, component, powerReceiver, tegGroup);
- }
- private void UpdateAppearance(
- EntityUid uid,
- TegGeneratorComponent component,
- ApcPowerReceiverComponent powerReceiver,
- TegNodeGroup nodeGroup)
- {
- int powerLevel;
- if (powerReceiver.Powered)
- {
- powerLevel = ContentHelpers.RoundToLevels(
- component.RampPosition - component.RampMinimum,
- component.MaxVisualPower - component.RampMinimum,
- 12);
- }
- else
- {
- powerLevel = 0;
- }
- _ambientSound.SetAmbience(uid, powerLevel >= 1);
- // TODO: Ok so this introduces popping which is a major shame big rip.
- // _ambientSound.SetVolume(uid, MathHelper.Lerp(component.VolumeMin, component.VolumeMax, MathHelper.Clamp01(component.RampPosition / component.MaxVisualPower)));
- _appearance.SetData(uid, TegVisuals.PowerOutput, powerLevel);
- if (nodeGroup.IsFullyBuilt)
- {
- UpdateCirculatorAppearance(nodeGroup.CirculatorA!.Owner, powerReceiver.Powered);
- UpdateCirculatorAppearance(nodeGroup.CirculatorB!.Owner, powerReceiver.Powered);
- }
- }
- [Access(typeof(TegNodeGroup))]
- public void UpdateGeneratorConnectivity(
- EntityUid uid,
- TegNodeGroup group,
- TegGeneratorComponent? component = null)
- {
- if (!Resolve(uid, ref component))
- return;
- var powerReceiver = Comp<ApcPowerReceiverComponent>(uid);
- _receiver.SetPowerDisabled(uid, !group.IsFullyBuilt, powerReceiver);
- UpdateAppearance(uid, component, powerReceiver, group);
- }
- [Access(typeof(TegNodeGroup))]
- public void UpdateCirculatorConnectivity(
- EntityUid uid,
- TegNodeGroup group,
- TegCirculatorComponent? component = null)
- {
- if (!Resolve(uid, ref component))
- return;
- // If the group IS fully built, the generator will update its circulators.
- // Otherwise, make sure circulator is set to nothing.
- if (!group.IsFullyBuilt)
- {
- UpdateCirculatorAppearance(uid, false);
- }
- }
- private void UpdateCirculatorAppearance(EntityUid uid, bool powered)
- {
- var circ = Comp<TegCirculatorComponent>(uid);
- TegCirculatorSpeed speed;
- if (powered && circ.LastPressureDelta > 0 && circ.LastMolesTransferred > 0)
- {
- if (circ.LastPressureDelta > circ.VisualSpeedDelta)
- speed = TegCirculatorSpeed.SpeedFast;
- else
- speed = TegCirculatorSpeed.SpeedSlow;
- }
- else
- {
- speed = TegCirculatorSpeed.SpeedStill;
- }
- _appearance.SetData(uid, TegVisuals.CirculatorSpeed, speed);
- _appearance.SetData(uid, TegVisuals.CirculatorPower, powered);
- if (_pointLight.TryGetLight(uid, out var pointLight))
- {
- _pointLight.SetEnabled(uid, powered, pointLight);
- _pointLight.SetColor(uid, speed == TegCirculatorSpeed.SpeedFast ? circ.LightColorFast : circ.LightColorSlow, pointLight);
- }
- }
- private void GeneratorPowerChange(EntityUid uid, TegGeneratorComponent component, ref PowerChangedEvent args)
- {
- // TODO: I wish power events didn't go out on shutdown.
- if (TerminatingOrDeleted(uid))
- return;
- var nodeGroup = GetNodeGroup(uid);
- if (nodeGroup == null)
- return;
- UpdateAppearance(uid, component, Comp<ApcPowerReceiverComponent>(uid), nodeGroup);
- }
- /// <returns>Null if the node group is not yet available. This can happen during initialization.</returns>
- private TegNodeGroup? GetNodeGroup(EntityUid uidGenerator)
- {
- NodeContainerComponent? nodeContainer = null;
- if (!_nodeContainerQuery.Resolve(uidGenerator, ref nodeContainer))
- return null;
- if (!nodeContainer.Nodes.TryGetValue(NodeNameTeg, out var tegNode))
- return null;
- if (tegNode.NodeGroup is not TegNodeGroup tegGroup)
- return null;
- return tegGroup;
- }
- private static (GasMixture, float δp) GetCirculatorAirTransfer(GasMixture airInlet, GasMixture airOutlet)
- {
- var n1 = airInlet.TotalMoles;
- var n2 = airOutlet.TotalMoles;
- var p1 = airInlet.Pressure;
- var p2 = airOutlet.Pressure;
- var V1 = airInlet.Volume;
- var V2 = airOutlet.Volume;
- var T1 = airInlet.Temperature;
- var T2 = airOutlet.Temperature;
- var δp = p1 - p2;
- var denom = T1 * V2 + T2 * V1;
- if (δp > 0 && p1 > 0 && denom > 0)
- {
- var transferMoles = n1 - (n1 + n2) * T2 * V1 / denom;
- return (airInlet.Remove(transferMoles), δp);
- }
- return (new GasMixture(), δp);
- }
- private (PipeNode inlet, PipeNode outlet) GetPipes(EntityUid uidCirculator)
- {
- var nodeContainer = _nodeContainerQuery.GetComponent(uidCirculator);
- var inlet = (PipeNode) nodeContainer.Nodes[NodeNameInlet];
- var outlet = (PipeNode) nodeContainer.Nodes[NodeNameOutlet];
- return (inlet, outlet);
- }
- private void DeviceNetworkPacketReceived(
- EntityUid uid,
- TegGeneratorComponent component,
- DeviceNetworkPacketEvent args)
- {
- if (!args.Data.TryGetValue(DeviceNetworkConstants.Command, out string? cmd))
- return;
- switch (cmd)
- {
- case DeviceNetworkCommandSyncData:
- var group = GetNodeGroup(uid);
- if (group is not { IsFullyBuilt: true })
- return;
- var supplier = Comp<PowerSupplierComponent>(uid);
- var payload = new NetworkPayload
- {
- [DeviceNetworkConstants.Command] = DeviceNetworkCommandSyncData,
- [DeviceNetworkCommandSyncData] = new TegSensorData
- {
- CirculatorA = GetCirculatorSensorData(group.CirculatorA!.Owner),
- CirculatorB = GetCirculatorSensorData(group.CirculatorB!.Owner),
- LastGeneration = component.LastGeneration,
- PowerOutput = supplier.CurrentSupply,
- RampPosition = component.RampPosition
- }
- };
- _deviceNetwork.QueuePacket(uid, args.SenderAddress, payload);
- break;
- }
- }
- private TegSensorData.Circulator GetCirculatorSensorData(EntityUid circulator)
- {
- var (inlet, outlet) = GetPipes(circulator);
- return new TegSensorData.Circulator(
- inlet.Air.Pressure,
- outlet.Air.Pressure,
- inlet.Air.Temperature,
- outlet.Air.Temperature);
- }
- }
|