Adds the thermo-electric generator (#18840)
* Basic TEG start. Connects via node group. * Basic TEG test map * Sensor monitoring basics, TEG circulator flow * Basic power generation (it doesn't work) * More sensor monitoring work * Battery (SMES) monitoring system. * Sensor monitoring fixes Make it work properly when mapped. * Test map improvements * Revise TEG power output mechanism. Now uses a fixed supplier with a custom ramping system. * TEG test map fixes * Make air alarms and pumps open UI on activate. * Clean up thermo machines power switch. Removed separate Enabled bool from the component that always matched the power receiver's state. This enables adding a PowerSwitch component to give us alt click/verb menu. * TEG but now fancy * Make sensor monitoring console obviously WiP to mappers. * Vending machine sound, because of course. * Terrible, terrible graph background color * Examine improvements for the TEG. * Account for electrical power when equalizing gas mixtures. * Get rid of the TegCirculatorArrow logic. Use TimedDespawn instead. The "no show in right-click menuu" goes into a new general-purpose component. Thanks Julian. * Put big notice of "not ready, here's why" on the sensor monitoring console. * TryGetComponent -> TryComp * Lol there's a HideContextMenu tag * Test fixes * Guidebook for TEG Fixed rotation on GuideEntityEmbed not working correctly. Added Margin property to GuideEntityEmbed * Make TEG power bar default to invisible. So it doesn't appear in the guidebook and spawn menu.
This commit is contained in:
committed by
GitHub
parent
61bf951ec4
commit
a242af506e
@@ -0,0 +1,43 @@
|
||||
using Content.Shared.Atmos;
|
||||
|
||||
namespace Content.Server.Power.Generation.Teg;
|
||||
|
||||
/// <summary>
|
||||
/// A "circulator" for the thermo-electric generator (TEG).
|
||||
/// Circulators are used by the TEG to take in a side of either hot or cold gas.
|
||||
/// </summary>
|
||||
/// <seealso cref="TegSystem"/>
|
||||
[RegisterComponent]
|
||||
[Access(typeof(TegSystem))]
|
||||
public sealed class TegCirculatorComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The difference between the inlet and outlet pressure at the start of the previous tick.
|
||||
/// </summary>
|
||||
[DataField("last_pressure_delta")] [ViewVariables(VVAccess.ReadWrite)]
|
||||
public float LastPressureDelta;
|
||||
|
||||
/// <summary>
|
||||
/// The amount of moles transferred by the circulator last tick.
|
||||
/// </summary>
|
||||
[DataField("last_moles_transferred")] [ViewVariables(VVAccess.ReadWrite)]
|
||||
public float LastMolesTransferred;
|
||||
|
||||
/// <summary>
|
||||
/// Minimum pressure delta between inlet and outlet for which the circulator animation speed is "fast".
|
||||
/// </summary>
|
||||
[DataField("visual_speed_delta")] [ViewVariables(VVAccess.ReadWrite)]
|
||||
public float VisualSpeedDelta = 5 * Atmospherics.OneAtmosphere;
|
||||
|
||||
/// <summary>
|
||||
/// Light color of this circulator when it's running at "slow" speed.
|
||||
/// </summary>
|
||||
[DataField("light_color_slow")] [ViewVariables(VVAccess.ReadWrite)]
|
||||
public Color LightColorSlow;
|
||||
|
||||
/// <summary>
|
||||
/// Light color of this circulator when it's running at "fast" speed.
|
||||
/// </summary>
|
||||
[DataField("light_color_fast")] [ViewVariables(VVAccess.ReadWrite)]
|
||||
public Color LightColorFast;
|
||||
}
|
||||
69
Content.Server/Power/Generation/Teg/TegGeneratorComponent.cs
Normal file
69
Content.Server/Power/Generation/Teg/TegGeneratorComponent.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
namespace Content.Server.Power.Generation.Teg;
|
||||
|
||||
/// <summary>
|
||||
/// The centerpiece for the thermo-electric generator (TEG).
|
||||
/// </summary>
|
||||
/// <seealso cref="TegSystem"/>
|
||||
[RegisterComponent]
|
||||
[Access(typeof(TegSystem))]
|
||||
public sealed class TegGeneratorComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// When transferring energy from the hot to cold side,
|
||||
/// determines how much of that energy can be extracted as electricity.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A value of 0.9 means that 90% of energy transferred goes to electricity.
|
||||
/// </remarks>
|
||||
[ViewVariables(VVAccess.ReadWrite)] [DataField("thermal_efficiency")]
|
||||
public float ThermalEfficiency = 0.65f;
|
||||
|
||||
/// <summary>
|
||||
/// Simple factor that scales effective electricity generation.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)] [DataField("power_factor")]
|
||||
public float PowerFactor = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Amount of energy (Joules) generated by the TEG last atmos tick.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)] [DataField("last_generation")]
|
||||
public float LastGeneration;
|
||||
|
||||
/// <summary>
|
||||
/// The current target for TEG power generation.
|
||||
/// Drifts towards actual power draw of the network with <see cref="PowerFactor"/>.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)] [DataField("ramp_position")]
|
||||
public float RampPosition;
|
||||
|
||||
/// <summary>
|
||||
/// Factor by which TEG power generation scales, both up and down.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)] [DataField("ramp_factor")]
|
||||
public float RampFactor = 1.05f;
|
||||
|
||||
/// <summary>
|
||||
/// Minimum position for the ramp. Avoids TEG taking too long to start.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)] [DataField("ramp_threshold")]
|
||||
public float RampMinimum = 5000;
|
||||
|
||||
/// <summary>
|
||||
/// Power output value at which the sprite appearance and sound volume should cap out.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)] [DataField("max_visual_power")]
|
||||
public float MaxVisualPower = 200_000;
|
||||
|
||||
/// <summary>
|
||||
/// Minimum ambient sound volume, when we're producing just barely any power at all.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)] [DataField("volume_min")]
|
||||
public float VolumeMin = -9;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum ambient sound volume, when we're producing >= <see cref="MaxVisualPower"/> power.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)] [DataField("volume_max")]
|
||||
public float VolumeMax = -4;
|
||||
}
|
||||
206
Content.Server/Power/Generation/Teg/TegNodeGroup.cs
Normal file
206
Content.Server/Power/Generation/Teg/TegNodeGroup.cs
Normal file
@@ -0,0 +1,206 @@
|
||||
using System.Linq;
|
||||
using Content.Server.NodeContainer;
|
||||
using Content.Server.NodeContainer.NodeGroups;
|
||||
using Content.Server.NodeContainer.Nodes;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.Power.Generation.Teg;
|
||||
|
||||
/// <summary>
|
||||
/// Node group that connects the central TEG with its two circulators.
|
||||
/// </summary>
|
||||
/// <seealso cref="TegNodeGenerator"/>
|
||||
/// <seealso cref="TegNodeCirculator"/>
|
||||
/// <seealso cref="TegSystem"/>
|
||||
[NodeGroup(NodeGroupID.Teg)]
|
||||
public sealed class TegNodeGroup : BaseNodeGroup
|
||||
{
|
||||
/// <summary>
|
||||
/// If true, this TEG is fully built and has all its parts properly connected.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public bool IsFullyBuilt { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The central generator component.
|
||||
/// </summary>
|
||||
/// <seealso cref="TegGeneratorComponent"/>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public TegNodeGenerator? Generator { get; private set; }
|
||||
|
||||
// Illustration for how the TEG A/B circulators are laid out.
|
||||
// Circulator B Generator Circulator A
|
||||
// ^ -> |
|
||||
// | V
|
||||
// They have rotations like the arrows point out.
|
||||
|
||||
/// <summary>
|
||||
/// The A-side circulator. This is the circulator that is in the direction FACING the center component's rotation.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Not filled in if there is no center piece to deduce relative rotation from.
|
||||
/// </remarks>
|
||||
/// <seealso cref="TegCirculatorComponent"/>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public TegNodeCirculator? CirculatorA { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The B-side circulator. This circulator is opposite <see cref="CirculatorA"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Not filled in if there is no center piece to deduce relative rotation from.
|
||||
/// </remarks>
|
||||
/// <seealso cref="TegCirculatorComponent"/>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public TegNodeCirculator? CirculatorB { get; private set; }
|
||||
|
||||
private IEntityManager? _entityManager;
|
||||
|
||||
public override void Initialize(Node sourceNode, IEntityManager entMan)
|
||||
{
|
||||
base.Initialize(sourceNode, entMan);
|
||||
|
||||
_entityManager = entMan;
|
||||
}
|
||||
|
||||
public override void LoadNodes(List<Node> groupNodes)
|
||||
{
|
||||
DebugTools.Assert(groupNodes.Count <= 3, "The TEG has at most 3 parts");
|
||||
DebugTools.Assert(_entityManager != null);
|
||||
|
||||
base.LoadNodes(groupNodes);
|
||||
|
||||
Generator = groupNodes.OfType<TegNodeGenerator>().SingleOrDefault();
|
||||
if (Generator != null)
|
||||
{
|
||||
// If we have a generator, we can assign CirculatorA and CirculatorB based on relative rotation.
|
||||
var xformGenerator = _entityManager.GetComponent<TransformComponent>(Generator.Owner);
|
||||
var genDir = xformGenerator.LocalRotation.GetDir();
|
||||
|
||||
foreach (var node in groupNodes)
|
||||
{
|
||||
if (node is not TegNodeCirculator circulator)
|
||||
continue;
|
||||
|
||||
var xform = _entityManager.GetComponent<TransformComponent>(node.Owner);
|
||||
var dir = xform.LocalRotation.GetDir();
|
||||
if (genDir.GetClockwise90Degrees() == dir)
|
||||
{
|
||||
CirculatorA = circulator;
|
||||
}
|
||||
else
|
||||
{
|
||||
CirculatorB = circulator;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
IsFullyBuilt = Generator != null && CirculatorA != null && CirculatorB != null;
|
||||
|
||||
var tegSystem = _entityManager.EntitySysManager.GetEntitySystem<TegSystem>();
|
||||
foreach (var node in groupNodes)
|
||||
{
|
||||
if (node is TegNodeGenerator generator)
|
||||
tegSystem.UpdateGeneratorConnectivity(generator.Owner, this);
|
||||
|
||||
if (node is TegNodeCirculator circulator)
|
||||
tegSystem.UpdateCirculatorConnectivity(circulator.Owner, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Node used by the central TEG generator component.
|
||||
/// </summary>
|
||||
/// <seealso cref="TegNodeGroup"/>
|
||||
/// <seealso cref="TegGeneratorComponent"/>
|
||||
[DataDefinition]
|
||||
public sealed class TegNodeGenerator : Node
|
||||
{
|
||||
public override IEnumerable<Node> GetReachableNodes(
|
||||
TransformComponent xform,
|
||||
EntityQuery<NodeContainerComponent> nodeQuery,
|
||||
EntityQuery<TransformComponent> xformQuery,
|
||||
MapGridComponent? grid,
|
||||
IEntityManager entMan)
|
||||
{
|
||||
if (!xform.Anchored || grid == null)
|
||||
yield break;
|
||||
|
||||
var gridIndex = grid.TileIndicesFor(xform.Coordinates);
|
||||
|
||||
var dir = xform.LocalRotation.GetDir();
|
||||
var a = FindCirculator(dir);
|
||||
var b = FindCirculator(dir.GetOpposite());
|
||||
|
||||
if (a != null)
|
||||
yield return a;
|
||||
|
||||
if (b != null)
|
||||
yield return b;
|
||||
|
||||
TegNodeCirculator? FindCirculator(Direction searchDir)
|
||||
{
|
||||
var targetIdx = gridIndex.Offset(searchDir);
|
||||
|
||||
foreach (var node in NodeHelpers.GetNodesInTile(nodeQuery, grid, targetIdx))
|
||||
{
|
||||
if (node is not TegNodeCirculator circulator)
|
||||
continue;
|
||||
|
||||
var entity = node.Owner;
|
||||
var entityXform = xformQuery.GetComponent(entity);
|
||||
var entityDir = entityXform.LocalRotation.GetDir();
|
||||
|
||||
if (entityDir == searchDir.GetClockwise90Degrees())
|
||||
return circulator;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Node used by the central TEG circulator entities.
|
||||
/// </summary>
|
||||
/// <seealso cref="TegNodeGroup"/>
|
||||
/// <seealso cref="TegCirculatorComponent"/>
|
||||
[DataDefinition]
|
||||
public sealed class TegNodeCirculator : Node
|
||||
{
|
||||
public override IEnumerable<Node> GetReachableNodes(
|
||||
TransformComponent xform,
|
||||
EntityQuery<NodeContainerComponent> nodeQuery,
|
||||
EntityQuery<TransformComponent> xformQuery,
|
||||
MapGridComponent? grid,
|
||||
IEntityManager entMan)
|
||||
{
|
||||
if (!xform.Anchored || grid == null)
|
||||
yield break;
|
||||
|
||||
var gridIndex = grid.TileIndicesFor(xform.Coordinates);
|
||||
|
||||
var dir = xform.LocalRotation.GetDir();
|
||||
var searchDir = dir.GetClockwise90Degrees();
|
||||
var targetIdx = gridIndex.Offset(searchDir);
|
||||
|
||||
foreach (var node in NodeHelpers.GetNodesInTile(nodeQuery, grid, targetIdx))
|
||||
{
|
||||
if (node is not TegNodeGenerator generator)
|
||||
continue;
|
||||
|
||||
var entity = node.Owner;
|
||||
var entityXform = xformQuery.GetComponent(entity);
|
||||
var entityDir = entityXform.LocalRotation.GetDir();
|
||||
|
||||
if (entityDir == searchDir || entityDir == searchDir.GetOpposite())
|
||||
{
|
||||
yield return generator;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
52
Content.Server/Power/Generation/Teg/TegSensorData.cs
Normal file
52
Content.Server/Power/Generation/Teg/TegSensorData.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using Content.Server.Power.Components;
|
||||
|
||||
namespace Content.Server.Power.Generation.Teg;
|
||||
|
||||
/// <summary>
|
||||
/// Sensor data reported by the <see cref="TegGeneratorComponent"/> when queried over the device network.
|
||||
/// </summary>
|
||||
/// <seealso cref="TegSystem"/>
|
||||
public sealed class TegSensorData
|
||||
{
|
||||
/// <summary>
|
||||
/// Information for the A-side circulator.
|
||||
/// </summary>
|
||||
public Circulator CirculatorA;
|
||||
|
||||
/// <summary>
|
||||
/// Information for the B-side circulator.
|
||||
/// </summary>
|
||||
public Circulator CirculatorB;
|
||||
|
||||
/// <summary>
|
||||
/// The amount of energy (Joules) generated by the TEG last atmos tick.
|
||||
/// </summary>
|
||||
/// <seealso cref="TegGeneratorComponent.LastGeneration"/>
|
||||
public float LastGeneration;
|
||||
|
||||
/// <summary>
|
||||
/// Ramping position for the TEG power generation.
|
||||
/// </summary>
|
||||
/// <seealso cref="TegGeneratorComponent.RampPosition"/>
|
||||
public float RampPosition;
|
||||
|
||||
/// <summary>
|
||||
/// Power (Watts) actually being supplied by the TEG to connected power network.
|
||||
/// </summary>
|
||||
/// <seealso cref="PowerSupplierComponent.CurrentSupply"/>
|
||||
public float PowerOutput;
|
||||
|
||||
/// <summary>
|
||||
/// Information for a single TEG circulator.
|
||||
/// </summary>
|
||||
/// <param name="InletPressure">Pressure measured at the circulator's input pipe</param>
|
||||
/// <param name="OutletPressure">Pressure measured at the circulator's output pipe</param>
|
||||
/// <param name="InletTemperature">Temperature measured at the circulator's input pipe</param>
|
||||
/// <param name="OutletTemperature">Temperature measured at the circulator's output pipe</param>
|
||||
public record struct Circulator(
|
||||
float InletPressure,
|
||||
float OutletPressure,
|
||||
float InletTemperature,
|
||||
float OutletTemperature);
|
||||
}
|
||||
|
||||
383
Content.Server/Power/Generation/Teg/TegSystem.cs
Normal file
383
Content.Server/Power/Generation/Teg/TegSystem.cs
Normal file
@@ -0,0 +1,383 @@
|
||||
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.Examine;
|
||||
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 AtmosphereSystem _atmosphere = default!;
|
||||
[Dependency] private readonly DeviceNetworkSystem _deviceNetwork = default!;
|
||||
[Dependency] private readonly AppearanceSystem _appearance = default!;
|
||||
[Dependency] private readonly PointLightSystem _pointLight = default!;
|
||||
[Dependency] private readonly AmbientSoundSystem _ambientSound = 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, AtmosDeviceUpdateEvent args)
|
||||
{
|
||||
var tegGroup = GetNodeGroup(uid);
|
||||
if (tegGroup is not { IsFullyBuilt: true })
|
||||
return;
|
||||
|
||||
var supplier = Comp<PowerSupplierComponent>(uid);
|
||||
var powerReceiver = Comp<ApcPowerReceiverComponent>(uid);
|
||||
if (!powerReceiver.Powered)
|
||||
{
|
||||
supplier.MaxSupply = 0;
|
||||
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);
|
||||
var cB = _atmosphere.GetHeatCapacity(airB);
|
||||
|
||||
// Shift ramp position based on demand and generation from previous tick.
|
||||
var curRamp = component.RampPosition;
|
||||
var lastDraw = supplier.CurrentSupply;
|
||||
// Limit amount lost/gained based on power factor.
|
||||
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;
|
||||
var cHot = hotA ? cA : cB;
|
||||
|
||||
// Calculate maximum amount of energy to generate this tick based on ramping above.
|
||||
// This clamps the thermal energy transfer as well.
|
||||
var targetEnergy = curRamp / _atmosphere.AtmosTickRate;
|
||||
var transferMax = targetEnergy / (component.ThermalEfficiency * component.PowerFactor);
|
||||
|
||||
// Calculate thermal and electrical energy transfer between the two sides.
|
||||
var δT = MathF.Abs(airA.Temperature - airB.Temperature);
|
||||
var transfer = Math.Min(δT * cA * cB / (cA + cB - cHot * component.ThermalEfficiency), transferMax);
|
||||
electricalEnergy = transfer * component.ThermalEfficiency * 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 * _atmosphere.AtmosTickRate;
|
||||
// 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);
|
||||
|
||||
powerReceiver.PowerDisabled = !group.IsFullyBuilt;
|
||||
|
||||
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 (TryComp(uid, out PointLightComponent? pointLight))
|
||||
{
|
||||
_pointLight.SetEnabled(uid, powered, pointLight);
|
||||
pointLight.Color = speed == TegCirculatorSpeed.SpeedFast ? circ.LightColorFast : circ.LightColorSlow;
|
||||
}
|
||||
}
|
||||
|
||||
private void GeneratorPowerChange(EntityUid uid, TegGeneratorComponent component, ref PowerChangedEvent args)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user