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:
Pieter-Jan Briers
2023-08-12 22:41:55 +02:00
committed by GitHub
parent 61bf951ec4
commit a242af506e
74 changed files with 5546 additions and 22 deletions

View File

@@ -51,6 +51,11 @@ namespace Content.Client.Computer
_window?.Dispose(); _window?.Dispose();
} }
} }
protected override void ReceiveMessage(BoundUserInterfaceMessage message)
{
_window?.ReceiveMessage(message);
}
} }
/// <summary> /// <summary>
@@ -79,6 +84,10 @@ namespace Content.Client.Computer
void UpdateState(TState state) void UpdateState(TState state)
{ {
} }
void ReceiveMessage(BoundUserInterfaceMessage message)
{
}
} }
} }

View File

@@ -359,6 +359,9 @@ namespace Content.Client.Examine
_idCounter = 0; _idCounter = 0;
RaiseNetworkEvent(new ExamineSystemMessages.RequestExamineInfoMessage(entity, _idCounter, true)); RaiseNetworkEvent(new ExamineSystemMessages.RequestExamineInfoMessage(entity, _idCounter, true));
} }
RaiseLocalEvent(entity, new ClientExaminedEvent(entity, playerEnt.Value));
_lastExaminedEntity = entity; _lastExaminedEntity = entity;
} }
@@ -384,4 +387,26 @@ namespace Content.Client.Examine
} }
} }
} }
/// <summary>
/// An entity was examined on the client.
/// </summary>
public sealed class ClientExaminedEvent : EntityEventArgs
{
/// <summary>
/// The entity performing the examining.
/// </summary>
public readonly EntityUid Examiner;
/// <summary>
/// Entity being examined, for broadcast event purposes.
/// </summary>
public readonly EntityUid Examined;
public ClientExaminedEvent(EntityUid examined, EntityUid examiner)
{
Examined = examined;
Examiner = examiner;
}
}
} }

View File

@@ -1,5 +1,6 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Globalization; using System.Globalization;
using System.Linq;
using System.Numerics; using System.Numerics;
using Content.Client.ContextMenu.UI; using Content.Client.ContextMenu.UI;
using Content.Client.Examine; using Content.Client.Examine;
@@ -15,6 +16,7 @@ using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML; using Robust.Client.UserInterface.XAML;
using Robust.Shared.Input; using Robust.Shared.Input;
using Robust.Shared.Map; using Robust.Shared.Map;
using Robust.Shared.Utility;
namespace Content.Client.Guidebook.Controls; namespace Content.Client.Guidebook.Controls;
@@ -169,10 +171,17 @@ public sealed partial class GuideEntityEmbed : BoxContainer, IDocumentTag
if (args.TryGetValue("Rotation", out var rotation)) if (args.TryGetValue("Rotation", out var rotation))
{ {
Sprite.Rotation = Angle.FromDegrees(double.Parse(rotation)); View.OverrideDirection = Angle.FromDegrees(double.Parse(rotation)).GetDir();
} }
if (args.TryGetValue("Margin", out var margin))
{
Margin = ParseThickness(margin);
}
else
{
Margin = new Thickness(4, 8); Margin = new Thickness(4, 8);
}
// By default, we will map-initialize guidebook entities. // By default, we will map-initialize guidebook entities.
if (!args.TryGetValue("Init", out var mapInit) || !bool.Parse(mapInit)) if (!args.TryGetValue("Init", out var mapInit) || !bool.Parse(mapInit))
@@ -181,4 +190,20 @@ public sealed partial class GuideEntityEmbed : BoxContainer, IDocumentTag
control = this; control = this;
return true; return true;
} }
private static Thickness ParseThickness(string value)
{
if (string.IsNullOrWhiteSpace(value))
return default;
var split = value.Split(" ", StringSplitOptions.RemoveEmptyEntries).Select(x => Parse.Float(x)).ToArray();
if (split.Length == 1)
return new Thickness(split[0]);
if (split.Length == 2)
return new Thickness(split[0], split[1]);
if (split.Length == 4)
return new Thickness(split[0], split[1], split[2], split[3]);
throw new Exception("Invalid Thickness format!");
}
} }

View File

@@ -0,0 +1,8 @@
namespace Content.Client.Power.Generation.Teg;
/// <seealso cref="TegSystem"/>
[RegisterComponent]
public sealed class TegCirculatorComponent : Component
{
}

View File

@@ -0,0 +1,26 @@
using Content.Client.Examine;
using Robust.Shared.Map;
namespace Content.Client.Power.Generation.Teg;
/// <summary>
/// Handles client-side logic for the thermo-electric generator (TEG).
/// </summary>
/// <remarks>
/// <para>
/// TEG circulators show which direction the in- and outlet port is by popping up two floating arrows when examined.
/// </para>
/// </remarks>
/// <seealso cref="TegCirculatorComponent"/>
public sealed class TegSystem : EntitySystem
{
public override void Initialize()
{
SubscribeLocalEvent<TegCirculatorComponent, ClientExaminedEvent>(CirculatorExamined);
}
private void CirculatorExamined(EntityUid uid, TegCirculatorComponent component, ClientExaminedEvent args)
{
Spawn("TegCirculatorArrow", new EntityCoordinates(uid, 0, 0));
}
}

View File

@@ -0,0 +1,10 @@
<contentControls:FancyWindow
xmlns="https://spacestation14.io"
xmlns:contentControls="clr-namespace:Content.Client.UserInterface.Controls"
Title="{Loc 'sensor-monitoring-window-title'}" MinWidth="640" MinHeight="480">
<ScrollContainer>
<BoxContainer Name="Asdf" Orientation="Vertical" Margin="4 0">
</BoxContainer>
</ScrollContainer>
</contentControls:FancyWindow>

View File

@@ -0,0 +1,264 @@
using System.Linq;
using System.Numerics;
using Content.Client.Computer;
using Content.Client.Stylesheets;
using Content.Client.UserInterface.Controls;
using Content.Shared.SensorMonitoring;
using JetBrains.Annotations;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Timing;
using ConsoleUIState = Content.Shared.SensorMonitoring.SensorMonitoringConsoleBoundInterfaceState;
using IncrementalUIState = Content.Shared.SensorMonitoring.SensorMonitoringIncrementalUpdate;
namespace Content.Client.SensorMonitoring;
[GenerateTypedNameReferences]
public sealed partial class SensorMonitoringWindow : FancyWindow, IComputerWindow<ConsoleUIState>
{
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly ILocalizationManager _loc = default!;
private TimeSpan _retentionTime;
private readonly Dictionary<int, SensorData> _sensorData = new();
public SensorMonitoringWindow()
{
RobustXamlLoader.Load(this);
}
public void UpdateState(ConsoleUIState state)
{
_retentionTime = state.RetentionTime;
_sensorData.Clear();
foreach (var netSensor in state.Sensors)
{
var sensor = new SensorData
{
Name = netSensor.Name,
Address = netSensor.Address,
DeviceType = netSensor.DeviceType
};
_sensorData.Add(netSensor.NetId, sensor);
foreach (var netStream in netSensor.Streams)
{
var stream = new SensorStream
{
Name = netStream.Name,
Unit = netStream.Unit
};
sensor.Streams.Add(netStream.NetId, stream);
foreach (var sample in netStream.Samples)
{
stream.Samples.Enqueue(sample);
}
}
}
Update();
}
public void ReceiveMessage(BoundUserInterfaceMessage message)
{
if (message is not IncrementalUIState incremental)
return;
foreach (var removed in incremental.RemovedSensors)
{
_sensorData.Remove(removed);
}
foreach (var netSensor in incremental.Sensors)
{
// TODO: Fuck this doesn't work if a sensor is added while the UI is open.
if (!_sensorData.TryGetValue(netSensor.NetId, out var sensor))
continue;
foreach (var netStream in netSensor.Streams)
{
// TODO: Fuck this doesn't work if a stream is added while the UI is open.
if (!sensor.Streams.TryGetValue(netStream.NetId, out var stream))
continue;
foreach (var (time, value) in netStream.Samples)
{
stream.Samples.Enqueue(new SensorSample(time + incremental.RelTime, value));
}
}
}
CullOldSamples();
Update();
}
private void Update()
{
Asdf.RemoveAllChildren();
var curTime = _gameTiming.CurTime;
var startTime = curTime - _retentionTime;
foreach (var sensor in _sensorData.Values)
{
var labelName = new Label { Text = sensor.Name, StyleClasses = { StyleBase.StyleClassLabelHeading } };
var labelAddress = new Label
{
Text = sensor.Address,
Margin = new Thickness(4, 0),
VerticalAlignment = VAlignment.Bottom,
StyleClasses = { StyleNano.StyleClassLabelSecondaryColor }
};
Asdf.AddChild(new BoxContainer
{
Orientation = BoxContainer.LayoutOrientation.Horizontal, Children =
{
labelName,
labelAddress
}
});
foreach (var stream in sensor.Streams.Values)
{
var maxValue = stream.Unit switch
{
SensorUnit.PressureKpa => 5000, // 5 MPa
SensorUnit.Ratio => 1,
SensorUnit.PowerW => 1_000_000, // 1 MW
SensorUnit.EnergyJ => 2_000_000, // 2 MJ
_ => 1000
};
// TODO: Better way to do this?
var lastSample = stream.Samples.Last();
Asdf.AddChild(new BoxContainer
{
Orientation = BoxContainer.LayoutOrientation.Horizontal,
Children =
{
new Label { Text = stream.Name, StyleClasses = { "monospace" }, HorizontalExpand = true },
new Label { Text = FormatValue(stream.Unit, lastSample.Value) }
}
});
Asdf.AddChild(new GraphView(stream.Samples, startTime, curTime, maxValue) { MinHeight = 150 });
Asdf.AddChild(new PanelContainer { StyleClasses = { StyleBase.ClassLowDivider } });
}
}
}
private string FormatValue(SensorUnit unit, float value)
{
return _loc.GetString(
"sensor-monitoring-value-display",
("unit", unit.ToString()),
("value", value));
}
private void CullOldSamples()
{
var startTime = _gameTiming.CurTime - _retentionTime;
foreach (var sensor in _sensorData.Values)
{
foreach (var stream in sensor.Streams.Values)
{
while (stream.Samples.TryPeek(out var sample) && sample.Time < startTime)
{
stream.Samples.Dequeue();
}
}
}
}
private sealed class SensorData
{
public string Name = "";
public string Address = "";
public SensorDeviceType DeviceType;
public readonly Dictionary<int, SensorStream> Streams = new();
}
private sealed class SensorStream
{
public string Name = "";
public SensorUnit Unit;
public readonly Queue<SensorSample> Samples = new();
}
private sealed class GraphView : Control
{
private readonly Queue<SensorSample> _samples;
private readonly TimeSpan _startTime;
private readonly TimeSpan _curTime;
private readonly float _maxY;
public GraphView(Queue<SensorSample> samples, TimeSpan startTime, TimeSpan curTime, float maxY)
{
_samples = samples;
_startTime = startTime;
_curTime = curTime;
_maxY = maxY;
RectClipContent = true;
}
protected override void Draw(DrawingHandleScreen handle)
{
base.Draw(handle);
var window = (float) (_curTime - _startTime).TotalSeconds;
// TODO: omg this is terrible don't fucking hardcode this size to something uncached huge omfg.
var vertices = new Vector2[25000];
var countVtx = 0;
var lastPoint = new Vector2(float.NaN, float.NaN);
foreach (var (time, sample) in _samples)
{
var relTime = (float) (time - _startTime).TotalSeconds;
var posY = PixelHeight - (sample / _maxY) * PixelHeight;
var posX = (relTime / window) * PixelWidth;
var newPoint = new Vector2(posX, posY);
if (float.IsFinite(lastPoint.X))
{
handle.DrawLine(lastPoint, newPoint, Color.White);
vertices[countVtx++] = lastPoint;
vertices[countVtx++] = lastPoint with { Y = PixelHeight };
vertices[countVtx++] = newPoint;
vertices[countVtx++] = newPoint;
vertices[countVtx++] = lastPoint with { Y = PixelHeight };
vertices[countVtx++] = newPoint with { Y = PixelHeight };
}
lastPoint = newPoint;
}
handle.DrawPrimitives(DrawPrimitiveTopology.TriangleList, vertices.AsSpan(0, countVtx), Color.White.WithAlpha(0.1f));
}
}
}
[UsedImplicitly]
public sealed class
SensorMonitoringConsoleBoundUserInterface : ComputerBoundUserInterface<SensorMonitoringWindow, ConsoleUIState>
{
public SensorMonitoringConsoleBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
}
}

View File

@@ -45,6 +45,7 @@ namespace Content.IntegrationTests.Tests
private static readonly string[] GameMaps = private static readonly string[] GameMaps =
{ {
"Dev", "Dev",
"TestTeg",
"Fland", "Fland",
"Meta", "Meta",
"Packed", "Packed",

View File

@@ -166,7 +166,7 @@ public sealed class AirAlarmSystem : EntitySystem
SubscribeLocalEvent<AirAlarmComponent, DeviceListUpdateEvent>(OnDeviceListUpdate); SubscribeLocalEvent<AirAlarmComponent, DeviceListUpdateEvent>(OnDeviceListUpdate);
SubscribeLocalEvent<AirAlarmComponent, BoundUIClosedEvent>(OnClose); SubscribeLocalEvent<AirAlarmComponent, BoundUIClosedEvent>(OnClose);
SubscribeLocalEvent<AirAlarmComponent, ComponentShutdown>(OnShutdown); SubscribeLocalEvent<AirAlarmComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<AirAlarmComponent, InteractHandEvent>(OnInteract); SubscribeLocalEvent<AirAlarmComponent, ActivateInWorldEvent>(OnActivate);
} }
private void OnDeviceListUpdate(EntityUid uid, AirAlarmComponent component, DeviceListUpdateEvent args) private void OnDeviceListUpdate(EntityUid uid, AirAlarmComponent component, DeviceListUpdateEvent args)
@@ -225,7 +225,7 @@ public sealed class AirAlarmSystem : EntitySystem
_activeUserInterfaces.Remove(uid); _activeUserInterfaces.Remove(uid);
} }
private void OnInteract(EntityUid uid, AirAlarmComponent component, InteractHandEvent args) private void OnActivate(EntityUid uid, AirAlarmComponent component, ActivateInWorldEvent args)
{ {
if (!_interactionSystem.InRangeUnobstructed(args.User, args.Target)) if (!_interactionSystem.InRangeUnobstructed(args.User, args.Target))
return; return;

View File

@@ -40,5 +40,8 @@ namespace Content.Server.Atmos.Piping.Binary.Components
[DataField("overclockThreshold")] [DataField("overclockThreshold")]
public float OverclockThreshold { get; set; } = 1000; public float OverclockThreshold { get; set; } = 1000;
[DataField("lastMolesTransferred")]
public float LastMolesTransferred;
} }
} }

View File

@@ -36,7 +36,7 @@ namespace Content.Server.Atmos.Piping.Binary.EntitySystems
SubscribeLocalEvent<GasPressurePumpComponent, AtmosDeviceUpdateEvent>(OnPumpUpdated); SubscribeLocalEvent<GasPressurePumpComponent, AtmosDeviceUpdateEvent>(OnPumpUpdated);
SubscribeLocalEvent<GasPressurePumpComponent, AtmosDeviceDisabledEvent>(OnPumpLeaveAtmosphere); SubscribeLocalEvent<GasPressurePumpComponent, AtmosDeviceDisabledEvent>(OnPumpLeaveAtmosphere);
SubscribeLocalEvent<GasPressurePumpComponent, ExaminedEvent>(OnExamined); SubscribeLocalEvent<GasPressurePumpComponent, ExaminedEvent>(OnExamined);
SubscribeLocalEvent<GasPressurePumpComponent, InteractHandEvent>(OnPumpInteractHand); SubscribeLocalEvent<GasPressurePumpComponent, ActivateInWorldEvent>(OnPumpActivate);
// Bound UI subscriptions // Bound UI subscriptions
SubscribeLocalEvent<GasPressurePumpComponent, GasPressurePumpChangeOutputPressureMessage>(OnOutputPressureChangeMessage); SubscribeLocalEvent<GasPressurePumpComponent, GasPressurePumpChangeOutputPressureMessage>(OnOutputPressureChangeMessage);
SubscribeLocalEvent<GasPressurePumpComponent, GasPressurePumpToggleStatusMessage>(OnToggleStatusMessage); SubscribeLocalEvent<GasPressurePumpComponent, GasPressurePumpToggleStatusMessage>(OnToggleStatusMessage);
@@ -99,7 +99,7 @@ namespace Content.Server.Atmos.Piping.Binary.EntitySystems
_userInterfaceSystem.TryCloseAll(uid, GasPressurePumpUiKey.Key); _userInterfaceSystem.TryCloseAll(uid, GasPressurePumpUiKey.Key);
} }
private void OnPumpInteractHand(EntityUid uid, GasPressurePumpComponent pump, InteractHandEvent args) private void OnPumpActivate(EntityUid uid, GasPressurePumpComponent pump, ActivateInWorldEvent args)
{ {
if (!EntityManager.TryGetComponent(args.User, out ActorComponent? actor)) if (!EntityManager.TryGetComponent(args.User, out ActorComponent? actor))
return; return;

View File

@@ -1,12 +1,17 @@
using Content.Server.Administration.Logs; using Content.Server.Administration.Logs;
using Content.Server.Atmos.EntitySystems; using Content.Server.Atmos.EntitySystems;
using Content.Server.Atmos.Monitor.Systems;
using Content.Server.Atmos.Piping.Binary.Components; using Content.Server.Atmos.Piping.Binary.Components;
using Content.Server.Atmos.Piping.Components; using Content.Server.Atmos.Piping.Components;
using Content.Server.DeviceNetwork;
using Content.Server.DeviceNetwork.Components;
using Content.Server.DeviceNetwork.Systems;
using Content.Server.NodeContainer; using Content.Server.NodeContainer;
using Content.Server.NodeContainer.EntitySystems; using Content.Server.NodeContainer.EntitySystems;
using Content.Server.NodeContainer.Nodes; using Content.Server.NodeContainer.Nodes;
using Content.Shared.Atmos.Piping; using Content.Shared.Atmos.Piping;
using Content.Shared.Atmos.Piping.Binary.Components; using Content.Shared.Atmos.Piping.Binary.Components;
using Content.Shared.Atmos.Piping.Unary.Components;
using Content.Shared.Audio; using Content.Shared.Audio;
using Content.Shared.Database; using Content.Shared.Database;
using Content.Shared.Examine; using Content.Shared.Examine;
@@ -29,6 +34,8 @@ namespace Content.Server.Atmos.Piping.Binary.EntitySystems
[Dependency] private readonly SharedAmbientSoundSystem _ambientSoundSystem = default!; [Dependency] private readonly SharedAmbientSoundSystem _ambientSoundSystem = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!; [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly NodeContainerSystem _nodeContainer = default!; [Dependency] private readonly NodeContainerSystem _nodeContainer = default!;
[Dependency] private readonly DeviceNetworkSystem _deviceNetwork = default!;
public override void Initialize() public override void Initialize()
{ {
@@ -38,10 +45,12 @@ namespace Content.Server.Atmos.Piping.Binary.EntitySystems
SubscribeLocalEvent<GasVolumePumpComponent, AtmosDeviceUpdateEvent>(OnVolumePumpUpdated); SubscribeLocalEvent<GasVolumePumpComponent, AtmosDeviceUpdateEvent>(OnVolumePumpUpdated);
SubscribeLocalEvent<GasVolumePumpComponent, AtmosDeviceDisabledEvent>(OnVolumePumpLeaveAtmosphere); SubscribeLocalEvent<GasVolumePumpComponent, AtmosDeviceDisabledEvent>(OnVolumePumpLeaveAtmosphere);
SubscribeLocalEvent<GasVolumePumpComponent, ExaminedEvent>(OnExamined); SubscribeLocalEvent<GasVolumePumpComponent, ExaminedEvent>(OnExamined);
SubscribeLocalEvent<GasVolumePumpComponent, InteractHandEvent>(OnPumpInteractHand); SubscribeLocalEvent<GasVolumePumpComponent, ActivateInWorldEvent>(OnPumpActivate);
// Bound UI subscriptions // Bound UI subscriptions
SubscribeLocalEvent<GasVolumePumpComponent, GasVolumePumpChangeTransferRateMessage>(OnTransferRateChangeMessage); SubscribeLocalEvent<GasVolumePumpComponent, GasVolumePumpChangeTransferRateMessage>(OnTransferRateChangeMessage);
SubscribeLocalEvent<GasVolumePumpComponent, GasVolumePumpToggleStatusMessage>(OnToggleStatusMessage); SubscribeLocalEvent<GasVolumePumpComponent, GasVolumePumpToggleStatusMessage>(OnToggleStatusMessage);
SubscribeLocalEvent<GasVolumePumpComponent, DeviceNetworkPacketEvent>(OnPacketRecv);
} }
private void OnInit(EntityUid uid, GasVolumePumpComponent pump, ComponentInit args) private void OnInit(EntityUid uid, GasVolumePumpComponent pump, ComponentInit args)
@@ -101,6 +110,8 @@ namespace Content.Server.Atmos.Piping.Binary.EntitySystems
} }
} }
pump.LastMolesTransferred = removed.TotalMoles;
_atmosphereSystem.Merge(outlet.Air, removed); _atmosphereSystem.Merge(outlet.Air, removed);
_ambientSoundSystem.SetAmbience(uid, removed.TotalMoles > 0f); _ambientSoundSystem.SetAmbience(uid, removed.TotalMoles > 0f);
} }
@@ -114,7 +125,7 @@ namespace Content.Server.Atmos.Piping.Binary.EntitySystems
_userInterfaceSystem.TryCloseAll(uid, GasVolumePumpUiKey.Key); _userInterfaceSystem.TryCloseAll(uid, GasVolumePumpUiKey.Key);
} }
private void OnPumpInteractHand(EntityUid uid, GasVolumePumpComponent pump, InteractHandEvent args) private void OnPumpActivate(EntityUid uid, GasVolumePumpComponent pump, ActivateInWorldEvent args)
{ {
if (!EntityManager.TryGetComponent(args.User, out ActorComponent? actor)) if (!EntityManager.TryGetComponent(args.User, out ActorComponent? actor))
return; return;
@@ -165,5 +176,24 @@ namespace Content.Server.Atmos.Piping.Binary.EntitySystems
_appearance.SetData(uid, PumpVisuals.Enabled, pump.Enabled, appearance); _appearance.SetData(uid, PumpVisuals.Enabled, pump.Enabled, appearance);
} }
private void OnPacketRecv(EntityUid uid, GasVolumePumpComponent component, DeviceNetworkPacketEvent args)
{
if (!TryComp(uid, out DeviceNetworkComponent? netConn)
|| !args.Data.TryGetValue(DeviceNetworkConstants.Command, out var cmd))
return;
var payload = new NetworkPayload();
switch (cmd)
{
case AtmosDeviceNetworkSystem.SyncData:
payload.Add(DeviceNetworkConstants.Command, AtmosDeviceNetworkSystem.SyncData);
payload.Add(AtmosDeviceNetworkSystem.SyncData, new GasVolumePumpData(component.LastMolesTransferred));
_deviceNetwork.QueuePacket(uid, args.SenderAddress, payload, device: netConn);
return;
}
}
} }
} }

View File

@@ -11,10 +11,6 @@ namespace Content.Server.Atmos.Piping.Unary.Components
[DataField("inlet")] [DataField("inlet")]
public string InletName = "pipe"; public string InletName = "pipe";
[ViewVariables(VVAccess.ReadWrite)]
[DataField("enabled")]
public bool Enabled = false;
/// <summary> /// <summary>
/// Current maximum temperature, calculated from <see cref="BaseHeatCapacity"/> and the quality of matter /// Current maximum temperature, calculated from <see cref="BaseHeatCapacity"/> and the quality of matter
/// bins. The heat capacity effectively determines the rate at which the thermo machine can add or remove /// bins. The heat capacity effectively determines the rate at which the thermo machine can add or remove
@@ -93,5 +89,11 @@ namespace Content.Server.Atmos.Piping.Unary.Components
[DataField("machinePartTemperature", customTypeSerializer: typeof(PrototypeIdSerializer<MachinePartPrototype>))] [DataField("machinePartTemperature", customTypeSerializer: typeof(PrototypeIdSerializer<MachinePartPrototype>))]
public string MachinePartTemperature = "Capacitor"; public string MachinePartTemperature = "Capacitor";
/// <summary>
/// Last amount of energy added/removed from the attached pipe network
/// </summary>
[DataField("lastEnergyDelta")]
[ViewVariables(VVAccess.ReadWrite)]
public float LastEnergyDelta;
} }
} }

View File

@@ -1,10 +1,15 @@
using Content.Server.Atmos.EntitySystems; using Content.Server.Atmos.EntitySystems;
using Content.Server.Atmos.Monitor.Systems;
using Content.Server.Atmos.Piping.Components; using Content.Server.Atmos.Piping.Components;
using Content.Server.Atmos.Piping.Unary.Components; using Content.Server.Atmos.Piping.Unary.Components;
using Content.Server.Construction; using Content.Server.Construction;
using Content.Server.DeviceNetwork;
using Content.Server.DeviceNetwork.Components;
using Content.Server.DeviceNetwork.Systems;
using Content.Server.NodeContainer; using Content.Server.NodeContainer;
using Content.Server.NodeContainer.EntitySystems; using Content.Server.NodeContainer.EntitySystems;
using Content.Server.NodeContainer.Nodes; using Content.Server.NodeContainer.Nodes;
using Content.Server.Power.Components;
using Content.Shared.Atmos; using Content.Shared.Atmos;
using Content.Shared.Atmos.Piping.Unary.Components; using Content.Shared.Atmos.Piping.Unary.Components;
using JetBrains.Annotations; using JetBrains.Annotations;
@@ -22,6 +27,8 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
[Dependency] private readonly SharedAppearanceSystem _appearance = default!; [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly PowerReceiverSystem _power = default!; [Dependency] private readonly PowerReceiverSystem _power = default!;
[Dependency] private readonly NodeContainerSystem _nodeContainer = default!; [Dependency] private readonly NodeContainerSystem _nodeContainer = default!;
[Dependency] private readonly DeviceNetworkSystem _deviceNetwork = default!;
public override void Initialize() public override void Initialize()
{ {
@@ -35,12 +42,15 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
// UI events // UI events
SubscribeLocalEvent<GasThermoMachineComponent, GasThermomachineToggleMessage>(OnToggleMessage); SubscribeLocalEvent<GasThermoMachineComponent, GasThermomachineToggleMessage>(OnToggleMessage);
SubscribeLocalEvent<GasThermoMachineComponent, GasThermomachineChangeTemperatureMessage>(OnChangeTemperature); SubscribeLocalEvent<GasThermoMachineComponent, GasThermomachineChangeTemperatureMessage>(OnChangeTemperature);
// Device network
SubscribeLocalEvent<GasThermoMachineComponent, DeviceNetworkPacketEvent>(OnPacketRecv);
} }
private void OnThermoMachineUpdated(EntityUid uid, GasThermoMachineComponent thermoMachine, AtmosDeviceUpdateEvent args) private void OnThermoMachineUpdated(EntityUid uid, GasThermoMachineComponent thermoMachine, AtmosDeviceUpdateEvent args)
{ {
if (!(thermoMachine.Enabled && _power.IsPowered(uid)) if (!(_power.IsPowered(uid))
|| !TryComp(uid, out NodeContainerComponent? nodeContainer) || !TryComp(uid, out NodeContainerComponent? nodeContainer)
|| !_nodeContainer.TryGetNode(nodeContainer, thermoMachine.InletName, out PipeNode? inlet)) || !_nodeContainer.TryGetNode(nodeContainer, thermoMachine.InletName, out PipeNode? inlet))
{ {
@@ -50,10 +60,14 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
var airHeatCapacity = _atmosphereSystem.GetHeatCapacity(inlet.Air); var airHeatCapacity = _atmosphereSystem.GetHeatCapacity(inlet.Air);
var combinedHeatCapacity = airHeatCapacity + thermoMachine.HeatCapacity; var combinedHeatCapacity = airHeatCapacity + thermoMachine.HeatCapacity;
var startEnergy = inlet.Air.Temperature * airHeatCapacity;
if (!MathHelper.CloseTo(combinedHeatCapacity, 0, 0.001f)) if (!MathHelper.CloseTo(combinedHeatCapacity, 0, 0.001f))
{ {
var combinedEnergy = thermoMachine.HeatCapacity * thermoMachine.TargetTemperature + airHeatCapacity * inlet.Air.Temperature; var combinedEnergy = thermoMachine.HeatCapacity * thermoMachine.TargetTemperature + airHeatCapacity * inlet.Air.Temperature;
inlet.Air.Temperature = combinedEnergy / combinedHeatCapacity; inlet.Air.Temperature = combinedEnergy / combinedHeatCapacity;
thermoMachine.LastEnergyDelta = inlet.Air.Temperature * airHeatCapacity - startEnergy;
} }
} }
@@ -98,7 +112,7 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
private void OnToggleMessage(EntityUid uid, GasThermoMachineComponent thermoMachine, GasThermomachineToggleMessage args) private void OnToggleMessage(EntityUid uid, GasThermoMachineComponent thermoMachine, GasThermomachineToggleMessage args)
{ {
SetEnabled(uid, thermoMachine, _power.TogglePower(uid)); _power.TogglePower(uid);
DirtyUI(uid, thermoMachine); DirtyUI(uid, thermoMachine);
} }
@@ -115,13 +129,12 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
if (!Resolve(uid, ref thermoMachine, ref ui, false)) if (!Resolve(uid, ref thermoMachine, ref ui, false))
return; return;
_userInterfaceSystem.TrySetUiState(uid, ThermomachineUiKey.Key, ApcPowerReceiverComponent? powerReceiver = null;
new GasThermomachineBoundUserInterfaceState(thermoMachine.MinTemperature, thermoMachine.MaxTemperature, thermoMachine.TargetTemperature, thermoMachine.Enabled, thermoMachine.Mode), null, ui); if (!Resolve(uid, ref powerReceiver))
} return;
private void SetEnabled(EntityUid uid, GasThermoMachineComponent thermoMachine, bool enabled) _userInterfaceSystem.TrySetUiState(uid, ThermomachineUiKey.Key,
{ new GasThermomachineBoundUserInterfaceState(thermoMachine.MinTemperature, thermoMachine.MaxTemperature, thermoMachine.TargetTemperature, !powerReceiver.PowerDisabled, thermoMachine.Mode), null, ui);
thermoMachine.Enabled = enabled;
} }
private void OnExamined(EntityUid uid, GasThermoMachineComponent thermoMachine, ExaminedEvent args) private void OnExamined(EntityUid uid, GasThermoMachineComponent thermoMachine, ExaminedEvent args)
@@ -137,5 +150,25 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
args.PushMarkup(str); args.PushMarkup(str);
} }
private void OnPacketRecv(EntityUid uid, GasThermoMachineComponent component, DeviceNetworkPacketEvent args)
{
if (!TryComp(uid, out DeviceNetworkComponent? netConn)
|| !args.Data.TryGetValue(DeviceNetworkConstants.Command, out var cmd))
return;
var payload = new NetworkPayload();
switch (cmd)
{
case AtmosDeviceNetworkSystem.SyncData:
payload.Add(DeviceNetworkConstants.Command, AtmosDeviceNetworkSystem.SyncData);
payload.Add(AtmosDeviceNetworkSystem.SyncData, new GasThermoMachineData(component.LastEnergyDelta));
_deviceNetwork.QueuePacket(uid, args.SenderAddress, payload, device: netConn);
return;
}
}
} }
} }

View File

@@ -438,6 +438,7 @@ namespace Content.Server.NodeContainer.EntitySystems
NodeGroupID.AMEngine => Color.Purple, NodeGroupID.AMEngine => Color.Purple,
NodeGroupID.Pipe => Color.Blue, NodeGroupID.Pipe => Color.Blue,
NodeGroupID.WireNet => Color.DarkMagenta, NodeGroupID.WireNet => Color.DarkMagenta,
NodeGroupID.Teg => Color.Red,
_ => Color.White _ => Color.White
}; };
} }

View File

@@ -1,4 +1,5 @@
using System.Reflection; using System.Reflection;
using Content.Server.Power.Generation.Teg;
using Robust.Shared.Reflection; using Robust.Shared.Reflection;
namespace Content.Server.NodeContainer.NodeGroups namespace Content.Server.NodeContainer.NodeGroups
@@ -61,5 +62,12 @@ namespace Content.Server.NodeContainer.NodeGroups
Pipe, Pipe,
WireNet, WireNet,
Spreader, Spreader,
/// <summary>
/// Group used by the TEG.
/// </summary>
/// <seealso cref="TegSystem"/>
/// <seealso cref="TegNodeGroup"/>
Teg,
} }
} }

View File

@@ -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;
}

View 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 &gt;= <see cref="MaxVisualPower"/> power.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)] [DataField("volume_max")]
public float VolumeMax = -4;
}

View 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;
}
}
}
}

View 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);
}

View 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);
}
}

View File

@@ -0,0 +1,32 @@
using Content.Server.Power.Components;
namespace Content.Server.SensorMonitoring;
/// <summary>
/// Enables a battery entity (such as an SMES) to be monitored via the sensor monitoring console.
/// </summary>
/// <remarks>
/// The entity should also have a <see cref="BatteryComponent"/> and <see cref="PowerNetworkBatteryComponent"/>.
/// </remarks>
[RegisterComponent]
public sealed class BatterySensorComponent : Component
{
}
/// <summary>
/// Device network data sent by a <see cref="BatterySensorComponent"/>.
/// </summary>
/// <param name="Charge">The current energy charge of the battery, in joules (J).</param>
/// <param name="MaxCharge">The maximum energy capacity of the battery, in joules (J).</param>
/// <param name="Receiving">The current amount of power being received by the battery, in watts (W).</param>
/// <param name="MaxReceiving">The maximum amount of power that can be received by the battery, in watts (W).</param>
/// <param name="Supplying">The current amount of power being supplied by the battery, in watts (W).</param>
/// <param name="MaxSupplying">The maximum amount of power that can be received by the battery, in watts (W).</param>
public sealed record BatterySensorData(
float Charge,
float MaxCharge,
float Receiving,
float MaxReceiving,
float Supplying,
float MaxSupplying
);

View File

@@ -0,0 +1,45 @@
using Content.Server.DeviceNetwork;
using Content.Server.DeviceNetwork.Systems;
using Content.Server.Power.Components;
namespace Content.Server.SensorMonitoring;
public sealed class BatterySensorSystem : EntitySystem
{
public const string DeviceNetworkCommandSyncData = "bat_sync_data";
[Dependency] private readonly DeviceNetworkSystem _deviceNetwork = default!;
public override void Initialize()
{
SubscribeLocalEvent<BatterySensorComponent, DeviceNetworkPacketEvent>(PacketReceived);
}
private void PacketReceived(EntityUid uid, BatterySensorComponent component, DeviceNetworkPacketEvent args)
{
if (!args.Data.TryGetValue(DeviceNetworkConstants.Command, out string? cmd))
return;
switch (cmd)
{
case DeviceNetworkCommandSyncData:
var battery = Comp<BatteryComponent>(uid);
var netBattery = Comp<PowerNetworkBatteryComponent>(uid);
var payload = new NetworkPayload
{
[DeviceNetworkConstants.Command] = DeviceNetworkCommandSyncData,
[DeviceNetworkCommandSyncData] = new BatterySensorData(
battery.Charge,
battery.MaxCharge,
netBattery.CurrentReceiving,
netBattery.MaxChargeRate,
netBattery.CurrentSupply,
netBattery.MaxSupply)
};
_deviceNetwork.QueuePacket(uid, args.SenderAddress, payload);
break;
}
}
}

View File

@@ -0,0 +1,64 @@
using Content.Shared.SensorMonitoring;
using Robust.Server.Player;
using Robust.Shared.Collections;
namespace Content.Server.SensorMonitoring;
[RegisterComponent]
public sealed class SensorMonitoringConsoleComponent : Component
{
/// <summary>
/// Used to assign network IDs for sensors and sensor streams.
/// </summary>
public int IdCounter;
/// <summary>
/// If enabled, additional data streams are shown intended to only be visible for debugging.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("debug_streams")]
public bool DebugStreams = false;
[ViewVariables(VVAccess.ReadWrite)]
public Dictionary<EntityUid, SensorData> Sensors = new();
[DataField("retentionTime")]
public TimeSpan RetentionTime = TimeSpan.FromMinutes(1);
// UI update tracking stuff.
public HashSet<IPlayerSession> InitialUIStateSent = new();
public TimeSpan LastUIUpdate;
public ValueList<int> RemovedSensors;
public sealed class SensorData
{
[ViewVariables(VVAccess.ReadWrite)]
public int NetId;
[ViewVariables(VVAccess.ReadWrite)]
public SensorDeviceType DeviceType;
[ViewVariables(VVAccess.ReadWrite)]
public Dictionary<string, SensorStream> Streams = new();
}
public sealed class SensorStream
{
[ViewVariables(VVAccess.ReadWrite)]
public int NetId;
[ViewVariables(VVAccess.ReadWrite)]
public SensorUnit Unit;
// Queue<T> is a ring buffer internally, and we can still iterate over it.
// I don't wanna write a ring buffer myself, so this is pretty convenient!
[ViewVariables]
public Queue<SensorSample> Samples = new();
}
public sealed class ViewingPlayer
{
}
}

View File

@@ -0,0 +1,138 @@
using Content.Server.DeviceNetwork.Components;
using Content.Shared.SensorMonitoring;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.Collections;
using ConsoleUIState = Content.Shared.SensorMonitoring.SensorMonitoringConsoleBoundInterfaceState;
using IncrementalUIState = Content.Shared.SensorMonitoring.SensorMonitoringIncrementalUpdate;
namespace Content.Server.SensorMonitoring;
public sealed partial class SensorMonitoringConsoleSystem
{
private void InitUI()
{
SubscribeLocalEvent<SensorMonitoringConsoleComponent, BoundUIClosedEvent>(ConsoleUIClosed);
}
private void UpdateConsoleUI(EntityUid uid, SensorMonitoringConsoleComponent comp)
{
if (!_userInterface.TryGetUi(uid, SensorMonitoringConsoleUiKey.Key, out var ui))
return;
if (ui.SubscribedSessions.Count == 0)
return;
ConsoleUIState? fullState = null;
SensorMonitoringIncrementalUpdate? incrementalUpdate = null;
foreach (var session in ui.SubscribedSessions)
{
if (comp.InitialUIStateSent.Contains(session))
{
incrementalUpdate ??= CalculateIncrementalUpdate();
_userInterface.TrySendUiMessage(ui, incrementalUpdate, session);
}
else
{
fullState ??= CalculateFullState();
UserInterfaceSystem.SetUiState(ui, fullState, session);
comp.InitialUIStateSent.Add(session);
}
}
comp.LastUIUpdate = _gameTiming.CurTime;
comp.RemovedSensors.Clear();
ConsoleUIState CalculateFullState()
{
var sensors = new ValueList<ConsoleUIState.SensorData>();
var streams = new ValueList<ConsoleUIState.SensorStream>();
foreach (var (ent, data) in comp.Sensors)
{
streams.Clear();
var name = MetaData(ent).EntityName;
var address = Comp<DeviceNetworkComponent>(ent).Address;
foreach (var (streamName, stream) in data.Streams)
{
streams.Add(new ConsoleUIState.SensorStream
{
NetId = stream.NetId,
Name = streamName,
Unit = stream.Unit,
Samples = stream.Samples.ToArray()
});
}
sensors.Add(new ConsoleUIState.SensorData
{
NetId = data.NetId,
Name = name,
Address = address,
DeviceType = data.DeviceType,
Streams = streams.ToArray()
});
}
return new ConsoleUIState
{
RetentionTime = comp.RetentionTime,
Sensors = sensors.ToArray()
};
}
SensorMonitoringIncrementalUpdate CalculateIncrementalUpdate()
{
var sensors = new ValueList<IncrementalUIState.SensorData>();
var streams = new ValueList<IncrementalUIState.SensorStream>();
var samples = new ValueList<SensorSample>();
foreach (var data in comp.Sensors.Values)
{
streams.Clear();
foreach (var stream in data.Streams.Values)
{
samples.Clear();
foreach (var (sampleTime, value) in stream.Samples)
{
if (sampleTime >= comp.LastUIUpdate)
samples.Add(new SensorSample(sampleTime - comp.LastUIUpdate, value));
}
streams.Add(new IncrementalUIState.SensorStream
{
NetId = stream.NetId,
Unit = stream.Unit,
Samples = samples.ToArray()
});
}
sensors.Add(new IncrementalUIState.SensorData { NetId = data.NetId, Streams = streams.ToArray() });
}
return new IncrementalUIState
{
RelTime = comp.LastUIUpdate,
RemovedSensors = comp.RemovedSensors.ToArray(),
Sensors = sensors.ToArray(),
};
}
}
private static void ConsoleUIClosed(
EntityUid uid,
SensorMonitoringConsoleComponent component,
BoundUIClosedEvent args)
{
if (!args.UiKey.Equals(SensorMonitoringConsoleUiKey.Key))
return;
if (args.Session is not IPlayerSession player)
return;
component.InitialUIStateSent.Remove(player);
}
}

View File

@@ -0,0 +1,329 @@
using Content.Server.Atmos.Monitor.Components;
using Content.Server.Atmos.Monitor.Systems;
using Content.Server.Atmos.Piping.Binary.Components;
using Content.Server.Atmos.Piping.Components;
using Content.Server.Atmos.Piping.Unary.Components;
using Content.Server.DeviceNetwork;
using Content.Server.DeviceNetwork.Components;
using Content.Server.DeviceNetwork.Systems;
using Content.Server.Power.Generation.Teg;
using Content.Shared.Atmos.Monitor;
using Content.Shared.Atmos.Piping.Binary.Components;
using Content.Shared.Atmos.Piping.Unary.Components;
using Content.Shared.DeviceNetwork.Components;
using Content.Shared.DeviceNetwork.Systems;
using Content.Shared.SensorMonitoring;
using Robust.Server.GameObjects;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using ConsoleUIState = Content.Shared.SensorMonitoring.SensorMonitoringConsoleBoundInterfaceState;
namespace Content.Server.SensorMonitoring;
public sealed partial class SensorMonitoringConsoleSystem : EntitySystem
{
// TODO: THIS THING IS HEAVILY WIP AND NOT READY FOR GENERAL USE BY PLAYERS.
// Some of the issues, off the top of my head:
// Way too huge network load when opened
// UI doesn't update properly in cases like adding new streams/devices
// Deleting connected devices causes exceptions
// UI sucks. need a way to make basic dashboards like Grafana, and save them.
private EntityQuery<DeviceNetworkComponent> _deviceNetworkQuery;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly DeviceNetworkSystem _deviceNetwork = default!;
[Dependency] private readonly UserInterfaceSystem _userInterface = default!;
public override void Initialize()
{
base.Initialize();
InitUI();
UpdatesBefore.Add(typeof(UserInterfaceSystem));
SubscribeLocalEvent<SensorMonitoringConsoleComponent, DeviceListUpdateEvent>(DeviceListUpdated);
SubscribeLocalEvent<SensorMonitoringConsoleComponent, ComponentStartup>(ConsoleStartup);
SubscribeLocalEvent<SensorMonitoringConsoleComponent, DeviceNetworkPacketEvent>(DevicePacketReceived);
SubscribeLocalEvent<SensorMonitoringConsoleComponent, AtmosDeviceUpdateEvent>(AtmosUpdate);
_deviceNetworkQuery = GetEntityQuery<DeviceNetworkComponent>();
}
public override void Update(float frameTime)
{
var consoles = EntityQueryEnumerator<SensorMonitoringConsoleComponent>();
while (consoles.MoveNext(out var entityUid, out var comp))
{
UpdateConsole(entityUid, comp);
}
}
private void UpdateConsole(EntityUid uid, SensorMonitoringConsoleComponent comp)
{
var minTime = _gameTiming.CurTime - comp.RetentionTime;
SensorUpdate(uid, comp);
foreach (var data in comp.Sensors.Values)
{
// Cull old data.
foreach (var stream in data.Streams.Values)
{
while (stream.Samples.TryPeek(out var sample) && sample.Time < minTime)
{
stream.Samples.Dequeue();
}
}
}
UpdateConsoleUI(uid, comp);
}
private void ConsoleStartup(EntityUid uid, SensorMonitoringConsoleComponent component, ComponentStartup args)
{
if (TryComp(uid, out DeviceListComponent? network))
UpdateDevices(uid, component, network.Devices, Array.Empty<EntityUid>());
}
private void DeviceListUpdated(
EntityUid uid,
SensorMonitoringConsoleComponent component,
DeviceListUpdateEvent args)
{
UpdateDevices(uid, component, args.Devices, args.OldDevices);
}
private void UpdateDevices(
EntityUid uid,
SensorMonitoringConsoleComponent component,
IEnumerable<EntityUid> newDevices,
IEnumerable<EntityUid> oldDevices)
{
var kept = new HashSet<EntityUid>();
foreach (var newDevice in newDevices)
{
var deviceType = DetectDeviceType(newDevice);
if (deviceType == SensorDeviceType.Unknown)
continue;
kept.Add(newDevice);
var sensor = component.Sensors.GetOrNew(newDevice);
sensor.DeviceType = deviceType;
if (sensor.NetId == 0)
sensor.NetId = MakeNetId(component);
}
foreach (var oldDevice in oldDevices)
{
if (kept.Contains(oldDevice))
continue;
if (component.Sensors.TryGetValue(oldDevice, out var sensorData))
{
component.RemovedSensors.Add(sensorData.NetId);
component.Sensors.Remove(oldDevice);
}
}
}
private SensorDeviceType DetectDeviceType(EntityUid entity)
{
if (HasComp<TegGeneratorComponent>(entity))
return SensorDeviceType.Teg;
if (HasComp<AtmosMonitorComponent>(entity))
return SensorDeviceType.AtmosSensor;
if (HasComp<GasThermoMachineComponent>(entity))
return SensorDeviceType.ThermoMachine;
if (HasComp<GasVolumePumpComponent>(entity))
return SensorDeviceType.VolumePump;
if (HasComp<BatterySensorComponent>(entity))
return SensorDeviceType.Battery;
return SensorDeviceType.Unknown;
}
private void DevicePacketReceived(EntityUid uid, SensorMonitoringConsoleComponent component,
DeviceNetworkPacketEvent args)
{
if (!component.Sensors.TryGetValue(args.Sender, out var sensorData))
return;
if (!args.Data.TryGetValue(DeviceNetworkConstants.Command, out string? command))
return;
switch (sensorData.DeviceType)
{
case SensorDeviceType.Teg:
if (command != TegSystem.DeviceNetworkCommandSyncData)
return;
if (!args.Data.TryGetValue(TegSystem.DeviceNetworkCommandSyncData, out TegSensorData? tegData))
return;
// @formatter:off
WriteSample(component, sensorData, "teg_last_generated", SensorUnit.EnergyJ, tegData.LastGeneration);
WriteSample(component, sensorData, "teg_power", SensorUnit.PowerW, tegData.PowerOutput);
if (component.DebugStreams)
WriteSample(component, sensorData, "teg_ramp_pos", SensorUnit.PowerW, tegData.RampPosition);
WriteSample(component, sensorData, "teg_circ_a_in_pressure", SensorUnit.PressureKpa, tegData.CirculatorA.InletPressure);
WriteSample(component, sensorData, "teg_circ_a_in_temperature", SensorUnit.TemperatureK, tegData.CirculatorA.InletTemperature);
WriteSample(component, sensorData, "teg_circ_a_out_pressure", SensorUnit.PressureKpa, tegData.CirculatorA.OutletPressure);
WriteSample(component, sensorData, "teg_circ_a_out_temperature", SensorUnit.TemperatureK, tegData.CirculatorA.OutletTemperature);
WriteSample(component, sensorData, "teg_circ_b_in_pressure", SensorUnit.PressureKpa, tegData.CirculatorB.InletPressure);
WriteSample(component, sensorData, "teg_circ_b_in_temperature", SensorUnit.TemperatureK, tegData.CirculatorB.InletTemperature);
WriteSample(component, sensorData, "teg_circ_b_out_pressure", SensorUnit.PressureKpa, tegData.CirculatorB.OutletPressure);
WriteSample(component, sensorData, "teg_circ_b_out_temperature", SensorUnit.TemperatureK, tegData.CirculatorB.OutletTemperature);
// @formatter:on
break;
case SensorDeviceType.AtmosSensor:
if (command != AtmosDeviceNetworkSystem.SyncData)
return;
if (!args.Data.TryGetValue(AtmosDeviceNetworkSystem.SyncData, out AtmosSensorData? atmosData))
return;
// @formatter:off
WriteSample(component, sensorData, "atmo_pressure", SensorUnit.PressureKpa, atmosData.Pressure);
WriteSample(component, sensorData, "atmo_temperature", SensorUnit.TemperatureK, atmosData.Temperature);
// @formatter:on
break;
case SensorDeviceType.ThermoMachine:
if (command != AtmosDeviceNetworkSystem.SyncData)
return;
if (!args.Data.TryGetValue(AtmosDeviceNetworkSystem.SyncData, out GasThermoMachineData? thermoData))
return;
// @formatter:off
WriteSample(component, sensorData, "abs_energy_delta", SensorUnit.EnergyJ, MathF.Abs(thermoData.EnergyDelta));
// @formatter:on
break;
case SensorDeviceType.VolumePump:
if (command != AtmosDeviceNetworkSystem.SyncData)
return;
if (!args.Data.TryGetValue(AtmosDeviceNetworkSystem.SyncData, out GasVolumePumpData? volumePumpData))
return;
// @formatter:off
WriteSample(component, sensorData, "moles_transferred", SensorUnit.Moles, volumePumpData.LastMolesTransferred);
// @formatter:on
break;
case SensorDeviceType.Battery:
if (command != BatterySensorSystem.DeviceNetworkCommandSyncData)
return;
if (!args.Data.TryGetValue(BatterySensorSystem.DeviceNetworkCommandSyncData, out BatterySensorData? batteryData))
return;
// @formatter:off
WriteSample(component, sensorData, "charge", SensorUnit.EnergyJ, batteryData.Charge);
WriteSample(component, sensorData, "charge_max", SensorUnit.EnergyJ, batteryData.MaxCharge);
WriteSample(component, sensorData, "receiving", SensorUnit.PowerW, batteryData.Receiving);
WriteSample(component, sensorData, "receiving_max", SensorUnit.PowerW, batteryData.MaxReceiving);
WriteSample(component, sensorData, "supplying", SensorUnit.PowerW, batteryData.Supplying);
WriteSample(component, sensorData, "supplying_max", SensorUnit.PowerW, batteryData.MaxSupplying);
// @formatter:on
break;
}
}
private void WriteSample(
SensorMonitoringConsoleComponent component,
SensorMonitoringConsoleComponent.SensorData sensorData,
string streamName,
SensorUnit unit,
float value)
{
var stream = sensorData.Streams.GetOrNew(streamName);
stream.Unit = unit;
if (stream.NetId == 0)
stream.NetId = MakeNetId(component);
var time = _gameTiming.CurTime;
stream.Samples.Enqueue(new SensorSample(time, value));
}
private static int MakeNetId(SensorMonitoringConsoleComponent component)
{
return ++component.IdCounter;
}
private void AtmosUpdate(
EntityUid uid,
SensorMonitoringConsoleComponent comp,
AtmosDeviceUpdateEvent args)
{
foreach (var (ent, data) in comp.Sensors)
{
// Send network requests for new data!
NetworkPayload payload;
switch (data.DeviceType)
{
case SensorDeviceType.Teg:
payload = new NetworkPayload
{
[DeviceNetworkConstants.Command] = TegSystem.DeviceNetworkCommandSyncData
};
break;
case SensorDeviceType.AtmosSensor:
case SensorDeviceType.ThermoMachine:
case SensorDeviceType.VolumePump:
payload = new NetworkPayload
{
[DeviceNetworkConstants.Command] = AtmosDeviceNetworkSystem.SyncData
};
break;
default:
// Unknown device type, don't do anything.
continue;
}
var address = _deviceNetworkQuery.GetComponent(ent);
_deviceNetwork.QueuePacket(uid, address.Address, payload);
}
}
private void SensorUpdate(EntityUid uid, SensorMonitoringConsoleComponent comp)
{
foreach (var (ent, data) in comp.Sensors)
{
// Send network requests for new data!
NetworkPayload payload;
switch (data.DeviceType)
{
case SensorDeviceType.Battery:
payload = new NetworkPayload
{
[DeviceNetworkConstants.Command] = BatterySensorSystem.DeviceNetworkCommandSyncData
};
break;
default:
// Unknown device type, don't do anything.
continue;
}
var address = _deviceNetworkQuery.GetComponent(ent);
_deviceNetwork.QueuePacket(uid, address.Address, payload);
}
}
}

View File

@@ -2,6 +2,8 @@
namespace Content.Shared.Atmos.Piping.Binary.Components namespace Content.Shared.Atmos.Piping.Binary.Components
{ {
public sealed record GasVolumePumpData(float LastMolesTransferred);
[Serializable, NetSerializable] [Serializable, NetSerializable]
public enum GasVolumePumpUiKey public enum GasVolumePumpUiKey
{ {

View File

@@ -2,6 +2,9 @@
namespace Content.Shared.Atmos.Piping.Unary.Components; namespace Content.Shared.Atmos.Piping.Unary.Components;
[Serializable, NetSerializable]
public sealed record GasThermoMachineData(float EnergyDelta);
[Serializable] [Serializable]
[NetSerializable] [NetSerializable]
public enum ThermomachineUiKey public enum ThermomachineUiKey

View File

@@ -0,0 +1,36 @@
using Robust.Shared.Serialization;
namespace Content.Shared.Power.Generation.Teg;
/// <summary>
/// Appearance keys for the TEG &amp; its circulators.
/// </summary>
[Serializable, NetSerializable]
public enum TegVisuals
{
PowerOutput,
CirculatorSpeed,
CirculatorPower,
}
/// <summary>
/// Visual sprite layers for the TEG &amp; its circulators.
/// </summary>
[Serializable, NetSerializable]
public enum TegVisualLayers
{
PowerOutput,
CirculatorBase,
CirculatorLight
}
/// <summary>
/// Visual speed levels for the TEG circulators.
/// </summary>
[Serializable, NetSerializable]
public enum TegCirculatorSpeed
{
SpeedStill,
SpeedSlow,
SpeedFast
}

View File

@@ -0,0 +1,112 @@
using Robust.Shared.Serialization;
using ConsoleUIState = Content.Shared.SensorMonitoring.SensorMonitoringConsoleBoundInterfaceState;
namespace Content.Shared.SensorMonitoring;
[Serializable, NetSerializable]
public sealed class SensorMonitoringConsoleBoundInterfaceState : BoundUserInterfaceState
{
public TimeSpan RetentionTime;
public SensorData[] Sensors = Array.Empty<SensorData>();
[Serializable, NetSerializable]
public sealed class SensorData
{
public int NetId;
public string Name = "";
public string Address = "";
public SensorDeviceType DeviceType;
public SensorStream[] Streams = Array.Empty<SensorStream>();
}
[Serializable, NetSerializable]
public sealed class SensorStream
{
public int NetId;
public string Name = "";
public SensorUnit Unit;
public SensorSample[] Samples = Array.Empty<SensorSample>();
}
}
[Serializable, NetSerializable]
public sealed class SensorMonitoringIncrementalUpdate : BoundUserInterfaceMessage
{
public TimeSpan RelTime;
public SensorData[] Sensors = Array.Empty<SensorData>();
public int[] RemovedSensors = Array.Empty<int>();
[Serializable, NetSerializable]
public sealed class SensorData
{
public int NetId;
public SensorStream[] Streams = Array.Empty<SensorStream>();
}
[Serializable, NetSerializable]
public sealed class SensorStream
{
public int NetId;
public SensorUnit Unit;
// Note: these samples have their time values relative to RelTime.
// This improves effectiveness of integer compression in NetSerializer.
public SensorSample[] Samples = Array.Empty<SensorSample>();
}
}
[Serializable, NetSerializable]
public enum SensorMonitoringConsoleUiKey
{
Key
}
[Serializable, NetSerializable]
public enum SensorUnit : byte
{
Undetermined = 0,
/// <summary>
/// A pressure value in kilopascals (kPa).
/// </summary>
PressureKpa,
/// <summary>
/// A temperature value in Kelvin (K).
/// </summary>
TemperatureK,
/// <summary>
/// An amount of matter in moles.
/// </summary>
Moles,
/// <summary>
/// A value in the range 0-1.
/// </summary>
/* L + */ Ratio,
/// <summary>
/// Power in Watts (W).
/// </summary>
PowerW,
/// <summary>
/// Energy in Joules (J).
/// </summary>
EnergyJ
}
[Serializable, NetSerializable]
public enum SensorDeviceType
{
Unknown = 0,
Teg,
AtmosSensor,
ThermoMachine,
VolumePump,
Battery,
}
[Serializable, NetSerializable]
public record struct SensorSample(TimeSpan Time, float Value);

View File

@@ -24,12 +24,20 @@ device-frequency-prototype-name-surveillance-camera-entertainment = Entertainmen
device-address-prefix-vent = VNT- device-address-prefix-vent = VNT-
device-address-prefix-scrubber = SCR- device-address-prefix-scrubber = SCR-
device-address-prefix-sensor = SNS- device-address-prefix-sensor = SNS-
# Damn bet you couldn't see this one coming.
device-address-prefix-teg = TEG-
device-address-prefix-heater = HTR-
device-address-prefix-freezer = FZR-
device-address-prefix-volume-pump = VPP-
device-address-prefix-smes = SMS-
#PDAs and terminals #PDAs and terminals
device-address-prefix-console = CLS- device-address-prefix-console = CLS-
device-address-prefix-fire-alarm = FIR- device-address-prefix-fire-alarm = FIR-
device-address-prefix-air-alarm = AIR- device-address-prefix-air-alarm = AIR-
device-address-prefix-sensor-monitor = MON-
device-address-examine-message = The device's address is {$address}. device-address-examine-message = The device's address is {$address}.
#Device net ID names #Device net ID names
@@ -39,3 +47,4 @@ device-net-id-wireless = Wireless
device-net-id-apc = Apc device-net-id-apc = Apc
device-net-id-atmos-devices = Atmos Devices device-net-id-atmos-devices = Atmos Devices
device-net-id-reserved = Reserved device-net-id-reserved = Reserved

View File

@@ -9,6 +9,7 @@ guide-entry-network-configurator = Network Configurator
guide-entry-power = Power guide-entry-power = Power
guide-entry-ame = Antimatter Engine (AME) guide-entry-ame = Antimatter Engine (AME)
guide-entry-singularity = Singularity guide-entry-singularity = Singularity
guide-entry-teg = Thermo-electric Generator (TEG)
guide-entry-controls = Controls guide-entry-controls = Controls
guide-entry-radio = Radio guide-entry-radio = Radio
guide-entry-jobs = Jobs guide-entry-jobs = Jobs

View File

@@ -0,0 +1,2 @@
teg-generator-examine-power = It's generating [color=yellow]{ POWERWATTS($power) }[/color].
teg-generator-examine-connection = To function, a [color=white]circulator[/color] must be attached on both sides.

View File

@@ -0,0 +1,13 @@
sensor-monitoring-window-title = Sensor Monitoring Console
sensor-monitoring-value-display = {$unit ->
[PressureKpa] { PRESSURE($value) }
[PowerW] { POWERWATTS($value) }
[EnergyJ] { POWERJOULES($value) }
[TemperatureK] { TOSTRING($value, "N3") } K
[Ratio] { NATURALPERCENT($value) }
[Moles] { TOSTRING($value, "N3") } mol
*[Other] { $value }
}
# ({ TOSTRING(SUB($value, 273.15), "N3") } °C)

File diff suppressed because it is too large Load Diff

View File

@@ -377,3 +377,14 @@
price: 150 price: 150
- type: ComputerBoard - type: ComputerBoard
prototype: ComputerMassMedia prototype: ComputerMassMedia
- type: entity
parent: BaseComputerCircuitboard
id: SensorConsoleCircuitboard
name: sensor monitoring console board
description: A computer printed circuit board for a sensor monitoring console.
components:
- type: Sprite
state: cpu_engineering
- type: ComputerBoard
prototype: ComputerSensorMonitoring

View File

@@ -0,0 +1,11 @@
# Station prototype for cut-down test maps that don't need all the infrastructure.
- type: entity
id: TestStation
parent:
- BaseStation
- BaseStationJobsSpawning
- BaseStationRecords
- BaseStationAlertLevels
noSpawn: true
components:
- type: Transform

View File

@@ -972,3 +972,46 @@
interfaces: interfaces:
- key: enum.NewsWriteUiKey.Key - key: enum.NewsWriteUiKey.Key
type: NewsWriteBoundUserInterface type: NewsWriteBoundUserInterface
- type: entity
parent: BaseComputer
id: ComputerSensorMonitoring
name: sensor monitoring computer
description: A flexible console for monitoring all kinds of sensors.
# Putting this as "DO NOT MAP" until the performance issues are fixed.
# And it's more fleshed out.
suffix: TESTING, DO NOT MAP
components:
- type: Sprite
layers:
- map: ["computerLayerBody"]
state: computer
- map: ["computerLayerKeyboard"]
state: generic_keyboard
- map: ["computerLayerScreen"]
state: sensors
- map: ["computerLayerKeys"]
state: atmos_key
- type: PointLight
radius: 1.5
energy: 1.6
color: "#43ccb5"
- type: Computer
board: SensorConsoleCircuitboard
- type: SensorMonitoringConsole
- type: ActivatableUI
key: enum.SensorMonitoringConsoleUiKey.Key
- type: UserInterface
interfaces:
- key: enum.SensorMonitoringConsoleUiKey.Key
type: SensorMonitoringConsoleBoundUserInterface
- type: DeviceNetwork
deviceNetId: AtmosDevices
receiveFrequencyId: AtmosMonitor
transmitFrequencyId: AtmosMonitor
prefix: device-address-prefix-sensor-monitor
sendBroadcastAttemptEvent: true
examinableAddress: true
- type: WiredNetworkConnection
- type: DeviceList
- type: AtmosDevice

View File

@@ -102,6 +102,14 @@
range: 5 range: 5
sound: sound:
path: /Audio/Ambience/Objects/gas_pump.ogg path: /Audio/Ambience/Objects/gas_pump.ogg
- type: DeviceNetwork
deviceNetId: AtmosDevices
receiveFrequencyId: AtmosMonitor
transmitFrequencyId: AtmosMonitor
sendBroadcastAttemptEvent: true
examinableAddress: true
prefix: device-address-prefix-volume-pump
- type: WiredNetworkConnection
- type: entity - type: entity
parent: GasBinaryBase parent: GasBinaryBase

View File

@@ -250,6 +250,14 @@
pipeDirection: South pipeDirection: South
- type: Transform - type: Transform
noRot: false noRot: false
- type: DeviceNetwork
deviceNetId: AtmosDevices
receiveFrequencyId: AtmosMonitor
transmitFrequencyId: AtmosMonitor
sendBroadcastAttemptEvent: true
examinableAddress: true
- type: WiredNetworkConnection
- type: PowerSwitch
- type: entity - type: entity
parent: BaseGasThermoMachine parent: BaseGasThermoMachine
@@ -281,6 +289,8 @@
powerDisabled: true #starts off powerDisabled: true #starts off
- type: Machine - type: Machine
board: ThermomachineFreezerMachineCircuitBoard board: ThermomachineFreezerMachineCircuitBoard
- type: DeviceNetwork
prefix: device-address-prefix-freezer
- type: entity - type: entity
parent: GasThermoMachineFreezer parent: GasThermoMachineFreezer
@@ -322,6 +332,8 @@
powerDisabled: true #starts off powerDisabled: true #starts off
- type: Machine - type: Machine
board: ThermomachineHeaterMachineCircuitBoard board: ThermomachineHeaterMachineCircuitBoard
- type: DeviceNetwork
prefix: device-address-prefix-heater
- type: entity - type: entity
parent: GasThermoMachineHeater parent: GasThermoMachineHeater

View File

@@ -0,0 +1,189 @@
- type: entity
id: TegCenter
name: thermo-electric generator
description: A high efficiency generator that uses energy transfer between hot and cold gases to produce electricity.
parent: BaseMachinePowered
placement:
mode: SnapgridCenter
components:
- type: Transform
noRot: false
- type: Sprite
sprite: Structures/Power/Generation/teg.rsi
layers:
- state: teg
- state: teg_mid
shader: unshaded
map: [ "enum.PowerDeviceVisualLayers.Powered" ]
- state: teg-op1
shader: unshaded
visible: false
map: [ "enum.TegVisualLayers.PowerOutput" ]
- type: GenericVisualizer
visuals:
enum.PowerDeviceVisuals.Powered:
enum.PowerDeviceVisualLayers.Powered:
True: { visible: true }
False: { visible: false }
enum.TegVisuals.PowerOutput:
enum.TegVisualLayers.PowerOutput:
0: { visible: false }
1: { visible: true, state: teg-op1 }
2: { visible: true, state: teg-op2 }
3: { visible: true, state: teg-op3 }
4: { visible: true, state: teg-op4 }
5: { visible: true, state: teg-op5 }
6: { visible: true, state: teg-op6 }
7: { visible: true, state: teg-op7 }
8: { visible: true, state: teg-op8 }
9: { visible: true, state: teg-op9 }
10: { visible: true, state: teg-op10 }
11: { visible: true, state: teg-op11 }
- type: Anchorable
- type: Pullable
- type: NodeContainer
examinable: true
nodes:
output:
!type:CableDeviceNode
nodeGroupID: HVPower
teg:
!type:TegNodeGenerator
nodeGroupID: Teg
- type: Rotatable
# Note that only the TEG center is an AtmosDevice.
# It fires processing on behalf of its connected circulators.
- type: AtmosDevice
- type: TegGenerator
- type: DeviceNetwork
deviceNetId: AtmosDevices
receiveFrequencyId: AtmosMonitor
transmitFrequencyId: AtmosMonitor
prefix: device-address-prefix-teg
sendBroadcastAttemptEvent: true
examinableAddress: true
- type: WiredNetworkConnection
- type: PowerSupplier
# Have practically irrelevant supply ramping.
# Ramping is effectively implemented by the TEG manually,
# due to how power output is handled.
supplyRampRate: 100000000
supplyRampTolerance: 10000000000
- type: Appearance
- type: ApcPowerReceiver
powerLoad: 1000
- type: LitOnPowered
- type: PointLight
enabled: false
castShadows: false
radius: 1.5
color: "#FFAA00"
- type: AmbientSound
volume: -4
range: 6
enabled: false
sound:
path: /Audio/Ambience/Objects/vending_machine_hum.ogg
- type: entity
id: TegCirculator
name: circulator
description: Passes gas through the thermo-electric generator to exchange heat. Has an inlet and outlet port.
parent: BaseMachine
placement:
mode: SnapgridCenter
components:
- type: Transform
noRot: false
# visuals
- type: Sprite
sprite: Structures/Power/Generation/teg.rsi
layers:
- state: circ-0
map: [ "enum.TegVisualLayers.CirculatorBase" ]
- state: circ-0-light
shader: unshaded
map: [ "enum.TegVisualLayers.CirculatorLight" ]
- type: GenericVisualizer
visuals:
enum.TegVisuals.CirculatorPower:
enum.TegVisualLayers.CirculatorLight:
True: { visible: true }
False: { visible: false }
enum.TegVisuals.CirculatorSpeed:
enum.TegVisualLayers.CirculatorBase:
SpeedStill: { state: circ-0 }
SpeedSlow: { state: circ-1 }
SpeedFast: { state: circ-2 }
enum.TegVisualLayers.CirculatorLight:
SpeedStill: { state: circ-0-light }
SpeedSlow: { state: circ-1-light }
SpeedFast: { state: circ-2-light }
- type: Appearance
- type: PointLight
enabled: false
castShadows: false
radius: 1.5
color: "#CC00FF"
# tags
- type: Tag
tags:
- Pipe
- Unstackable
# basic interactions
- type: Rotatable
- type: Anchorable
- type: Pullable
# functionality
- type: NodeContainer
nodes:
inlet:
!type:PipeNode
nodeGroupID: Pipe
pipeDirection: North
volume: 100
outlet:
!type:PipeNode
nodeGroupID: Pipe
pipeDirection: South
volume: 100
circulator:
!type:TegNodeCirculator
nodeGroupID: Teg
- type: AtmosUnsafeUnanchor
- type: TegCirculator
light_color_fast: '#AA00FF'
light_color_slow: '#FF3300'
- # Spawned by the client-side circulator examine code to indicate the inlet/outlet direction.
type: entity
id: TegCirculatorArrow
noSpawn: true
components:
- type: Sprite
sprite: Markers/teg_arrow.rsi
color: "#FFFFFFBB"
layers:
- state: arrow
offset: 0, 0.75
- state: arrow
offset: 0, -0.75
- type: TimedDespawn
lifetime: 2
- type: Tag
tags:
- HideContextMenu

View File

@@ -78,6 +78,15 @@
- type: Damageable - type: Damageable
damageContainer: Inorganic damageContainer: Inorganic
damageModifierSet: StrongMetallic damageModifierSet: StrongMetallic
- type: BatterySensor
- type: DeviceNetwork
deviceNetId: AtmosDevices
receiveFrequencyId: AtmosMonitor
transmitFrequencyId: AtmosMonitor
prefix: device-address-prefix-smes
sendBroadcastAttemptEvent: true
examinableAddress: true
- type: WiredNetworkConnection
# SMES' in use # SMES' in use

View File

@@ -51,6 +51,7 @@
children: children:
- AME - AME
- Singularity - Singularity
- TEG
- type: guideEntry - type: guideEntry
id: AME id: AME
@@ -61,3 +62,8 @@
id: Singularity id: Singularity
name: guide-entry-singularity name: guide-entry-singularity
text: "/ServerInfo/Guidebook/Engineering/Singularity.xml" text: "/ServerInfo/Guidebook/Engineering/Singularity.xml"
- type: guideEntry
id: TEG
name: guide-entry-teg
text: "/ServerInfo/Guidebook/Engineering/TEG.xml"

View File

@@ -31,3 +31,20 @@
- Captain - Captain
availableJobs: availableJobs:
Captain: [ -1, -1 ] Captain: [ -1, -1 ]
- type: gameMap
id: TestTeg
mapName: Test TEG
mapPath: /Maps/Test/test_teg.yml
minPlayers: 0
stations:
TEG:
stationProto: TestStation
components:
- type: StationNameSetup
mapNameTemplate: "TEG"
- type: StationJobs
overflowJobs:
- ChiefEngineer
availableJobs:
ChiefEngineer: [ -1, -1 ]

View File

@@ -0,0 +1,39 @@
<Document>
# Thermo-electric Engine (TEG)
The TEG generates power by exchanging heat between hot and cold gases. On station, hot gas is usually created by burning plasma, and an array of heat-exchanging pipes in space radiates away heat to make a cold side.
The TEG relies heavily on atmospherics piping. The only truly special component about it is the generator core, the rest is all off-the-shelf atmospherics equipment. Note that while the exact layout may vary significantly depending on station, the general components and setup are usually the same.
## Generator
The main generator itself is a machine made up of multiple parts: the core generator and two "circulators", in this arrangement:
<Box>
<GuideEntityEmbed Entity="TegCirculator" Rotation="180" Caption="" Margin="0" />
<GuideEntityEmbed Entity="TegCenter" Rotation="90" Caption="" Margin="0" />
<GuideEntityEmbed Entity="TegCirculator" Caption="" Margin="0" />
</Box>
The circulators take in either a hot or cold gas, and pass it through the machine to exchange heat. The gas then gets output on the other end of the circulator. The generator produces the actual power and outputs it over an HV wire.
Note that the circulators are [color=#a4885c]directional[/color]: they will only let gas through one way. You can see this direction in-game by examining the circulator itself. A pressure difference is required across the input and output, so pumps are generally provided and must be turned on.
There is no preference for which side must be hot or cold, there need only be a difference in temperature between them. The gases in the two "loops" are never mixed, only energy is exchanged between them. The hot side will cool down, the cold side will heat up.
## Burn Chamber
As I'm sure a wise person once said: the best way to make something hot is to light it on fire. Well, depending on context that may not be very wise, but luckily your engineering department has just what's needed to do it wisely after all.
TODO: somebody fill this out once we settle on a general mapped layout for burn chambers.
## Coolant Array
A whole bunch of heat-exchanger piping in space. There's not much to say here: gas goes through and cools down.
## The Pipes
TODO: somebody fill this out once we settle on a general mapped layout for burn chambers.
</Document>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,15 @@
{
"version": 1,
"license": "CC-BY-SA-3.0",
"copyright": "Made by PJB3005",
"size": {
"x": 32,
"y": 32
},
"states": [
{
"name": "arrow",
"delays": [[0.033, 0.033, 0.033, 0.033, 0.033, 0.033, 0.033, 0.033, 0.033, 0.033, 0.033, 0.033, 0.033, 0.033, 0.033, 0.033, 0.033]]
}
]
}

View File

@@ -1460,6 +1460,10 @@
"name": "security_key_off", "name": "security_key_off",
"directions": 4 "directions": 4
}, },
{
"name": "sensors",
"directions": 4
},
{ {
"name": "shuttle", "name": "shuttle",
"directions": 4, "directions": 4,

Binary file not shown.

After

Width:  |  Height:  |  Size: 685 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 346 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 614 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

@@ -0,0 +1,279 @@
{
"version": 1,
"license": "CC-BY-SA-3.0",
"copyright": "Taken from https://github.com/Baystation12/Baystation12/blob/fc2196fa74492570e5abb847085afca0e53f4ea8/icons/obj/power.dmi. Modified to split light layers",
"size": {
"x": 32,
"y": 32
},
"states": [
{
"name": "teg",
"directions": 4
},
{
"name": "teg-op1",
"directions": 4
},
{
"name": "teg-op2",
"directions": 4
},
{
"name": "teg-op3",
"directions": 4
},
{
"name": "teg-op4",
"directions": 4
},
{
"name": "teg-op5",
"directions": 4
},
{
"name": "teg-op6",
"directions": 4
},
{
"name": "teg-op7",
"directions": 4
},
{
"name": "teg-op8",
"directions": 4
},
{
"name": "teg-op9",
"directions": 4
},
{
"name": "teg-op10",
"directions": 4
},
{
"name": "teg-op11",
"directions": 4
},
{
"name": "teg_mid",
"directions": 4,
"delays": [
[
0.1,
0.1,
0.1,
0.1
],
[
0.1,
0.1,
0.1,
0.1
],
[
0.1,
0.1,
0.1,
0.1
],
[
0.1,
0.1,
0.1,
0.1
]
]
},
{
"name": "teg-oc00"
},
{
"name": "teg-oc10",
"delays": [
[
0.3,
0.3
]
]
},
{
"name": "teg-oc01",
"delays": [
[
0.3,
0.3
]
]
},
{
"name": "teg-oc11",
"delays": [
[
0.3,
0.3
]
]
},
{
"name": "circ-2",
"directions": 4,
"delays": [
[
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1
],
[
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1
],
[
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1
],
[
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1
]
]
},
{
"name": "circ-2-light",
"directions": 4,
"delays": [
[
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1
],
[
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1
],
[
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1
],
[
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1
]
]
},
{
"name": "circ-1",
"directions": 4,
"delays": [
[
0.2,
0.2,
0.2,
0.2
],
[
0.2,
0.2,
0.2,
0.2
],
[
0.2,
0.2,
0.2,
0.2
],
[
0.2,
0.2,
0.2,
0.2
]
]
},
{
"name": "circ-1-light",
"directions": 4,
"delays": [
[
0.2,
0.2,
0.2,
0.2
],
[
0.2,
0.2,
0.2,
0.2
],
[
0.2,
0.2,
0.2,
0.2
],
[
0.2,
0.2,
0.2,
0.2
]
]
},
{
"name": "circ-0",
"directions": 4
},
{
"name": "circ-0-light",
"directions": 4
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 297 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 303 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 371 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 449 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 445 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 352 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 366 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 375 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 376 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 422 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 427 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 425 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -502,6 +502,7 @@ public sealed class $CLASS$ : Shared$CLASS$ {
<s:Boolean x:Key="/Default/UserDictionary/Words/=cellslot/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=cellslot/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=CentCom/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=CentCom/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Charlieson/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=Charlieson/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=circulator/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Clonexadone/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=Clonexadone/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Collidable/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=Collidable/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Collidables/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=Collidables/@EntryIndexedValue">True</s:Boolean>