Merge main and stable back together
This commit is contained in:
37
.github/CODEOWNERS
vendored
37
.github/CODEOWNERS
vendored
@@ -2,49 +2,30 @@
|
||||
|
||||
# Sorting by path instead of by who added it one day :(
|
||||
# this isn't how codeowners rules work pls read the first comment instead of trying to force a sorting order
|
||||
/Resources/ConfigPresets/WizardsDen/ @Chief-Engineer
|
||||
|
||||
# Moony's Gargantuan List Of Things She Cares About, or MGLOTSCA for short.
|
||||
# You need to add your name to these entries, not make a new one, if you care about them.
|
||||
/Content.*/Toolshed/ @moonheart08
|
||||
**/Toolshed/** @moonheart08
|
||||
*Command.cs @moonheart08
|
||||
/Content.*/Administration/ @moonheart08 @DrSmugleaf @Chief-Engineer
|
||||
/Content.*/Station/ @moonheart08
|
||||
/Content.*/Maps/ @moonheart08
|
||||
/Content.*/GameTicking/ @moonheart08 @EmoGarbage404
|
||||
/Resources/ServerInfo/ @moonheart08 @Chief-Engineer
|
||||
/Resources/ServerInfo/Guidebook/ @moonheart08 @EmoGarbage404
|
||||
/Resources/ServerInfo/Guidebook/ServerRules/ @Chief-Engineer
|
||||
/Resources/engineCommandPerms.yml @moonheart08 @Chief-Engineer
|
||||
/Resources/clientCommandPerms.yml @moonheart08 @Chief-Engineer
|
||||
/Resources/ConfigPresets/WizardsDen/ @nikthechampiongr
|
||||
/Content.*/Administration/ @DrSmugleaf @nikthechampiongr
|
||||
/Resources/ServerInfo/ @nikthechampiongr
|
||||
/Resources/ServerInfo/Guidebook/ServerRules/ @nikthechampiongr
|
||||
|
||||
/Resources/Prototypes/Maps/** @Emisse
|
||||
|
||||
/Resources/Prototypes/Body/ @DrSmugleaf # suffering
|
||||
/Resources/Prototypes/Entities/Mobs/Player/ @DrSmugleaf
|
||||
/Resources/Prototypes/Entities/Mobs/Species/ @DrSmugleaf
|
||||
/Resources/Prototypes/Guidebook/rules.yml @Chief-Engineer
|
||||
/Resources/Prototypes/Guidebook/rules.yml @nikthechampiongr
|
||||
/Content.*/Body/ @DrSmugleaf
|
||||
/Content.YAMLLinter @DrSmugleaf
|
||||
/Content.Shared/Damage/ @DrSmugleaf
|
||||
|
||||
/Content.*/Anomaly/ @EmoGarbage404 @TheShuEd
|
||||
/Content.*/Lathe/ @EmoGarbage404
|
||||
/Content.*/Materials/ @EmoGarbage404
|
||||
/Content.*/Mech/ @EmoGarbage404
|
||||
/Content.*/Research/ @EmoGarbage404
|
||||
/Content.*/Stack/ @EmoGarbage404
|
||||
/Content.*/Xenoarchaeology/ @EmoGarbage404
|
||||
/Content.*/Zombies/ @EmoGarbage404
|
||||
/Resources/Prototypes/Entities/Structures/Specific/anomalies.yml @EmoGarbage404 @TheShuEd
|
||||
/Resources/Prototypes/Research/ @EmoGarbage404
|
||||
/Content.*/Anomaly/ @TheShuEd
|
||||
/Resources/Prototypes/Entities/Structures/Specific/anomalies.yml @TheShuEd
|
||||
|
||||
/Content.*/Forensics/ @ficcialfaint
|
||||
|
||||
# SKREEEE
|
||||
/Content.*.Database/ @PJB3005 @DrSmugleaf
|
||||
/Content.Shared.Database/Log*.cs @PJB3005 @DrSmugleaf @Chief-Engineer
|
||||
/Content.Shared.Database/Log*.cs @PJB3005 @DrSmugleaf @nikthechampiongr
|
||||
/Pow3r/ @PJB3005
|
||||
/Content.Server/Power/Pow3r/ @PJB3005
|
||||
|
||||
@@ -52,7 +33,7 @@
|
||||
/Content.*/Atmos/ @Partmedia
|
||||
/Content.*/Botany/ @Partmedia
|
||||
|
||||
#Jezi
|
||||
# Jezi
|
||||
/Content.*/Medical @Jezithyr
|
||||
/Content.*/Body @Jezithyr
|
||||
|
||||
|
||||
4
.github/labeler.yml
vendored
4
.github/labeler.yml
vendored
@@ -16,6 +16,10 @@
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: '**/*.swsl'
|
||||
|
||||
"Changes: Audio":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: '**/*.ogg'
|
||||
|
||||
"Changes: No C#":
|
||||
- changed-files:
|
||||
# Equiv to any-glob-to-all as long as this has one matcher. If ALL changed files are not C# files, then apply label.
|
||||
|
||||
@@ -31,19 +31,6 @@ public sealed partial class AtmosAlarmEntryContainer : BoxContainer
|
||||
[AtmosAlarmType.Danger] = "atmos-alerts-window-danger-state",
|
||||
};
|
||||
|
||||
private Dictionary<Gas, string> _gasShorthands = new Dictionary<Gas, string>()
|
||||
{
|
||||
[Gas.Ammonia] = "NH₃",
|
||||
[Gas.CarbonDioxide] = "CO₂",
|
||||
[Gas.Frezon] = "F",
|
||||
[Gas.Nitrogen] = "N₂",
|
||||
[Gas.NitrousOxide] = "N₂O",
|
||||
[Gas.Oxygen] = "O₂",
|
||||
[Gas.Plasma] = "P",
|
||||
[Gas.Tritium] = "T",
|
||||
[Gas.WaterVapor] = "H₂O",
|
||||
};
|
||||
|
||||
public AtmosAlarmEntryContainer(NetEntity uid, EntityCoordinates? coordinates)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
@@ -162,12 +149,11 @@ public sealed partial class AtmosAlarmEntryContainer : BoxContainer
|
||||
foreach ((var gas, (var mol, var percent, var alert)) in keyValuePairs)
|
||||
{
|
||||
FixedPoint2 gasPercent = percent * 100f;
|
||||
|
||||
var gasShorthand = _gasShorthands.GetValueOrDefault(gas, "X");
|
||||
var gasAbbreviation = Atmospherics.GasAbbreviations.GetValueOrDefault(gas, Loc.GetString("gas-unknown-abbreviation"));
|
||||
|
||||
var gasLabel = new Label()
|
||||
{
|
||||
Text = Loc.GetString("atmos-alerts-window-other-gases-value", ("shorthand", gasShorthand), ("value", gasPercent)),
|
||||
Text = Loc.GetString("atmos-alerts-window-other-gases-value", ("shorthand", gasAbbreviation), ("value", gasPercent)),
|
||||
FontOverride = normalFont,
|
||||
FontColorOverride = GetAlarmStateColor(alert),
|
||||
HorizontalAlignment = HAlignment.Center,
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
using Content.Shared.Atmos.Components;
|
||||
|
||||
namespace Content.Client.Atmos.Consoles;
|
||||
|
||||
public sealed class AtmosMonitoringConsoleBoundUserInterface : BoundUserInterface
|
||||
{
|
||||
[ViewVariables]
|
||||
private AtmosMonitoringConsoleWindow? _menu;
|
||||
|
||||
public AtmosMonitoringConsoleBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) { }
|
||||
|
||||
protected override void Open()
|
||||
{
|
||||
base.Open();
|
||||
|
||||
_menu = new AtmosMonitoringConsoleWindow(this, Owner);
|
||||
_menu.OpenCentered();
|
||||
_menu.OnClose += Close;
|
||||
}
|
||||
|
||||
protected override void UpdateState(BoundUserInterfaceState state)
|
||||
{
|
||||
base.UpdateState(state);
|
||||
|
||||
if (state is not AtmosMonitoringConsoleBoundInterfaceState castState)
|
||||
return;
|
||||
|
||||
EntMan.TryGetComponent<TransformComponent>(Owner, out var xform);
|
||||
_menu?.UpdateUI(xform?.Coordinates, castState.AtmosNetworks);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
if (!disposing)
|
||||
return;
|
||||
|
||||
_menu?.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
using Content.Client.Pinpointer.UI;
|
||||
using Content.Shared.Atmos.Components;
|
||||
using Content.Shared.Pinpointer;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.Collections;
|
||||
using Robust.Shared.Map.Components;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
|
||||
namespace Content.Client.Atmos.Consoles;
|
||||
|
||||
public sealed partial class AtmosMonitoringConsoleNavMapControl : NavMapControl
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
|
||||
public bool ShowPipeNetwork = true;
|
||||
public int? FocusNetId = null;
|
||||
|
||||
private const int ChunkSize = 4;
|
||||
|
||||
private readonly Color _basePipeNetColor = Color.LightGray;
|
||||
private readonly Color _unfocusedPipeNetColor = Color.DimGray;
|
||||
|
||||
private List<AtmosMonitoringConsoleLine> _atmosPipeNetwork = new();
|
||||
private Dictionary<Color, Color> _sRGBLookUp = new Dictionary<Color, Color>();
|
||||
|
||||
// Look up tables for merging continuous lines. Indexed by line color
|
||||
private Dictionary<Color, Dictionary<Vector2i, Vector2i>> _horizLines = new();
|
||||
private Dictionary<Color, Dictionary<Vector2i, Vector2i>> _horizLinesReversed = new();
|
||||
private Dictionary<Color, Dictionary<Vector2i, Vector2i>> _vertLines = new();
|
||||
private Dictionary<Color, Dictionary<Vector2i, Vector2i>> _vertLinesReversed = new();
|
||||
|
||||
public AtmosMonitoringConsoleNavMapControl() : base()
|
||||
{
|
||||
PostWallDrawingAction += DrawAllPipeNetworks;
|
||||
}
|
||||
|
||||
protected override void UpdateNavMap()
|
||||
{
|
||||
base.UpdateNavMap();
|
||||
|
||||
if (!_entManager.TryGetComponent<AtmosMonitoringConsoleComponent>(Owner, out var console))
|
||||
return;
|
||||
|
||||
if (!_entManager.TryGetComponent<MapGridComponent>(MapUid, out var grid))
|
||||
return;
|
||||
|
||||
_atmosPipeNetwork = GetDecodedAtmosPipeChunks(console.AtmosPipeChunks, grid);
|
||||
}
|
||||
|
||||
private void DrawAllPipeNetworks(DrawingHandleScreen handle)
|
||||
{
|
||||
if (!ShowPipeNetwork)
|
||||
return;
|
||||
|
||||
// Draw networks
|
||||
if (_atmosPipeNetwork != null && _atmosPipeNetwork.Any())
|
||||
DrawPipeNetwork(handle, _atmosPipeNetwork);
|
||||
}
|
||||
|
||||
private void DrawPipeNetwork(DrawingHandleScreen handle, List<AtmosMonitoringConsoleLine> atmosPipeNetwork)
|
||||
{
|
||||
var offset = GetOffset();
|
||||
offset = offset with { Y = -offset.Y };
|
||||
|
||||
if (WorldRange / WorldMaxRange > 0.5f)
|
||||
{
|
||||
var pipeNetworks = new Dictionary<Color, ValueList<Vector2>>();
|
||||
|
||||
foreach (var chunkedLine in atmosPipeNetwork)
|
||||
{
|
||||
var start = ScalePosition(chunkedLine.Origin - offset);
|
||||
var end = ScalePosition(chunkedLine.Terminus - offset);
|
||||
|
||||
if (!pipeNetworks.TryGetValue(chunkedLine.Color, out var subNetwork))
|
||||
subNetwork = new ValueList<Vector2>();
|
||||
|
||||
subNetwork.Add(start);
|
||||
subNetwork.Add(end);
|
||||
|
||||
pipeNetworks[chunkedLine.Color] = subNetwork;
|
||||
}
|
||||
|
||||
foreach ((var color, var subNetwork) in pipeNetworks)
|
||||
{
|
||||
if (subNetwork.Count > 0)
|
||||
handle.DrawPrimitives(DrawPrimitiveTopology.LineList, subNetwork.Span, color);
|
||||
}
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
var pipeVertexUVs = new Dictionary<Color, ValueList<Vector2>>();
|
||||
|
||||
foreach (var chunkedLine in atmosPipeNetwork)
|
||||
{
|
||||
var leftTop = ScalePosition(new Vector2
|
||||
(Math.Min(chunkedLine.Origin.X, chunkedLine.Terminus.X) - 0.1f,
|
||||
Math.Min(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) - 0.1f)
|
||||
- offset);
|
||||
|
||||
var rightTop = ScalePosition(new Vector2
|
||||
(Math.Max(chunkedLine.Origin.X, chunkedLine.Terminus.X) + 0.1f,
|
||||
Math.Min(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) - 0.1f)
|
||||
- offset);
|
||||
|
||||
var leftBottom = ScalePosition(new Vector2
|
||||
(Math.Min(chunkedLine.Origin.X, chunkedLine.Terminus.X) - 0.1f,
|
||||
Math.Max(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) + 0.1f)
|
||||
- offset);
|
||||
|
||||
var rightBottom = ScalePosition(new Vector2
|
||||
(Math.Max(chunkedLine.Origin.X, chunkedLine.Terminus.X) + 0.1f,
|
||||
Math.Max(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) + 0.1f)
|
||||
- offset);
|
||||
|
||||
if (!pipeVertexUVs.TryGetValue(chunkedLine.Color, out var pipeVertexUV))
|
||||
pipeVertexUV = new ValueList<Vector2>();
|
||||
|
||||
pipeVertexUV.Add(leftBottom);
|
||||
pipeVertexUV.Add(leftTop);
|
||||
pipeVertexUV.Add(rightBottom);
|
||||
pipeVertexUV.Add(leftTop);
|
||||
pipeVertexUV.Add(rightBottom);
|
||||
pipeVertexUV.Add(rightTop);
|
||||
|
||||
pipeVertexUVs[chunkedLine.Color] = pipeVertexUV;
|
||||
}
|
||||
|
||||
foreach ((var color, var pipeVertexUV) in pipeVertexUVs)
|
||||
{
|
||||
if (pipeVertexUV.Count > 0)
|
||||
handle.DrawPrimitives(DrawPrimitiveTopology.TriangleList, pipeVertexUV.Span, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<AtmosMonitoringConsoleLine> GetDecodedAtmosPipeChunks(Dictionary<Vector2i, AtmosPipeChunk>? chunks, MapGridComponent? grid)
|
||||
{
|
||||
var decodedOutput = new List<AtmosMonitoringConsoleLine>();
|
||||
|
||||
if (chunks == null || grid == null)
|
||||
return decodedOutput;
|
||||
|
||||
// Clear stale look up table values
|
||||
_horizLines.Clear();
|
||||
_horizLinesReversed.Clear();
|
||||
_vertLines.Clear();
|
||||
_vertLinesReversed.Clear();
|
||||
|
||||
// Generate masks
|
||||
var northMask = (ulong)1 << 0;
|
||||
var southMask = (ulong)1 << 1;
|
||||
var westMask = (ulong)1 << 2;
|
||||
var eastMask = (ulong)1 << 3;
|
||||
|
||||
foreach ((var chunkOrigin, var chunk) in chunks)
|
||||
{
|
||||
var list = new List<AtmosMonitoringConsoleLine>();
|
||||
|
||||
foreach (var ((netId, hexColor), atmosPipeData) in chunk.AtmosPipeData)
|
||||
{
|
||||
// Determine the correct coloration for the pipe
|
||||
var color = Color.FromHex(hexColor) * _basePipeNetColor;
|
||||
|
||||
if (FocusNetId != null && FocusNetId != netId)
|
||||
color *= _unfocusedPipeNetColor;
|
||||
|
||||
// Get the associated line look up tables
|
||||
if (!_horizLines.TryGetValue(color, out var horizLines))
|
||||
{
|
||||
horizLines = new();
|
||||
_horizLines[color] = horizLines;
|
||||
}
|
||||
|
||||
if (!_horizLinesReversed.TryGetValue(color, out var horizLinesReversed))
|
||||
{
|
||||
horizLinesReversed = new();
|
||||
_horizLinesReversed[color] = horizLinesReversed;
|
||||
}
|
||||
|
||||
if (!_vertLines.TryGetValue(color, out var vertLines))
|
||||
{
|
||||
vertLines = new();
|
||||
_vertLines[color] = vertLines;
|
||||
}
|
||||
|
||||
if (!_vertLinesReversed.TryGetValue(color, out var vertLinesReversed))
|
||||
{
|
||||
vertLinesReversed = new();
|
||||
_vertLinesReversed[color] = vertLinesReversed;
|
||||
}
|
||||
|
||||
// Loop over the chunk
|
||||
for (var tileIdx = 0; tileIdx < ChunkSize * ChunkSize; tileIdx++)
|
||||
{
|
||||
if (atmosPipeData == 0)
|
||||
continue;
|
||||
|
||||
var mask = (ulong)SharedNavMapSystem.AllDirMask << tileIdx * SharedNavMapSystem.Directions;
|
||||
|
||||
if ((atmosPipeData & mask) == 0)
|
||||
continue;
|
||||
|
||||
var relativeTile = GetTileFromIndex(tileIdx);
|
||||
var tile = (chunk.Origin * ChunkSize + relativeTile) * grid.TileSize;
|
||||
tile = tile with { Y = -tile.Y };
|
||||
|
||||
// Calculate the draw point offsets
|
||||
var vertLineOrigin = (atmosPipeData & northMask << tileIdx * SharedNavMapSystem.Directions) > 0 ?
|
||||
new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 1f) : new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 0.5f);
|
||||
|
||||
var vertLineTerminus = (atmosPipeData & southMask << tileIdx * SharedNavMapSystem.Directions) > 0 ?
|
||||
new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 0f) : new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 0.5f);
|
||||
|
||||
var horizLineOrigin = (atmosPipeData & eastMask << tileIdx * SharedNavMapSystem.Directions) > 0 ?
|
||||
new Vector2(grid.TileSize * 1f, -grid.TileSize * 0.5f) : new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 0.5f);
|
||||
|
||||
var horizLineTerminus = (atmosPipeData & westMask << tileIdx * SharedNavMapSystem.Directions) > 0 ?
|
||||
new Vector2(grid.TileSize * 0f, -grid.TileSize * 0.5f) : new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 0.5f);
|
||||
|
||||
// Since we can have pipe lines that have a length of a half tile,
|
||||
// double the vectors and convert to vector2i so we can merge them
|
||||
AddOrUpdateNavMapLine(ConvertVector2ToVector2i(tile + horizLineOrigin, 2), ConvertVector2ToVector2i(tile + horizLineTerminus, 2), horizLines, horizLinesReversed);
|
||||
AddOrUpdateNavMapLine(ConvertVector2ToVector2i(tile + vertLineOrigin, 2), ConvertVector2ToVector2i(tile + vertLineTerminus, 2), vertLines, vertLinesReversed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Scale the vector2is back down and convert to vector2
|
||||
foreach (var (color, horizLines) in _horizLines)
|
||||
{
|
||||
// Get the corresponding sRBG color
|
||||
var sRGB = GetsRGBColor(color);
|
||||
|
||||
foreach (var (origin, terminal) in horizLines)
|
||||
decodedOutput.Add(new AtmosMonitoringConsoleLine
|
||||
(ConvertVector2iToVector2(origin, 0.5f), ConvertVector2iToVector2(terminal, 0.5f), sRGB));
|
||||
}
|
||||
|
||||
foreach (var (color, vertLines) in _vertLines)
|
||||
{
|
||||
// Get the corresponding sRBG color
|
||||
var sRGB = GetsRGBColor(color);
|
||||
|
||||
foreach (var (origin, terminal) in vertLines)
|
||||
decodedOutput.Add(new AtmosMonitoringConsoleLine
|
||||
(ConvertVector2iToVector2(origin, 0.5f), ConvertVector2iToVector2(terminal, 0.5f), sRGB));
|
||||
}
|
||||
|
||||
return decodedOutput;
|
||||
}
|
||||
|
||||
private Vector2 ConvertVector2iToVector2(Vector2i vector, float scale = 1f)
|
||||
{
|
||||
return new Vector2(vector.X * scale, vector.Y * scale);
|
||||
}
|
||||
|
||||
private Vector2i ConvertVector2ToVector2i(Vector2 vector, float scale = 1f)
|
||||
{
|
||||
return new Vector2i((int)MathF.Round(vector.X * scale), (int)MathF.Round(vector.Y * scale));
|
||||
}
|
||||
|
||||
private Vector2i GetTileFromIndex(int index)
|
||||
{
|
||||
var x = index / ChunkSize;
|
||||
var y = index % ChunkSize;
|
||||
return new Vector2i(x, y);
|
||||
}
|
||||
|
||||
private Color GetsRGBColor(Color color)
|
||||
{
|
||||
if (!_sRGBLookUp.TryGetValue(color, out var sRGB))
|
||||
{
|
||||
sRGB = Color.ToSrgb(color);
|
||||
_sRGBLookUp[color] = sRGB;
|
||||
}
|
||||
|
||||
return sRGB;
|
||||
}
|
||||
}
|
||||
|
||||
public struct AtmosMonitoringConsoleLine
|
||||
{
|
||||
public readonly Vector2 Origin;
|
||||
public readonly Vector2 Terminus;
|
||||
public readonly Color Color;
|
||||
|
||||
public AtmosMonitoringConsoleLine(Vector2 origin, Vector2 terminus, Color color)
|
||||
{
|
||||
Origin = origin;
|
||||
Terminus = terminus;
|
||||
Color = color;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
using Content.Shared.Atmos.Components;
|
||||
using Content.Shared.Atmos.Consoles;
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Client.Atmos.Consoles;
|
||||
|
||||
public sealed class AtmosMonitoringConsoleSystem : SharedAtmosMonitoringConsoleSystem
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<AtmosMonitoringConsoleComponent, ComponentHandleState>(OnHandleState);
|
||||
}
|
||||
|
||||
private void OnHandleState(EntityUid uid, AtmosMonitoringConsoleComponent component, ref ComponentHandleState args)
|
||||
{
|
||||
Dictionary<Vector2i, Dictionary<(int, string), ulong>> modifiedChunks;
|
||||
Dictionary<NetEntity, AtmosDeviceNavMapData> atmosDevices;
|
||||
|
||||
switch (args.Current)
|
||||
{
|
||||
case AtmosMonitoringConsoleDeltaState delta:
|
||||
{
|
||||
modifiedChunks = delta.ModifiedChunks;
|
||||
atmosDevices = delta.AtmosDevices;
|
||||
|
||||
foreach (var index in component.AtmosPipeChunks.Keys)
|
||||
{
|
||||
if (!delta.AllChunks!.Contains(index))
|
||||
component.AtmosPipeChunks.Remove(index);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case AtmosMonitoringConsoleState state:
|
||||
{
|
||||
modifiedChunks = state.Chunks;
|
||||
atmosDevices = state.AtmosDevices;
|
||||
|
||||
foreach (var index in component.AtmosPipeChunks.Keys)
|
||||
{
|
||||
if (!state.Chunks.ContainsKey(index))
|
||||
component.AtmosPipeChunks.Remove(index);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var (origin, chunk) in modifiedChunks)
|
||||
{
|
||||
var newChunk = new AtmosPipeChunk(origin);
|
||||
newChunk.AtmosPipeData = new Dictionary<(int, string), ulong>(chunk);
|
||||
|
||||
component.AtmosPipeChunks[origin] = newChunk;
|
||||
}
|
||||
|
||||
component.AtmosDevices.Clear();
|
||||
|
||||
foreach (var (nuid, atmosDevice) in atmosDevices)
|
||||
{
|
||||
component.AtmosDevices[nuid] = atmosDevice;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
<controls:FancyWindow xmlns="https://spacestation14.io"
|
||||
xmlns:ui="clr-namespace:Content.Client.Atmos.Consoles"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
|
||||
Title="{Loc 'atmos-monitoring-window-title'}"
|
||||
Resizable="False"
|
||||
SetSize="1120 750"
|
||||
MinSize="1120 750">
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<!-- Main display -->
|
||||
<BoxContainer Orientation="Horizontal" VerticalExpand="True" HorizontalExpand="True">
|
||||
<!-- Nav map -->
|
||||
<BoxContainer Orientation="Vertical" VerticalExpand="True" HorizontalExpand="True">
|
||||
<ui:AtmosMonitoringConsoleNavMapControl Name="NavMap" Margin="5 5" VerticalExpand="True" HorizontalExpand="True">
|
||||
|
||||
<!-- System warning -->
|
||||
<PanelContainer Name="SystemWarningPanel"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Top"
|
||||
HorizontalExpand="True"
|
||||
Margin="0 48 0 0"
|
||||
Visible="False">
|
||||
<RichTextLabel Name="SystemWarningLabel" Margin="12 8 12 8"/>
|
||||
</PanelContainer>
|
||||
|
||||
</ui:AtmosMonitoringConsoleNavMapControl>
|
||||
|
||||
<!-- Nav map legend -->
|
||||
<BoxContainer Orientation="Horizontal" Margin="0 10 0 10">
|
||||
<TextureRect Stretch="KeepAspectCentered"
|
||||
TexturePath="/Textures/Interface/NavMap/beveled_square.png"
|
||||
Modulate="#a9a9a9"
|
||||
SetSize="16 16"
|
||||
Margin="20 0 5 0"/>
|
||||
<Label Text="{Loc 'atmos-monitoring-window-label-gas-opening'}"/>
|
||||
<TextureRect Stretch="KeepAspectCentered"
|
||||
TexturePath="/Textures/Interface/NavMap/beveled_circle.png"
|
||||
SetSize="16 16"
|
||||
Modulate="#a9a9a9"
|
||||
Margin="20 0 5 0"/>
|
||||
<Label Text="{Loc 'atmos-monitoring-window-label-gas-scrubber'}"/>
|
||||
<TextureRect Stretch="KeepAspectCentered"
|
||||
TexturePath="/Textures/Interface/NavMap/beveled_arrow_east.png"
|
||||
SetSize="16 16"
|
||||
Modulate="#a9a9a9"
|
||||
Margin="20 0 5 0"/>
|
||||
<Label Text="{Loc 'atmos-monitoring-window-label-gas-flow-regulator'}"/>
|
||||
<TextureRect Stretch="KeepAspectCentered"
|
||||
TexturePath="/Textures/Interface/NavMap/beveled_hexagon.png"
|
||||
SetSize="16 16"
|
||||
Modulate="#a9a9a9"
|
||||
Margin="20 0 5 0"/>
|
||||
<Label Text="{Loc 'atmos-monitoring-window-label-thermoregulator'}"/>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Atmosphere status -->
|
||||
<BoxContainer Orientation="Vertical" VerticalExpand="True" SetWidth="440" Margin="0 0 10 10">
|
||||
|
||||
<!-- Station name -->
|
||||
<controls:StripeBack>
|
||||
<PanelContainer>
|
||||
<RichTextLabel Name="StationName" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="0 5 0 3"/>
|
||||
</PanelContainer>
|
||||
</controls:StripeBack>
|
||||
|
||||
<!-- Alarm status (entries added by C# code) -->
|
||||
<TabContainer Name="MasterTabContainer" VerticalExpand="True" HorizontalExpand="True" Margin="0 10 0 0">
|
||||
<ScrollContainer HorizontalExpand="True" Margin="8, 8, 8, 8">
|
||||
<BoxContainer Name="AtmosNetworksTable" Orientation="Vertical" VerticalExpand="True" HorizontalExpand="True" Margin="0 0 0 10"/>
|
||||
</ScrollContainer>
|
||||
</TabContainer>
|
||||
|
||||
<!-- Overlay toggles -->
|
||||
<BoxContainer Orientation="Vertical" Margin="0 10 0 0">
|
||||
<Label Text="{Loc 'atmos-monitoring-window-toggle-overlays'}" Margin="0 0 0 5"/>
|
||||
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
|
||||
<CheckBox Name="ShowPipeNetwork" Text="{Loc 'atmos-monitoring-window-show-pipe-network'}" Pressed="True" HorizontalExpand="True"/>
|
||||
<CheckBox Name="ShowGasPipeSensors" Text="{Loc 'atmos-monitoring-window-show-gas-pipe-sensors'}" Pressed="False" HorizontalExpand="True"/>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
|
||||
</BoxContainer>
|
||||
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Footer -->
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<PanelContainer StyleClasses="LowDivider" />
|
||||
<BoxContainer Orientation="Horizontal" Margin="10 2 5 0" VerticalAlignment="Bottom">
|
||||
<Label Text="{Loc 'atmos-monitoring-window-flavor-left'}" StyleClasses="WindowFooterText" />
|
||||
<Label Text="{Loc 'atmos-monitoring-window-flavor-right'}" StyleClasses="WindowFooterText"
|
||||
HorizontalAlignment="Right" HorizontalExpand="True" Margin="0 0 5 0" />
|
||||
<TextureRect StyleClasses="NTLogoDark" Stretch="KeepAspectCentered"
|
||||
VerticalAlignment="Center" HorizontalAlignment="Right" SetSize="19 19"/>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</controls:FancyWindow>
|
||||
@@ -0,0 +1,455 @@
|
||||
using Content.Client.Pinpointer.UI;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Shared.Atmos.Components;
|
||||
using Content.Shared.Prototypes;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
|
||||
namespace Content.Client.Atmos.Consoles;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class AtmosMonitoringConsoleWindow : FancyWindow
|
||||
{
|
||||
private readonly IEntityManager _entManager;
|
||||
private readonly IPrototypeManager _protoManager;
|
||||
private readonly SpriteSystem _spriteSystem;
|
||||
|
||||
private EntityUid? _owner;
|
||||
private NetEntity? _focusEntity;
|
||||
private int? _focusNetId;
|
||||
|
||||
private bool _autoScrollActive = false;
|
||||
|
||||
private readonly Color _unfocusedDeviceColor = Color.DimGray;
|
||||
private ProtoId<NavMapBlipPrototype> _navMapConsoleProtoId = "NavMapConsole";
|
||||
private ProtoId<NavMapBlipPrototype> _gasPipeSensorProtoId = "GasPipeSensor";
|
||||
|
||||
public AtmosMonitoringConsoleWindow(AtmosMonitoringConsoleBoundUserInterface userInterface, EntityUid? owner)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
_entManager = IoCManager.Resolve<IEntityManager>();
|
||||
_protoManager = IoCManager.Resolve<IPrototypeManager>();
|
||||
_spriteSystem = _entManager.System<SpriteSystem>();
|
||||
|
||||
// Pass the owner to nav map
|
||||
_owner = owner;
|
||||
NavMap.Owner = _owner;
|
||||
|
||||
// Set nav map grid uid
|
||||
var stationName = Loc.GetString("atmos-monitoring-window-unknown-location");
|
||||
EntityCoordinates? consoleCoords = null;
|
||||
|
||||
if (_entManager.TryGetComponent<TransformComponent>(owner, out var xform))
|
||||
{
|
||||
consoleCoords = xform.Coordinates;
|
||||
NavMap.MapUid = xform.GridUid;
|
||||
|
||||
// Assign station name
|
||||
if (_entManager.TryGetComponent<MetaDataComponent>(xform.GridUid, out var stationMetaData))
|
||||
stationName = stationMetaData.EntityName;
|
||||
|
||||
var msg = new FormattedMessage();
|
||||
msg.TryAddMarkup(Loc.GetString("atmos-monitoring-window-station-name", ("stationName", stationName)), out _);
|
||||
|
||||
StationName.SetMessage(msg);
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
StationName.SetMessage(stationName);
|
||||
NavMap.Visible = false;
|
||||
}
|
||||
|
||||
// Set trackable entity selected action
|
||||
NavMap.TrackedEntitySelectedAction += SetTrackedEntityFromNavMap;
|
||||
|
||||
// Update nav map
|
||||
NavMap.ForceNavMapUpdate();
|
||||
|
||||
// Set tab container headers
|
||||
MasterTabContainer.SetTabTitle(0, Loc.GetString("atmos-monitoring-window-tab-networks"));
|
||||
|
||||
// Set UI toggles
|
||||
ShowPipeNetwork.OnToggled += _ => OnShowPipeNetworkToggled();
|
||||
ShowGasPipeSensors.OnToggled += _ => OnShowGasPipeSensors();
|
||||
|
||||
// Set nav map colors
|
||||
if (!_entManager.TryGetComponent<AtmosMonitoringConsoleComponent>(_owner, out var console))
|
||||
return;
|
||||
|
||||
NavMap.TileColor = console.NavMapTileColor;
|
||||
NavMap.WallColor = console.NavMapWallColor;
|
||||
|
||||
// Initalize
|
||||
UpdateUI(consoleCoords, Array.Empty<AtmosMonitoringConsoleEntry>());
|
||||
}
|
||||
|
||||
#region Toggle handling
|
||||
|
||||
private void OnShowPipeNetworkToggled()
|
||||
{
|
||||
if (_owner == null)
|
||||
return;
|
||||
|
||||
if (!_entManager.TryGetComponent<AtmosMonitoringConsoleComponent>(_owner.Value, out var console))
|
||||
return;
|
||||
|
||||
NavMap.ShowPipeNetwork = ShowPipeNetwork.Pressed;
|
||||
|
||||
foreach (var (netEnt, device) in console.AtmosDevices)
|
||||
{
|
||||
if (device.NavMapBlip == _gasPipeSensorProtoId)
|
||||
continue;
|
||||
|
||||
if (ShowPipeNetwork.Pressed)
|
||||
AddTrackedEntityToNavMap(device);
|
||||
|
||||
else
|
||||
NavMap.TrackedEntities.Remove(netEnt);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnShowGasPipeSensors()
|
||||
{
|
||||
if (_owner == null)
|
||||
return;
|
||||
|
||||
if (!_entManager.TryGetComponent<AtmosMonitoringConsoleComponent>(_owner.Value, out var console))
|
||||
return;
|
||||
|
||||
foreach (var (netEnt, device) in console.AtmosDevices)
|
||||
{
|
||||
if (device.NavMapBlip != _gasPipeSensorProtoId)
|
||||
continue;
|
||||
|
||||
if (ShowGasPipeSensors.Pressed)
|
||||
AddTrackedEntityToNavMap(device, true);
|
||||
|
||||
else
|
||||
NavMap.TrackedEntities.Remove(netEnt);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public void UpdateUI
|
||||
(EntityCoordinates? consoleCoords,
|
||||
AtmosMonitoringConsoleEntry[] atmosNetworks)
|
||||
{
|
||||
if (_owner == null)
|
||||
return;
|
||||
|
||||
if (!_entManager.TryGetComponent<AtmosMonitoringConsoleComponent>(_owner.Value, out var console))
|
||||
return;
|
||||
|
||||
// Reset nav map values
|
||||
NavMap.TrackedCoordinates.Clear();
|
||||
NavMap.TrackedEntities.Clear();
|
||||
|
||||
if (_focusEntity != null && !console.AtmosDevices.Any(x => x.Key == _focusEntity))
|
||||
ClearFocus();
|
||||
|
||||
// Add tracked entities to the nav map
|
||||
UpdateNavMapBlips();
|
||||
|
||||
// Show the monitor location
|
||||
var consoleNetEnt = _entManager.GetNetEntity(_owner);
|
||||
|
||||
if (consoleCoords != null && consoleNetEnt != null)
|
||||
{
|
||||
var proto = _protoManager.Index(_navMapConsoleProtoId);
|
||||
|
||||
if (proto.TexturePaths != null && proto.TexturePaths.Length != 0)
|
||||
{
|
||||
var texture = _spriteSystem.Frame0(new SpriteSpecifier.Texture(proto.TexturePaths[0]));
|
||||
var blip = new NavMapBlip(consoleCoords.Value, texture, proto.Color, proto.Blinks, proto.Selectable);
|
||||
NavMap.TrackedEntities[consoleNetEnt.Value] = blip;
|
||||
}
|
||||
}
|
||||
|
||||
// Update the nav map
|
||||
NavMap.ForceNavMapUpdate();
|
||||
|
||||
// Clear excess children from the tables
|
||||
while (AtmosNetworksTable.ChildCount > atmosNetworks.Length)
|
||||
AtmosNetworksTable.RemoveChild(AtmosNetworksTable.GetChild(AtmosNetworksTable.ChildCount - 1));
|
||||
|
||||
// Update all entries in each table
|
||||
for (int index = 0; index < atmosNetworks.Length; index++)
|
||||
{
|
||||
var entry = atmosNetworks.ElementAt(index);
|
||||
UpdateUIEntry(entry, index, AtmosNetworksTable, console);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateNavMapBlips()
|
||||
{
|
||||
if (_owner == null || !_entManager.TryGetComponent<AtmosMonitoringConsoleComponent>(_owner.Value, out var console))
|
||||
return;
|
||||
|
||||
if (NavMap.Visible)
|
||||
{
|
||||
foreach (var (netEnt, device) in console.AtmosDevices)
|
||||
{
|
||||
// Update the focus network ID, incase it has changed
|
||||
if (_focusEntity == netEnt)
|
||||
{
|
||||
_focusNetId = device.NetId;
|
||||
NavMap.FocusNetId = _focusNetId;
|
||||
}
|
||||
|
||||
var isSensor = device.NavMapBlip == _gasPipeSensorProtoId;
|
||||
|
||||
// Skip network devices if the toggled is off
|
||||
if (!ShowPipeNetwork.Pressed && !isSensor)
|
||||
continue;
|
||||
|
||||
// Skip gas pipe sensors if the toggle is off
|
||||
if (!ShowGasPipeSensors.Pressed && isSensor)
|
||||
continue;
|
||||
|
||||
AddTrackedEntityToNavMap(device, isSensor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void AddTrackedEntityToNavMap(AtmosDeviceNavMapData metaData, bool isSensor = false)
|
||||
{
|
||||
var proto = _protoManager.Index(metaData.NavMapBlip);
|
||||
|
||||
if (proto.TexturePaths == null || proto.TexturePaths.Length == 0)
|
||||
return;
|
||||
|
||||
var idx = Math.Clamp((int)metaData.Direction / 2, 0, proto.TexturePaths.Length - 1);
|
||||
var texture = proto.TexturePaths.Length > 0 ? proto.TexturePaths[idx] : proto.TexturePaths[0];
|
||||
var color = isSensor ? proto.Color : proto.Color * metaData.PipeColor;
|
||||
|
||||
if (_focusNetId != null && metaData.NetId != _focusNetId)
|
||||
color *= _unfocusedDeviceColor;
|
||||
|
||||
var blinks = proto.Blinks || _focusEntity == metaData.NetEntity;
|
||||
var coords = _entManager.GetCoordinates(metaData.NetCoordinates);
|
||||
var blip = new NavMapBlip(coords, _spriteSystem.Frame0(new SpriteSpecifier.Texture(texture)), color, blinks, proto.Selectable, proto.Scale);
|
||||
NavMap.TrackedEntities[metaData.NetEntity] = blip;
|
||||
}
|
||||
|
||||
private void UpdateUIEntry(AtmosMonitoringConsoleEntry data, int index, Control table, AtmosMonitoringConsoleComponent console)
|
||||
{
|
||||
// Make new UI entry if required
|
||||
if (index >= table.ChildCount)
|
||||
{
|
||||
var newEntryContainer = new AtmosMonitoringEntryContainer(data);
|
||||
|
||||
// On click
|
||||
newEntryContainer.FocusButton.OnButtonUp += args =>
|
||||
{
|
||||
if (_focusEntity == newEntryContainer.Data.NetEntity)
|
||||
{
|
||||
ClearFocus();
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
SetFocus(newEntryContainer.Data.NetEntity, newEntryContainer.Data.NetId);
|
||||
|
||||
var coords = _entManager.GetCoordinates(newEntryContainer.Data.Coordinates);
|
||||
NavMap.CenterToCoordinates(coords);
|
||||
}
|
||||
|
||||
// Update affected UI elements across all tables
|
||||
UpdateConsoleTable(console, AtmosNetworksTable, _focusEntity);
|
||||
};
|
||||
|
||||
// Add the entry to the current table
|
||||
table.AddChild(newEntryContainer);
|
||||
}
|
||||
|
||||
// Update values and UI elements
|
||||
var tableChild = table.GetChild(index);
|
||||
|
||||
if (tableChild is not AtmosMonitoringEntryContainer)
|
||||
{
|
||||
table.RemoveChild(tableChild);
|
||||
UpdateUIEntry(data, index, table, console);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var entryContainer = (AtmosMonitoringEntryContainer)tableChild;
|
||||
entryContainer.UpdateEntry(data, data.NetEntity == _focusEntity);
|
||||
}
|
||||
|
||||
private void UpdateConsoleTable(AtmosMonitoringConsoleComponent console, Control table, NetEntity? currTrackedEntity)
|
||||
{
|
||||
foreach (var tableChild in table.Children)
|
||||
{
|
||||
if (tableChild is not AtmosAlarmEntryContainer)
|
||||
continue;
|
||||
|
||||
var entryContainer = (AtmosAlarmEntryContainer)tableChild;
|
||||
|
||||
if (entryContainer.NetEntity != currTrackedEntity)
|
||||
entryContainer.RemoveAsFocus();
|
||||
|
||||
else if (entryContainer.NetEntity == currTrackedEntity)
|
||||
entryContainer.SetAsFocus();
|
||||
}
|
||||
}
|
||||
|
||||
private void SetTrackedEntityFromNavMap(NetEntity? focusEntity)
|
||||
{
|
||||
if (focusEntity == null)
|
||||
return;
|
||||
|
||||
if (!_entManager.TryGetComponent<AtmosMonitoringConsoleComponent>(_owner, out var console))
|
||||
return;
|
||||
|
||||
foreach (var (netEnt, device) in console.AtmosDevices)
|
||||
{
|
||||
if (netEnt != focusEntity)
|
||||
continue;
|
||||
|
||||
if (device.NavMapBlip != _gasPipeSensorProtoId)
|
||||
return;
|
||||
|
||||
// Set new focus
|
||||
SetFocus(focusEntity.Value, device.NetId);
|
||||
|
||||
// Get the scroll position of the selected entity on the selected button the UI
|
||||
ActivateAutoScrollToFocus();
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
AutoScrollToFocus();
|
||||
}
|
||||
|
||||
private void ActivateAutoScrollToFocus()
|
||||
{
|
||||
_autoScrollActive = true;
|
||||
}
|
||||
|
||||
private void AutoScrollToFocus()
|
||||
{
|
||||
if (!_autoScrollActive)
|
||||
return;
|
||||
|
||||
var scroll = AtmosNetworksTable.Parent as ScrollContainer;
|
||||
if (scroll == null)
|
||||
return;
|
||||
|
||||
if (!TryGetVerticalScrollbar(scroll, out var vScrollbar))
|
||||
return;
|
||||
|
||||
if (!TryGetNextScrollPosition(out float? nextScrollPosition))
|
||||
return;
|
||||
|
||||
vScrollbar.ValueTarget = nextScrollPosition.Value;
|
||||
|
||||
if (MathHelper.CloseToPercent(vScrollbar.Value, vScrollbar.ValueTarget))
|
||||
_autoScrollActive = false;
|
||||
}
|
||||
|
||||
private bool TryGetVerticalScrollbar(ScrollContainer scroll, [NotNullWhen(true)] out VScrollBar? vScrollBar)
|
||||
{
|
||||
vScrollBar = null;
|
||||
|
||||
foreach (var control in scroll.Children)
|
||||
{
|
||||
if (control is not VScrollBar)
|
||||
continue;
|
||||
|
||||
vScrollBar = (VScrollBar)control;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool TryGetNextScrollPosition([NotNullWhen(true)] out float? nextScrollPosition)
|
||||
{
|
||||
nextScrollPosition = null;
|
||||
|
||||
var scroll = AtmosNetworksTable.Parent as ScrollContainer;
|
||||
if (scroll == null)
|
||||
return false;
|
||||
|
||||
var container = scroll.Children.ElementAt(0) as BoxContainer;
|
||||
if (container == null || container.Children.Count() == 0)
|
||||
return false;
|
||||
|
||||
// Exit if the heights of the children haven't been initialized yet
|
||||
if (!container.Children.Any(x => x.Height > 0))
|
||||
return false;
|
||||
|
||||
nextScrollPosition = 0;
|
||||
|
||||
foreach (var control in container.Children)
|
||||
{
|
||||
if (control is not AtmosMonitoringEntryContainer)
|
||||
continue;
|
||||
|
||||
var entry = (AtmosMonitoringEntryContainer)control;
|
||||
|
||||
if (entry.Data.NetEntity == _focusEntity)
|
||||
return true;
|
||||
|
||||
nextScrollPosition += control.Height;
|
||||
}
|
||||
|
||||
// Failed to find control
|
||||
nextScrollPosition = null;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void SetFocus(NetEntity focusEntity, int focusNetId)
|
||||
{
|
||||
_focusEntity = focusEntity;
|
||||
_focusNetId = focusNetId;
|
||||
NavMap.FocusNetId = focusNetId;
|
||||
|
||||
OnFocusChanged();
|
||||
}
|
||||
|
||||
private void ClearFocus()
|
||||
{
|
||||
_focusEntity = null;
|
||||
_focusNetId = null;
|
||||
NavMap.FocusNetId = null;
|
||||
|
||||
OnFocusChanged();
|
||||
}
|
||||
|
||||
private void OnFocusChanged()
|
||||
{
|
||||
UpdateNavMapBlips();
|
||||
NavMap.ForceNavMapUpdate();
|
||||
|
||||
if (!_entManager.TryGetComponent<AtmosMonitoringConsoleComponent>(_owner, out var console))
|
||||
return;
|
||||
|
||||
for (int index = 0; index < AtmosNetworksTable.ChildCount; index++)
|
||||
{
|
||||
var entry = (AtmosMonitoringEntryContainer)AtmosNetworksTable.GetChild(index);
|
||||
|
||||
if (entry == null)
|
||||
continue;
|
||||
|
||||
UpdateUIEntry(entry.Data, index, AtmosNetworksTable, console);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<BoxContainer xmlns="https://spacestation14.io"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:s="clr-namespace:Content.Client.Stylesheets"
|
||||
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
Orientation="Vertical" HorizontalExpand ="True" Margin="0 0 0 3">
|
||||
|
||||
<!-- Network selection button -->
|
||||
<Button Name="FocusButton" HorizontalExpand="True" VerticalExpand="True" Margin="0 0 6 8" StyleClasses="OpenLeft" Access="Public">
|
||||
<BoxContainer HorizontalExpand="True" VerticalExpand="True" Orientation="Vertical">
|
||||
<BoxContainer HorizontalExpand="True" VerticalExpand="True" Orientation="Horizontal" SetHeight="32">
|
||||
<PanelContainer Name="NetworkColorStripe" HorizontalAlignment="Left" SetWidth="8" VerticalExpand="True" Margin="-8 -2 0 0">
|
||||
<PanelContainer.PanelOverride>
|
||||
<gfx:StyleBoxFlat BackgroundColor="#d7d7d7"/>
|
||||
</PanelContainer.PanelOverride>
|
||||
</PanelContainer>
|
||||
<Label Name="NetworkNameLabel" Text="???" HorizontalExpand="True" HorizontalAlignment="Center"/>
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Panel that appears on selecting the device -->
|
||||
|
||||
<PanelContainer HorizontalExpand="True" Margin="-8 0 -14 -4" Access="Public">
|
||||
<PanelContainer.PanelOverride>
|
||||
<gfx:StyleBoxFlat BackgroundColor="#25252a"/>
|
||||
</PanelContainer.PanelOverride>
|
||||
<BoxContainer Name="MainDataContainer" HorizontalExpand="True" VerticalExpand="True" Orientation="Vertical">
|
||||
<Control>
|
||||
<BoxContainer HorizontalExpand="True" VerticalExpand="True" Orientation="Vertical">
|
||||
<BoxContainer HorizontalExpand="True" Orientation="Horizontal">
|
||||
<Label Name="TemperatureHeaderLabel" Text="{Loc 'atmos-alerts-window-temperature-label'}" HorizontalAlignment="Center" HorizontalExpand="True" FontColorOverride="#a9a9a9" Margin="0 2 0 0" SetHeight="24"></Label>
|
||||
<Label Name="PressureHeaderLabel" Text="{Loc 'atmos-alerts-window-pressure-label'}" HorizontalAlignment="Center" HorizontalExpand="True" FontColorOverride="#a9a9a9" Margin="0 2 0 0" SetHeight="24"></Label>
|
||||
<Label Name="TotalMolHeaderLabel" Text="{Loc 'atmos-alerts-window-total-mol-label'}" HorizontalAlignment="Center" HorizontalExpand="True" FontColorOverride="#a9a9a9" Margin="0 2 0 0" SetHeight="24"></Label>
|
||||
</BoxContainer>
|
||||
<PanelContainer HorizontalExpand="True">
|
||||
<PanelContainer.PanelOverride>
|
||||
<gfx:StyleBoxFlat BackgroundColor="#202023"/>
|
||||
</PanelContainer.PanelOverride>
|
||||
<BoxContainer HorizontalExpand="True" Orientation="Horizontal">
|
||||
<Label Name="TemperatureLabel" Text="???" HorizontalAlignment="Center" HorizontalExpand="True" FontColorOverride="#a9a9a9" Margin="0 2 0 0" SetHeight="24"></Label>
|
||||
<Label Name="PressureLabel" Text="???" HorizontalAlignment="Center" HorizontalExpand="True" FontColorOverride="#a9a9a9" Margin="0 2 0 0" SetHeight="24"></Label>
|
||||
<Label Name="TotalMolLabel" Text="???" HorizontalAlignment="Center" HorizontalExpand="True" FontColorOverride="#a9a9a9" Margin="0 2 0 0" SetHeight="24"></Label>
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
<BoxContainer HorizontalExpand="True" Orientation="Horizontal" Margin="8 0">
|
||||
<TextureRect Name="ArrowTexture" VerticalAlignment="Center" SetSize="12 12" Stretch="KeepAspectCentered" Margin="3 0" TexturePath="/Textures/Interface/Nano/triangle_right.png"></TextureRect>
|
||||
<Label Name="GasesHeaderLabel" Text="{Loc 'atmos-monitoring-window-label-gases'}" HorizontalAlignment="Left" HorizontalExpand="True" FontColorOverride="#a9a9a9" Margin="4 0 0 0" SetHeight="24"></Label>
|
||||
</BoxContainer>
|
||||
|
||||
</BoxContainer>
|
||||
</Control>
|
||||
|
||||
<!-- Atmosphere status -->
|
||||
<Control Name="FocusContainer" ReservesSpace="False" Visible="False">
|
||||
<!-- Main container for displaying atmospheric data -->
|
||||
<BoxContainer HorizontalExpand="True" VerticalExpand="True" Orientation="Vertical">
|
||||
<PanelContainer HorizontalExpand="True">
|
||||
<PanelContainer.PanelOverride>
|
||||
<gfx:StyleBoxFlat BackgroundColor="#202023"/>
|
||||
</PanelContainer.PanelOverride>
|
||||
|
||||
<!-- Gas entries added via C# code -->
|
||||
<GridContainer Name="GasGridContainer" HorizontalExpand="True" Columns = "4"></GridContainer>
|
||||
</PanelContainer>
|
||||
</BoxContainer>
|
||||
</Control>
|
||||
</BoxContainer>
|
||||
|
||||
<!-- If the alarm is inactive, this is label is displayed instead -->
|
||||
<Label Name="NoDataLabel" Text="{Loc 'atmos-alerts-window-no-data-available'}" HorizontalAlignment="Center" Margin="0 15" FontColorOverride="#a9a9a9" ReservesSpace="False" Visible="False"></Label>
|
||||
|
||||
</PanelContainer>
|
||||
</BoxContainer>
|
||||
</Button>
|
||||
</BoxContainer>
|
||||
@@ -0,0 +1,166 @@
|
||||
using Content.Client.Stylesheets;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Atmos.Components;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Temperature;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using System.Linq;
|
||||
|
||||
namespace Content.Client.Atmos.Consoles;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class AtmosMonitoringEntryContainer : BoxContainer
|
||||
{
|
||||
public AtmosMonitoringConsoleEntry Data;
|
||||
|
||||
private readonly IEntityManager _entManager;
|
||||
private readonly IResourceCache _cache;
|
||||
|
||||
public AtmosMonitoringEntryContainer(AtmosMonitoringConsoleEntry data)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
_entManager = IoCManager.Resolve<IEntityManager>();
|
||||
_cache = IoCManager.Resolve<IResourceCache>();
|
||||
|
||||
Data = data;
|
||||
|
||||
// Modulate colored stripe
|
||||
NetworkColorStripe.Modulate = data.Color;
|
||||
|
||||
// Load fonts
|
||||
var headerFont = new VectorFont(_cache.GetResource<FontResource>("/Fonts/NotoSans/NotoSans-Bold.ttf"), 11);
|
||||
var normalFont = new VectorFont(_cache.GetResource<FontResource>("/Fonts/NotoSansDisplay/NotoSansDisplay-Regular.ttf"), 11);
|
||||
|
||||
// Set fonts
|
||||
TemperatureHeaderLabel.FontOverride = headerFont;
|
||||
PressureHeaderLabel.FontOverride = headerFont;
|
||||
TotalMolHeaderLabel.FontOverride = headerFont;
|
||||
GasesHeaderLabel.FontOverride = headerFont;
|
||||
|
||||
TemperatureLabel.FontOverride = normalFont;
|
||||
PressureLabel.FontOverride = normalFont;
|
||||
TotalMolLabel.FontOverride = normalFont;
|
||||
|
||||
NoDataLabel.FontOverride = headerFont;
|
||||
}
|
||||
|
||||
public void UpdateEntry(AtmosMonitoringConsoleEntry updatedData, bool isFocus)
|
||||
{
|
||||
// Load fonts
|
||||
var normalFont = new VectorFont(_cache.GetResource<FontResource>("/Fonts/NotoSansDisplay/NotoSansDisplay-Regular.ttf"), 11);
|
||||
|
||||
// Update name and values
|
||||
if (!string.IsNullOrEmpty(updatedData.Address))
|
||||
NetworkNameLabel.Text = Loc.GetString("atmos-alerts-window-alarm-label", ("name", updatedData.EntityName), ("address", updatedData.Address));
|
||||
|
||||
else
|
||||
NetworkNameLabel.Text = Loc.GetString(updatedData.EntityName);
|
||||
|
||||
Data = updatedData;
|
||||
|
||||
// Modulate colored stripe
|
||||
NetworkColorStripe.Modulate = Data.Color;
|
||||
|
||||
// Focus updates
|
||||
if (isFocus)
|
||||
SetAsFocus();
|
||||
else
|
||||
RemoveAsFocus();
|
||||
|
||||
// Check if powered
|
||||
if (!updatedData.IsPowered)
|
||||
{
|
||||
MainDataContainer.Visible = false;
|
||||
NoDataLabel.Visible = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Set container visibility
|
||||
MainDataContainer.Visible = true;
|
||||
NoDataLabel.Visible = false;
|
||||
|
||||
// Update temperature
|
||||
var isNotVacuum = updatedData.TotalMolData > 1e-6f;
|
||||
var tempK = (FixedPoint2)updatedData.TemperatureData;
|
||||
var tempC = (FixedPoint2)TemperatureHelpers.KelvinToCelsius(tempK.Float());
|
||||
|
||||
TemperatureLabel.Text = isNotVacuum ?
|
||||
Loc.GetString("atmos-alerts-window-temperature-value", ("valueInC", tempC), ("valueInK", tempK)) :
|
||||
Loc.GetString("atmos-alerts-window-invalid-value");
|
||||
|
||||
TemperatureLabel.FontColorOverride = isNotVacuum ? Color.DarkGray : StyleNano.DisabledFore;
|
||||
|
||||
// Update pressure
|
||||
PressureLabel.Text = Loc.GetString("atmos-alerts-window-pressure-value", ("value", (FixedPoint2)updatedData.PressureData));
|
||||
PressureLabel.FontColorOverride = isNotVacuum ? Color.DarkGray : StyleNano.DisabledFore;
|
||||
|
||||
// Update total mol
|
||||
TotalMolLabel.Text = Loc.GetString("atmos-alerts-window-total-mol-value", ("value", (FixedPoint2)updatedData.TotalMolData));
|
||||
TotalMolLabel.FontColorOverride = isNotVacuum ? Color.DarkGray : StyleNano.DisabledFore;
|
||||
|
||||
// Update other present gases
|
||||
GasGridContainer.RemoveAllChildren();
|
||||
|
||||
if (updatedData.GasData.Count() == 0)
|
||||
{
|
||||
// No gases
|
||||
var gasLabel = new Label()
|
||||
{
|
||||
Text = Loc.GetString("atmos-alerts-window-other-gases-value-nil"),
|
||||
FontOverride = normalFont,
|
||||
FontColorOverride = StyleNano.DisabledFore,
|
||||
HorizontalAlignment = HAlignment.Center,
|
||||
VerticalAlignment = VAlignment.Center,
|
||||
HorizontalExpand = true,
|
||||
Margin = new Thickness(0, 2, 0, 0),
|
||||
SetHeight = 24f,
|
||||
};
|
||||
|
||||
GasGridContainer.AddChild(gasLabel);
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
// Add an entry for each gas
|
||||
foreach (var (gas, percent) in updatedData.GasData)
|
||||
{
|
||||
var gasPercent = (FixedPoint2)0f;
|
||||
gasPercent = percent * 100f;
|
||||
|
||||
var gasAbbreviation = Atmospherics.GasAbbreviations.GetValueOrDefault(gas, Loc.GetString("gas-unknown-abbreviation"));
|
||||
|
||||
var gasLabel = new Label()
|
||||
{
|
||||
Text = Loc.GetString("atmos-alerts-window-other-gases-value", ("shorthand", gasAbbreviation), ("value", gasPercent)),
|
||||
FontOverride = normalFont,
|
||||
HorizontalAlignment = HAlignment.Center,
|
||||
VerticalAlignment = VAlignment.Center,
|
||||
HorizontalExpand = true,
|
||||
Margin = new Thickness(0, 2, 0, 0),
|
||||
SetHeight = 24f,
|
||||
};
|
||||
|
||||
GasGridContainer.AddChild(gasLabel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void SetAsFocus()
|
||||
{
|
||||
FocusButton.AddStyleClass(StyleNano.StyleClassButtonColorGreen);
|
||||
ArrowTexture.TexturePath = "/Textures/Interface/Nano/inverted_triangle.svg.png";
|
||||
FocusContainer.Visible = true;
|
||||
}
|
||||
|
||||
public void RemoveAsFocus()
|
||||
{
|
||||
FocusButton.RemoveStyleClass(StyleNano.StyleClassButtonColorGreen);
|
||||
ArrowTexture.TexturePath = "/Textures/Interface/Nano/triangle_right.png";
|
||||
FocusContainer.Visible = false;
|
||||
}
|
||||
}
|
||||
23
Content.Client/Atmos/EntitySystems/GasPressurePumpSystem.cs
Normal file
23
Content.Client/Atmos/EntitySystems/GasPressurePumpSystem.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using Content.Client.Atmos.UI;
|
||||
using Content.Shared.Atmos.Components;
|
||||
using Content.Shared.Atmos.EntitySystems;
|
||||
using Content.Shared.Atmos.Piping.Binary.Components;
|
||||
|
||||
namespace Content.Client.Atmos.EntitySystems;
|
||||
|
||||
public sealed class GasPressurePumpSystem : SharedGasPressurePumpSystem
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<GasPressurePumpComponent, AfterAutoHandleStateEvent>(OnPumpUpdate);
|
||||
}
|
||||
|
||||
private void OnPumpUpdate(Entity<GasPressurePumpComponent> ent, ref AfterAutoHandleStateEvent args)
|
||||
{
|
||||
if (UserInterfaceSystem.TryGetOpenUi<GasPressurePumpBoundUserInterface>(ent.Owner, GasPressurePumpUiKey.Key, out var bui))
|
||||
{
|
||||
bui.Update();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,65 +1,63 @@
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Atmos.Components;
|
||||
using Content.Shared.Atmos.Piping.Binary.Components;
|
||||
using Content.Shared.IdentityManagement;
|
||||
using Content.Shared.Localizations;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.UserInterface;
|
||||
|
||||
namespace Content.Client.Atmos.UI
|
||||
namespace Content.Client.Atmos.UI;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a <see cref="GasPressurePumpWindow"/> and updates it when new server messages are received.
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public sealed class GasPressurePumpBoundUserInterface : BoundUserInterface
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a <see cref="GasPressurePumpWindow"/> and updates it when new server messages are received.
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public sealed class GasPressurePumpBoundUserInterface : BoundUserInterface
|
||||
[ViewVariables]
|
||||
private const float MaxPressure = Atmospherics.MaxOutputPressure;
|
||||
|
||||
[ViewVariables]
|
||||
private GasPressurePumpWindow? _window;
|
||||
|
||||
public GasPressurePumpBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
|
||||
{
|
||||
[ViewVariables]
|
||||
private const float MaxPressure = Atmospherics.MaxOutputPressure;
|
||||
}
|
||||
|
||||
[ViewVariables]
|
||||
private GasPressurePumpWindow? _window;
|
||||
protected override void Open()
|
||||
{
|
||||
base.Open();
|
||||
|
||||
public GasPressurePumpBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
|
||||
{
|
||||
}
|
||||
_window = this.CreateWindow<GasPressurePumpWindow>();
|
||||
|
||||
protected override void Open()
|
||||
{
|
||||
base.Open();
|
||||
_window.ToggleStatusButtonPressed += OnToggleStatusButtonPressed;
|
||||
_window.PumpOutputPressureChanged += OnPumpOutputPressurePressed;
|
||||
Update();
|
||||
}
|
||||
|
||||
_window = this.CreateWindow<GasPressurePumpWindow>();
|
||||
public void Update()
|
||||
{
|
||||
if (_window == null)
|
||||
return;
|
||||
|
||||
_window.ToggleStatusButtonPressed += OnToggleStatusButtonPressed;
|
||||
_window.PumpOutputPressureChanged += OnPumpOutputPressurePressed;
|
||||
}
|
||||
_window.Title = Identity.Name(Owner, EntMan);
|
||||
|
||||
private void OnToggleStatusButtonPressed()
|
||||
{
|
||||
if (_window is null) return;
|
||||
SendMessage(new GasPressurePumpToggleStatusMessage(_window.PumpStatus));
|
||||
}
|
||||
if (!EntMan.TryGetComponent(Owner, out GasPressurePumpComponent? pump))
|
||||
return;
|
||||
|
||||
private void OnPumpOutputPressurePressed(string value)
|
||||
{
|
||||
var pressure = UserInputParser.TryFloat(value, out var parsed) ? parsed : 0f;
|
||||
if (pressure > MaxPressure) pressure = MaxPressure;
|
||||
_window.SetPumpStatus(pump.Enabled);
|
||||
_window.MaxPressure = pump.MaxTargetPressure;
|
||||
_window.SetOutputPressure(pump.TargetPressure);
|
||||
}
|
||||
|
||||
SendMessage(new GasPressurePumpChangeOutputPressureMessage(pressure));
|
||||
}
|
||||
private void OnToggleStatusButtonPressed()
|
||||
{
|
||||
if (_window is null) return;
|
||||
SendPredictedMessage(new GasPressurePumpToggleStatusMessage(_window.PumpStatus));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the UI state based on server-sent info
|
||||
/// </summary>
|
||||
/// <param name="state"></param>
|
||||
protected override void UpdateState(BoundUserInterfaceState state)
|
||||
{
|
||||
base.UpdateState(state);
|
||||
if (_window == null || state is not GasPressurePumpBoundUserInterfaceState cast)
|
||||
return;
|
||||
|
||||
_window.Title = (cast.PumpLabel);
|
||||
_window.SetPumpStatus(cast.Enabled);
|
||||
_window.SetOutputPressure(cast.OutputPressure);
|
||||
}
|
||||
private void OnPumpOutputPressurePressed(float value)
|
||||
{
|
||||
SendPredictedMessage(new GasPressurePumpChangeOutputPressureMessage(value));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,18 @@
|
||||
<DefaultWindow xmlns="https://spacestation14.io"
|
||||
<controls:FancyWindow xmlns="https://spacestation14.io"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
MinSize="200 120" Title="Pressure Pump">
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
SetSize="340 110" MinSize="340 110" Title="Pressure Pump">
|
||||
<BoxContainer Orientation="Vertical" Margin="5 5 5 5" SeparationOverride="10">
|
||||
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
|
||||
<Label Text="{Loc comp-gas-pump-ui-pump-status}"/>
|
||||
<Control MinSize="5 0" />
|
||||
<Label Text="{Loc comp-gas-pump-ui-pump-status}" Margin="0 0 5 0"/>
|
||||
<Button Name="ToggleStatusButton"/>
|
||||
<Control HorizontalExpand="True"/>
|
||||
<Button HorizontalAlignment="Right" Name="SetOutputPressureButton" Text="{Loc comp-gas-pump-ui-pump-set-rate}" Disabled="True" Margin="0 0 5 0"/>
|
||||
<Button Name="SetMaxPressureButton" Text="{Loc comp-gas-pump-ui-pump-set-max}" />
|
||||
</BoxContainer>
|
||||
|
||||
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
|
||||
<Label Text="{Loc comp-gas-pump-ui-pump-output-pressure}"/>
|
||||
<Control MinSize="5 0" />
|
||||
<LineEdit Name="PumpPressureOutputInput" MinSize="70 0" />
|
||||
<Control MinSize="5 0" />
|
||||
<Button Name="SetMaxPressureButton" Text="{Loc comp-gas-pump-ui-pump-set-max}" />
|
||||
<Control MinSize="5 0" />
|
||||
<Control HorizontalExpand="True" />
|
||||
<Button Name="SetOutputPressureButton" Text="{Loc comp-gas-pump-ui-pump-set-rate}" HorizontalAlignment="Right" Disabled="True"/>
|
||||
<FloatSpinBox HorizontalExpand="True" Name="PumpPressureOutputInput" MinSize="70 0" />
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</DefaultWindow>
|
||||
</controls:FancyWindow>
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using Content.Client.Atmos.EntitySystems;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Atmos.Prototypes;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Localization;
|
||||
|
||||
namespace Content.Client.Atmos.UI
|
||||
{
|
||||
@@ -16,12 +10,25 @@ namespace Content.Client.Atmos.UI
|
||||
/// Client-side UI used to control a gas pressure pump.
|
||||
/// </summary>
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class GasPressurePumpWindow : DefaultWindow
|
||||
public sealed partial class GasPressurePumpWindow : FancyWindow
|
||||
{
|
||||
public bool PumpStatus = true;
|
||||
|
||||
public event Action? ToggleStatusButtonPressed;
|
||||
public event Action<string>? PumpOutputPressureChanged;
|
||||
public event Action<float>? PumpOutputPressureChanged;
|
||||
|
||||
public float MaxPressure
|
||||
{
|
||||
get => _maxPressure;
|
||||
set
|
||||
{
|
||||
_maxPressure = value;
|
||||
|
||||
PumpPressureOutputInput.Value = MathF.Min(value, PumpPressureOutputInput.Value);
|
||||
}
|
||||
}
|
||||
|
||||
private float _maxPressure = Atmospherics.MaxOutputPressure;
|
||||
|
||||
public GasPressurePumpWindow()
|
||||
{
|
||||
@@ -30,23 +37,25 @@ namespace Content.Client.Atmos.UI
|
||||
ToggleStatusButton.OnPressed += _ => SetPumpStatus(!PumpStatus);
|
||||
ToggleStatusButton.OnPressed += _ => ToggleStatusButtonPressed?.Invoke();
|
||||
|
||||
PumpPressureOutputInput.OnTextChanged += _ => SetOutputPressureButton.Disabled = false;
|
||||
PumpPressureOutputInput.OnValueChanged += _ => SetOutputPressureButton.Disabled = false;
|
||||
|
||||
SetOutputPressureButton.OnPressed += _ =>
|
||||
{
|
||||
PumpOutputPressureChanged?.Invoke(PumpPressureOutputInput.Text ??= "");
|
||||
PumpPressureOutputInput.Value = Math.Clamp(PumpPressureOutputInput.Value, 0f, _maxPressure);
|
||||
PumpOutputPressureChanged?.Invoke(PumpPressureOutputInput.Value);
|
||||
SetOutputPressureButton.Disabled = true;
|
||||
};
|
||||
|
||||
SetMaxPressureButton.OnPressed += _ =>
|
||||
{
|
||||
PumpPressureOutputInput.Text = Atmospherics.MaxOutputPressure.ToString(CultureInfo.CurrentCulture);
|
||||
PumpPressureOutputInput.Value = _maxPressure;
|
||||
SetOutputPressureButton.Disabled = false;
|
||||
};
|
||||
}
|
||||
|
||||
public void SetOutputPressure(float pressure)
|
||||
{
|
||||
PumpPressureOutputInput.Text = pressure.ToString(CultureInfo.CurrentCulture);
|
||||
PumpPressureOutputInput.Value = pressure;
|
||||
}
|
||||
|
||||
public void SetPumpStatus(bool enabled)
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.Numerics;
|
||||
using Content.Client.Chat.Managers;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Chat;
|
||||
using Content.Shared.Speech;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
@@ -141,7 +142,12 @@ namespace Content.Client.Chat.UI
|
||||
Modulate = Color.White;
|
||||
}
|
||||
|
||||
var offset = (-_eyeManager.CurrentEye.Rotation).ToWorldVec() * -EntityVerticalOffset;
|
||||
var baseOffset = 0f;
|
||||
|
||||
if (_entityManager.TryGetComponent<SpeechComponent>(_senderEntity, out var speech))
|
||||
baseOffset = speech.SpeechBubbleOffset;
|
||||
|
||||
var offset = (-_eyeManager.CurrentEye.Rotation).ToWorldVec() * -(EntityVerticalOffset + baseOffset);
|
||||
var worldPos = _transformSystem.GetWorldPosition(xform) + offset;
|
||||
|
||||
var lowerCenter = _eyeManager.WorldToScreen(worldPos) / UIScale;
|
||||
|
||||
@@ -12,6 +12,7 @@ using Robust.Shared.Utility;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Robust.Client.Graphics;
|
||||
using static Robust.Client.UserInterface.Controls.BoxContainer;
|
||||
|
||||
namespace Content.Client.Chemistry.UI
|
||||
@@ -90,10 +91,40 @@ namespace Content.Client.Chemistry.UI
|
||||
|
||||
private ReagentButton MakeReagentButton(string text, ChemMasterReagentAmount amount, ReagentId id, bool isBuffer, string styleClass)
|
||||
{
|
||||
var button = new ReagentButton(text, amount, id, isBuffer, styleClass);
|
||||
button.OnPressed += args
|
||||
=> OnReagentButtonPressed?.Invoke(args, button);
|
||||
return button;
|
||||
var reagentTransferButton = new ReagentButton(text, amount, id, isBuffer, styleClass);
|
||||
reagentTransferButton.OnPressed += args
|
||||
=> OnReagentButtonPressed?.Invoke(args, reagentTransferButton);
|
||||
return reagentTransferButton;
|
||||
}
|
||||
/// <summary>
|
||||
/// Conditionally generates a set of reagent buttons based on the supplied boolean argument.
|
||||
/// This was moved outside of BuildReagentRow to facilitate conditional logic, stops indentation depth getting out of hand as well.
|
||||
/// </summary>
|
||||
private List<ReagentButton> CreateReagentTransferButtons(ReagentId reagent, bool isBuffer, bool addReagentButtons)
|
||||
{
|
||||
if (!addReagentButtons)
|
||||
return new List<ReagentButton>(); // Return an empty list if reagentTransferButton creation is disabled.
|
||||
|
||||
var buttonConfigs = new (string text, ChemMasterReagentAmount amount, string styleClass)[]
|
||||
{
|
||||
("1", ChemMasterReagentAmount.U1, StyleBase.ButtonOpenBoth),
|
||||
("5", ChemMasterReagentAmount.U5, StyleBase.ButtonOpenBoth),
|
||||
("10", ChemMasterReagentAmount.U10, StyleBase.ButtonOpenBoth),
|
||||
("25", ChemMasterReagentAmount.U25, StyleBase.ButtonOpenBoth),
|
||||
("50", ChemMasterReagentAmount.U50, StyleBase.ButtonOpenBoth),
|
||||
("100", ChemMasterReagentAmount.U100, StyleBase.ButtonOpenBoth),
|
||||
(Loc.GetString("chem-master-window-buffer-all-amount"), ChemMasterReagentAmount.All, StyleBase.ButtonOpenLeft),
|
||||
};
|
||||
|
||||
var buttons = new List<ReagentButton>();
|
||||
|
||||
foreach (var (text, amount, styleClass) in buttonConfigs)
|
||||
{
|
||||
var reagentTransferButton = MakeReagentButton(text, amount, reagent, isBuffer, styleClass);
|
||||
buttons.Add(reagentTransferButton);
|
||||
}
|
||||
|
||||
return buttons;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -102,24 +133,35 @@ namespace Content.Client.Chemistry.UI
|
||||
/// <param name="state">State data sent by the server.</param>
|
||||
public void UpdateState(BoundUserInterfaceState state)
|
||||
{
|
||||
var castState = (ChemMasterBoundUserInterfaceState) state;
|
||||
var castState = (ChemMasterBoundUserInterfaceState)state;
|
||||
|
||||
if (castState.UpdateLabel)
|
||||
LabelLine = GenerateLabel(castState);
|
||||
UpdatePanelInfo(castState);
|
||||
|
||||
var output = castState.OutputContainerInfo;
|
||||
// Ensure the Panel Info is updated, including UI elements for Buffer Volume, Output Container and so on
|
||||
UpdatePanelInfo(castState);
|
||||
|
||||
BufferCurrentVolume.Text = $" {castState.BufferCurrentVolume?.Int() ?? 0}u";
|
||||
|
||||
InputEjectButton.Disabled = castState.InputContainerInfo is null;
|
||||
OutputEjectButton.Disabled = output is null;
|
||||
CreateBottleButton.Disabled = output?.Reagents == null;
|
||||
CreatePillButton.Disabled = output?.Entities == null;
|
||||
OutputEjectButton.Disabled = castState.OutputContainerInfo is null;
|
||||
CreateBottleButton.Disabled = castState.OutputContainerInfo?.Reagents == null;
|
||||
CreatePillButton.Disabled = castState.OutputContainerInfo?.Entities == null;
|
||||
|
||||
UpdateDosageFields(castState);
|
||||
}
|
||||
|
||||
//assign default values for pill and bottle fields.
|
||||
private void UpdateDosageFields(ChemMasterBoundUserInterfaceState castState)
|
||||
{
|
||||
var output = castState.OutputContainerInfo;
|
||||
var remainingCapacity = output is null ? 0 : (output.MaxVolume - output.CurrentVolume).Int();
|
||||
var holdsReagents = output?.Reagents != null;
|
||||
var pillNumberMax = holdsReagents ? 0 : remainingCapacity;
|
||||
var bottleAmountMax = holdsReagents ? remainingCapacity : 0;
|
||||
var bufferVolume = castState.BufferCurrentVolume?.Int() ?? 0;
|
||||
|
||||
PillDosage.Value = (int)Math.Min(bufferVolume, castState.PillDosageLimit);
|
||||
|
||||
PillTypeButtons[castState.SelectedPillType].Pressed = true;
|
||||
PillNumber.IsValid = x => x >= 0 && x <= pillNumberMax;
|
||||
@@ -130,8 +172,19 @@ namespace Content.Client.Chemistry.UI
|
||||
PillNumber.Value = pillNumberMax;
|
||||
if (BottleDosage.Value > bottleAmountMax)
|
||||
BottleDosage.Value = bottleAmountMax;
|
||||
}
|
||||
|
||||
// Avoid division by zero
|
||||
if (PillDosage.Value > 0)
|
||||
{
|
||||
PillNumber.Value = Math.Min(bufferVolume / PillDosage.Value, pillNumberMax);
|
||||
}
|
||||
else
|
||||
{
|
||||
PillNumber.Value = 0;
|
||||
}
|
||||
|
||||
BottleDosage.Value = Math.Min(bottleAmountMax, bufferVolume);
|
||||
}
|
||||
/// <summary>
|
||||
/// Generate a product label based on reagents in the buffer.
|
||||
/// </summary>
|
||||
@@ -178,43 +231,20 @@ namespace Content.Client.Chemistry.UI
|
||||
var bufferVol = new Label
|
||||
{
|
||||
Text = $"{state.BufferCurrentVolume}u",
|
||||
StyleClasses = {StyleNano.StyleClassLabelSecondaryColor}
|
||||
StyleClasses = { StyleNano.StyleClassLabelSecondaryColor }
|
||||
};
|
||||
bufferHBox.AddChild(bufferVol);
|
||||
|
||||
// initialises rowCount to allow for striped rows
|
||||
|
||||
var rowCount = 0;
|
||||
foreach (var (reagent, quantity) in state.BufferReagents)
|
||||
{
|
||||
// Try to get the prototype for the given reagent. This gives us its name.
|
||||
_prototypeManager.TryIndex(reagent.Prototype, out ReagentPrototype? proto);
|
||||
var reagentId = reagent;
|
||||
_prototypeManager.TryIndex(reagentId.Prototype, out ReagentPrototype? proto);
|
||||
var name = proto?.LocalizedName ?? Loc.GetString("chem-master-window-unknown-reagent-text");
|
||||
|
||||
if (proto != null)
|
||||
{
|
||||
BufferInfo.Children.Add(new BoxContainer
|
||||
{
|
||||
Orientation = LayoutOrientation.Horizontal,
|
||||
Children =
|
||||
{
|
||||
new Label {Text = $"{name}: "},
|
||||
new Label
|
||||
{
|
||||
Text = $"{quantity}u",
|
||||
StyleClasses = {StyleNano.StyleClassLabelSecondaryColor}
|
||||
},
|
||||
|
||||
// Padding
|
||||
new Control {HorizontalExpand = true},
|
||||
|
||||
MakeReagentButton("1", ChemMasterReagentAmount.U1, reagent, true, StyleBase.ButtonOpenRight),
|
||||
MakeReagentButton("5", ChemMasterReagentAmount.U5, reagent, true, StyleBase.ButtonOpenBoth),
|
||||
MakeReagentButton("10", ChemMasterReagentAmount.U10, reagent, true, StyleBase.ButtonOpenBoth),
|
||||
MakeReagentButton("25", ChemMasterReagentAmount.U25, reagent, true, StyleBase.ButtonOpenBoth),
|
||||
MakeReagentButton("50", ChemMasterReagentAmount.U50, reagent, true, StyleBase.ButtonOpenBoth),
|
||||
MakeReagentButton("100", ChemMasterReagentAmount.U100, reagent, true, StyleBase.ButtonOpenBoth),
|
||||
MakeReagentButton(Loc.GetString("chem-master-window-buffer-all-amount"), ChemMasterReagentAmount.All, reagent, true, StyleBase.ButtonOpenLeft),
|
||||
}
|
||||
});
|
||||
}
|
||||
var reagentColor = proto?.SubstanceColor ?? default(Color);
|
||||
BufferInfo.Children.Add(BuildReagentRow(reagentColor, rowCount++, name, reagentId, quantity, true, true));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,104 +258,111 @@ namespace Content.Client.Chemistry.UI
|
||||
{
|
||||
Text = Loc.GetString("chem-master-window-no-container-loaded-text")
|
||||
});
|
||||
return;
|
||||
}
|
||||
else
|
||||
|
||||
// Name of the container and its fill status (Ex: 44/100u)
|
||||
control.Children.Add(new BoxContainer
|
||||
{
|
||||
// Name of the container and its fill status (Ex: 44/100u)
|
||||
control.Children.Add(new BoxContainer
|
||||
Orientation = LayoutOrientation.Horizontal,
|
||||
Children =
|
||||
{
|
||||
Orientation = LayoutOrientation.Horizontal,
|
||||
Children =
|
||||
new Label { Text = $"{info.DisplayName}: " },
|
||||
new Label
|
||||
{
|
||||
new Label {Text = $"{info.DisplayName}: "},
|
||||
new Label
|
||||
{
|
||||
Text = $"{info.CurrentVolume}/{info.MaxVolume}",
|
||||
StyleClasses = {StyleNano.StyleClassLabelSecondaryColor}
|
||||
}
|
||||
Text = $"{info.CurrentVolume}/{info.MaxVolume}",
|
||||
StyleClasses = { StyleNano.StyleClassLabelSecondaryColor }
|
||||
}
|
||||
});
|
||||
|
||||
IEnumerable<(string Name, ReagentId Id, FixedPoint2 Quantity)> contents;
|
||||
|
||||
if (info.Entities != null)
|
||||
{
|
||||
contents = info.Entities.Select(x => (x.Id, default(ReagentId), x.Quantity));
|
||||
}
|
||||
else if (info.Reagents != null)
|
||||
});
|
||||
// Initialises rowCount to allow for striped rows
|
||||
var rowCount = 0;
|
||||
|
||||
// Handle entities if they are not null
|
||||
if (info.Entities != null)
|
||||
{
|
||||
foreach (var (id, quantity) in info.Entities.Select(x => (x.Id, x.Quantity)))
|
||||
{
|
||||
contents = info.Reagents.Select(x =>
|
||||
{
|
||||
_prototypeManager.TryIndex(x.Reagent.Prototype, out ReagentPrototype? proto);
|
||||
var name = proto?.LocalizedName
|
||||
?? Loc.GetString("chem-master-window-unknown-reagent-text");
|
||||
|
||||
return (name, Id: x.Reagent, x.Quantity);
|
||||
})
|
||||
.OrderBy(r => r.Item1);
|
||||
control.Children.Add(BuildReagentRow(default(Color), rowCount++, id, default(ReagentId), quantity, false, addReagentButtons));
|
||||
}
|
||||
else
|
||||
}
|
||||
|
||||
// Handle reagents if they are not null
|
||||
if (info.Reagents != null)
|
||||
{
|
||||
foreach (var reagent in info.Reagents)
|
||||
{
|
||||
return;
|
||||
_prototypeManager.TryIndex(reagent.Reagent.Prototype, out ReagentPrototype? proto);
|
||||
var name = proto?.LocalizedName ?? Loc.GetString("chem-master-window-unknown-reagent-text");
|
||||
var reagentColor = proto?.SubstanceColor ?? default(Color);
|
||||
|
||||
control.Children.Add(BuildReagentRow(reagentColor, rowCount++, name, reagent.Reagent, reagent.Quantity, false, addReagentButtons));
|
||||
}
|
||||
|
||||
|
||||
foreach (var (name, id, quantity) in contents)
|
||||
{
|
||||
var inner = new BoxContainer
|
||||
{
|
||||
Orientation = LayoutOrientation.Horizontal,
|
||||
Children =
|
||||
{
|
||||
new Label { Text = $"{name}: " },
|
||||
new Label
|
||||
{
|
||||
Text = $"{quantity}u",
|
||||
StyleClasses = { StyleNano.StyleClassLabelSecondaryColor },
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (addReagentButtons)
|
||||
{
|
||||
var cs = inner.Children;
|
||||
|
||||
// Padding
|
||||
cs.Add(new Control { HorizontalExpand = true });
|
||||
|
||||
cs.Add(MakeReagentButton(
|
||||
"1", ChemMasterReagentAmount.U1, id, false, StyleBase.ButtonOpenRight));
|
||||
cs.Add(MakeReagentButton(
|
||||
"5", ChemMasterReagentAmount.U5, id, false, StyleBase.ButtonOpenBoth));
|
||||
cs.Add(MakeReagentButton(
|
||||
"10", ChemMasterReagentAmount.U10, id, false, StyleBase.ButtonOpenBoth));
|
||||
cs.Add(MakeReagentButton(
|
||||
"25", ChemMasterReagentAmount.U25, id, false, StyleBase.ButtonOpenBoth));
|
||||
cs.Add(MakeReagentButton(
|
||||
"50", ChemMasterReagentAmount.U50, id, false, StyleBase.ButtonOpenBoth));
|
||||
cs.Add(MakeReagentButton(
|
||||
"100", ChemMasterReagentAmount.U100, id, false, StyleBase.ButtonOpenBoth));
|
||||
cs.Add(MakeReagentButton(
|
||||
Loc.GetString("chem-master-window-buffer-all-amount"),
|
||||
ChemMasterReagentAmount.All, id, false, StyleBase.ButtonOpenLeft));
|
||||
}
|
||||
|
||||
control.Children.Add(inner);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public String LabelLine
|
||||
/// <summary>
|
||||
/// Take reagent/entity data and present rows, labels, and buttons appropriately. todo sprites?
|
||||
/// </summary>
|
||||
private Control BuildReagentRow(Color reagentColor, int rowCount, string name, ReagentId reagent, FixedPoint2 quantity, bool isBuffer, bool addReagentButtons)
|
||||
{
|
||||
get
|
||||
//Colors rows and sets fallback for reagentcolor to the same as background, this will hide colorPanel for entities hopefully
|
||||
var rowColor1 = Color.FromHex("#1B1B1E");
|
||||
var rowColor2 = Color.FromHex("#202025");
|
||||
var currentRowColor = (rowCount % 2 == 1) ? rowColor1 : rowColor2;
|
||||
if ((reagentColor == default(Color))|(!addReagentButtons))
|
||||
{
|
||||
return LabelLineEdit.Text;
|
||||
reagentColor = currentRowColor;
|
||||
}
|
||||
set
|
||||
//this calls the separated button builder, and stores the return to render after labels
|
||||
var reagentButtonConstructors = CreateReagentTransferButtons(reagent, isBuffer, addReagentButtons);
|
||||
|
||||
// Create the row layout with the color panel
|
||||
var rowContainer = new BoxContainer
|
||||
{
|
||||
LabelLineEdit.Text = value;
|
||||
Orientation = LayoutOrientation.Horizontal,
|
||||
Children =
|
||||
{
|
||||
new Label { Text = $"{name}: " },
|
||||
new Label
|
||||
{
|
||||
Text = $"{quantity}u",
|
||||
StyleClasses = { StyleNano.StyleClassLabelSecondaryColor }
|
||||
},
|
||||
|
||||
// Padding
|
||||
new Control { HorizontalExpand = true },
|
||||
// Colored panels for reagents
|
||||
new PanelContainer
|
||||
{
|
||||
Name = "colorPanel",
|
||||
VerticalExpand = true,
|
||||
MinWidth = 4,
|
||||
PanelOverride = new StyleBoxFlat
|
||||
{
|
||||
BackgroundColor = reagentColor
|
||||
},
|
||||
Margin = new Thickness(0, 1)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Add the reagent buttons after the color panel
|
||||
foreach (var reagentTransferButton in reagentButtonConstructors)
|
||||
{
|
||||
rowContainer.AddChild(reagentTransferButton);
|
||||
}
|
||||
//Apply panencontainer to allow for striped rows
|
||||
return new PanelContainer
|
||||
{
|
||||
PanelOverride = new StyleBoxFlat(currentRowColor),
|
||||
Children = { rowContainer }
|
||||
};
|
||||
}
|
||||
|
||||
public string LabelLine
|
||||
{
|
||||
get => LabelLineEdit.Text;
|
||||
set => LabelLineEdit.Text = value;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
8
Content.Client/Explosion/ScatteringGrenadeSystem.cs
Normal file
8
Content.Client/Explosion/ScatteringGrenadeSystem.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using Content.Shared.Explosion.EntitySystems;
|
||||
|
||||
namespace Content.Client.Explosion;
|
||||
|
||||
public sealed class ScatteringGrenadeSystem : SharedScatteringGrenadeSystem
|
||||
{
|
||||
|
||||
}
|
||||
101
Content.Client/Holopad/HolopadBoundUserInterface.cs
Normal file
101
Content.Client/Holopad/HolopadBoundUserInterface.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
using Content.Shared.Holopad;
|
||||
using Content.Shared.Silicons.StationAi;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared.Player;
|
||||
using System.Numerics;
|
||||
|
||||
namespace Content.Client.Holopad;
|
||||
|
||||
public sealed class HolopadBoundUserInterface : BoundUserInterface
|
||||
{
|
||||
[Dependency] private readonly ISharedPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly IClyde _displayManager = default!;
|
||||
|
||||
[ViewVariables]
|
||||
private HolopadWindow? _window;
|
||||
|
||||
public HolopadBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
}
|
||||
|
||||
protected override void Open()
|
||||
{
|
||||
base.Open();
|
||||
|
||||
_window = this.CreateWindow<HolopadWindow>();
|
||||
_window.Title = Loc.GetString("holopad-window-title", ("title", EntMan.GetComponent<MetaDataComponent>(Owner).EntityName));
|
||||
|
||||
if (this.UiKey is not HolopadUiKey)
|
||||
{
|
||||
Close();
|
||||
return;
|
||||
}
|
||||
|
||||
var uiKey = (HolopadUiKey)this.UiKey;
|
||||
|
||||
// AIs will see a different holopad interface to crew when interacting with them in the world
|
||||
if (uiKey == HolopadUiKey.InteractionWindow && EntMan.HasComponent<StationAiHeldComponent>(_playerManager.LocalEntity))
|
||||
uiKey = HolopadUiKey.InteractionWindowForAi;
|
||||
|
||||
_window.SetState(Owner, uiKey);
|
||||
_window.UpdateState(new Dictionary<NetEntity, string>());
|
||||
|
||||
// Set message actions
|
||||
_window.SendHolopadStartNewCallMessageAction += SendHolopadStartNewCallMessage;
|
||||
_window.SendHolopadAnswerCallMessageAction += SendHolopadAnswerCallMessage;
|
||||
_window.SendHolopadEndCallMessageAction += SendHolopadEndCallMessage;
|
||||
_window.SendHolopadStartBroadcastMessageAction += SendHolopadStartBroadcastMessage;
|
||||
_window.SendHolopadActivateProjectorMessageAction += SendHolopadActivateProjectorMessage;
|
||||
_window.SendHolopadRequestStationAiMessageAction += SendHolopadRequestStationAiMessage;
|
||||
|
||||
// If this call is addressed to an AI, open the window in the bottom right hand corner of the screen
|
||||
if (uiKey == HolopadUiKey.AiRequestWindow)
|
||||
_window.OpenCenteredAt(new Vector2(1f, 1f));
|
||||
|
||||
// Otherwise offset to the left so the holopad can still be seen
|
||||
else
|
||||
_window.OpenCenteredAt(new Vector2(0.3333f, 0.50f));
|
||||
}
|
||||
|
||||
protected override void UpdateState(BoundUserInterfaceState state)
|
||||
{
|
||||
base.UpdateState(state);
|
||||
|
||||
var castState = (HolopadBoundInterfaceState)state;
|
||||
EntMan.TryGetComponent<TransformComponent>(Owner, out var xform);
|
||||
|
||||
_window?.UpdateState(castState.Holopads);
|
||||
}
|
||||
|
||||
public void SendHolopadStartNewCallMessage(NetEntity receiver)
|
||||
{
|
||||
SendMessage(new HolopadStartNewCallMessage(receiver));
|
||||
}
|
||||
|
||||
public void SendHolopadAnswerCallMessage()
|
||||
{
|
||||
SendMessage(new HolopadAnswerCallMessage());
|
||||
}
|
||||
|
||||
public void SendHolopadEndCallMessage()
|
||||
{
|
||||
SendMessage(new HolopadEndCallMessage());
|
||||
}
|
||||
|
||||
public void SendHolopadStartBroadcastMessage()
|
||||
{
|
||||
SendMessage(new HolopadStartBroadcastMessage());
|
||||
}
|
||||
|
||||
public void SendHolopadActivateProjectorMessage()
|
||||
{
|
||||
SendMessage(new HolopadActivateProjectorMessage());
|
||||
}
|
||||
|
||||
public void SendHolopadRequestStationAiMessage()
|
||||
{
|
||||
SendMessage(new HolopadStationAiRequestMessage());
|
||||
}
|
||||
}
|
||||
172
Content.Client/Holopad/HolopadSystem.cs
Normal file
172
Content.Client/Holopad/HolopadSystem.cs
Normal file
@@ -0,0 +1,172 @@
|
||||
using Content.Shared.Chat.TypingIndicator;
|
||||
using Content.Shared.Holopad;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Timing;
|
||||
using System.Linq;
|
||||
|
||||
namespace Content.Client.Holopad;
|
||||
|
||||
public sealed class HolopadSystem : SharedHolopadSystem
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<HolopadHologramComponent, ComponentInit>(OnComponentInit);
|
||||
SubscribeLocalEvent<HolopadHologramComponent, BeforePostShaderRenderEvent>(OnShaderRender);
|
||||
SubscribeAllEvent<TypingChangedEvent>(OnTypingChanged);
|
||||
|
||||
SubscribeNetworkEvent<PlayerSpriteStateRequest>(OnPlayerSpriteStateRequest);
|
||||
SubscribeNetworkEvent<PlayerSpriteStateMessage>(OnPlayerSpriteStateMessage);
|
||||
}
|
||||
|
||||
private void OnComponentInit(EntityUid uid, HolopadHologramComponent component, ComponentInit ev)
|
||||
{
|
||||
if (!TryComp<SpriteComponent>(uid, out var sprite))
|
||||
return;
|
||||
|
||||
UpdateHologramSprite(uid);
|
||||
}
|
||||
|
||||
private void OnShaderRender(EntityUid uid, HolopadHologramComponent component, BeforePostShaderRenderEvent ev)
|
||||
{
|
||||
if (ev.Sprite.PostShader == null)
|
||||
return;
|
||||
|
||||
ev.Sprite.PostShader.SetParameter("t", (float)_timing.CurTime.TotalSeconds * component.ScrollRate);
|
||||
}
|
||||
|
||||
private void OnTypingChanged(TypingChangedEvent ev, EntitySessionEventArgs args)
|
||||
{
|
||||
var uid = args.SenderSession.AttachedEntity;
|
||||
|
||||
if (!Exists(uid))
|
||||
return;
|
||||
|
||||
if (!HasComp<HolopadUserComponent>(uid))
|
||||
return;
|
||||
|
||||
var netEv = new HolopadUserTypingChangedEvent(GetNetEntity(uid.Value), ev.IsTyping);
|
||||
RaiseNetworkEvent(netEv);
|
||||
}
|
||||
|
||||
private void OnPlayerSpriteStateRequest(PlayerSpriteStateRequest ev)
|
||||
{
|
||||
var targetPlayer = GetEntity(ev.TargetPlayer);
|
||||
var player = _playerManager.LocalSession?.AttachedEntity;
|
||||
|
||||
// Ignore the request if received by a player who isn't the target
|
||||
if (targetPlayer != player)
|
||||
return;
|
||||
|
||||
if (!TryComp<SpriteComponent>(player, out var playerSprite))
|
||||
return;
|
||||
|
||||
var spriteLayerData = new List<PrototypeLayerData>();
|
||||
|
||||
if (playerSprite.Visible)
|
||||
{
|
||||
// Record the RSI paths, state names and shader paramaters of all visible layers
|
||||
for (int i = 0; i < playerSprite.AllLayers.Count(); i++)
|
||||
{
|
||||
if (!playerSprite.TryGetLayer(i, out var layer))
|
||||
continue;
|
||||
|
||||
if (!layer.Visible ||
|
||||
string.IsNullOrEmpty(layer.ActualRsi?.Path.ToString()) ||
|
||||
string.IsNullOrEmpty(layer.State.Name))
|
||||
continue;
|
||||
|
||||
var layerDatum = new PrototypeLayerData();
|
||||
layerDatum.RsiPath = layer.ActualRsi.Path.ToString();
|
||||
layerDatum.State = layer.State.Name;
|
||||
|
||||
if (layer.CopyToShaderParameters != null)
|
||||
{
|
||||
var key = (string)layer.CopyToShaderParameters.LayerKey;
|
||||
|
||||
if (playerSprite.LayerMapTryGet(key, out var otherLayerIdx) &&
|
||||
playerSprite.TryGetLayer(otherLayerIdx, out var otherLayer) &&
|
||||
otherLayer.Visible)
|
||||
{
|
||||
layerDatum.MapKeys = new() { key };
|
||||
|
||||
layerDatum.CopyToShaderParameters = new PrototypeCopyToShaderParameters()
|
||||
{
|
||||
LayerKey = key,
|
||||
ParameterTexture = layer.CopyToShaderParameters.ParameterTexture,
|
||||
ParameterUV = layer.CopyToShaderParameters.ParameterUV
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
spriteLayerData.Add(layerDatum);
|
||||
}
|
||||
}
|
||||
|
||||
// Return the recorded data to the server
|
||||
var evResponse = new PlayerSpriteStateMessage(ev.TargetPlayer, spriteLayerData.ToArray());
|
||||
RaiseNetworkEvent(evResponse);
|
||||
}
|
||||
|
||||
private void OnPlayerSpriteStateMessage(PlayerSpriteStateMessage ev)
|
||||
{
|
||||
UpdateHologramSprite(GetEntity(ev.SpriteEntity), ev.SpriteLayerData);
|
||||
}
|
||||
|
||||
private void UpdateHologramSprite(EntityUid uid, PrototypeLayerData[]? layerData = null)
|
||||
{
|
||||
if (!TryComp<SpriteComponent>(uid, out var hologramSprite))
|
||||
return;
|
||||
|
||||
if (!TryComp<HolopadHologramComponent>(uid, out var holopadhologram))
|
||||
return;
|
||||
|
||||
for (int i = hologramSprite.AllLayers.Count() - 1; i >= 0; i--)
|
||||
hologramSprite.RemoveLayer(i);
|
||||
|
||||
if (layerData == null || layerData.Length == 0)
|
||||
{
|
||||
layerData = new PrototypeLayerData[1];
|
||||
layerData[0] = new PrototypeLayerData()
|
||||
{
|
||||
RsiPath = holopadhologram.RsiPath,
|
||||
State = holopadhologram.RsiState
|
||||
};
|
||||
}
|
||||
|
||||
for (int i = 0; i < layerData.Length; i++)
|
||||
{
|
||||
var layer = layerData[i];
|
||||
layer.Shader = "unshaded";
|
||||
|
||||
hologramSprite.AddLayer(layerData[i], i);
|
||||
}
|
||||
|
||||
UpdateHologramShader(uid, hologramSprite, holopadhologram);
|
||||
}
|
||||
|
||||
private void UpdateHologramShader(EntityUid uid, SpriteComponent sprite, HolopadHologramComponent holopadHologram)
|
||||
{
|
||||
// Find the texture height of the largest layer
|
||||
float texHeight = sprite.AllLayers.Max(x => x.PixelSize.Y);
|
||||
|
||||
var instance = _prototypeManager.Index<ShaderPrototype>(holopadHologram.ShaderName).InstanceUnique();
|
||||
instance.SetParameter("color1", new Vector3(holopadHologram.Color1.R, holopadHologram.Color1.G, holopadHologram.Color1.B));
|
||||
instance.SetParameter("color2", new Vector3(holopadHologram.Color2.R, holopadHologram.Color2.G, holopadHologram.Color2.B));
|
||||
instance.SetParameter("alpha", holopadHologram.Alpha);
|
||||
instance.SetParameter("intensity", holopadHologram.Intensity);
|
||||
instance.SetParameter("texHeight", texHeight);
|
||||
instance.SetParameter("t", (float)_timing.CurTime.TotalSeconds * holopadHologram.ScrollRate);
|
||||
|
||||
sprite.PostShader = instance;
|
||||
sprite.RaiseShaderEvent = true;
|
||||
}
|
||||
}
|
||||
107
Content.Client/Holopad/HolopadWindow.xaml
Normal file
107
Content.Client/Holopad/HolopadWindow.xaml
Normal file
@@ -0,0 +1,107 @@
|
||||
<controls:FancyWindow xmlns="https://spacestation14.io"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
|
||||
Resizable="False"
|
||||
MaxSize="400 800"
|
||||
MinSize="400 150">
|
||||
<BoxContainer Orientation="Vertical" VerticalExpand="True" HorizontalExpand="True">
|
||||
|
||||
<BoxContainer Name="ControlsLockOutContainer" Orientation="Vertical" VerticalExpand="True" HorizontalExpand="True" ReservesSpace="False" Visible="False">
|
||||
<!-- Header text -->
|
||||
<controls:StripeBack>
|
||||
<PanelContainer>
|
||||
<RichTextLabel Name="EmergencyBroadcastText" VerticalAlignment="Center" HorizontalAlignment="Center" Margin="10 10 10 10" ReservesSpace="False"/>
|
||||
</PanelContainer>
|
||||
</controls:StripeBack>
|
||||
|
||||
<Label Text="{Loc 'holopad-window-controls-locked-out'}" HorizontalAlignment="Center" Margin="10 5 10 0" ReservesSpace="False"/>
|
||||
<RichTextLabel Name="LockOutIdText" HorizontalAlignment="Center" Margin="10 5 10 0" ReservesSpace="False"/>
|
||||
<Label Name="LockOutCountDownText" Text="{Loc 'holopad-window-controls-unlock-countdown'}" HorizontalAlignment="Center" Margin="10 15 10 10" ReservesSpace="False"/>
|
||||
</BoxContainer>
|
||||
|
||||
<BoxContainer Name="ControlsContainer" Orientation="Vertical" VerticalExpand="True" HorizontalExpand="True" ReservesSpace="False">
|
||||
|
||||
<!-- Active call controls (either this or the call placement controls will be active) -->
|
||||
<BoxContainer Name="ActiveCallControlsContainer" Orientation="Vertical" VerticalExpand="True" HorizontalExpand="True" ReservesSpace="False">
|
||||
|
||||
<!-- Header text -->
|
||||
<BoxContainer MinHeight="60" Orientation="Vertical" VerticalAlignment="Center">
|
||||
<Label Name="CallStatusText" Margin="10 5 10 0" ReservesSpace="False"/>
|
||||
<RichTextLabel Name="CallerIdText" HorizontalAlignment="Center" Margin="0 0 0 0" ReservesSpace="False"/>
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Controls (the answer call button is absent when the phone is not ringing) -->
|
||||
<GridContainer Columns="2" ReservesSpace="False">
|
||||
<Control HorizontalExpand="True" Margin="10 0 2 5">
|
||||
<Button Name="AnswerCallButton" Text="{Loc 'holopad-window-answer-call'}" StyleClasses="OpenRight" Margin="0 0 0 5" Disabled="True"/>
|
||||
</Control>
|
||||
<Control HorizontalExpand="True" Margin="2 0 10 5">
|
||||
<Button Name="EndCallButton" Text="{Loc 'holopad-window-end-call'}" StyleClasses="OpenLeft" Margin="0 0 0 5" Disabled="True"/>
|
||||
</Control>
|
||||
</GridContainer>
|
||||
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Call placement controls (either this or the active call controls will be active) -->
|
||||
<BoxContainer Name="CallPlacementControlsContainer" Orientation="Vertical" VerticalExpand="True" HorizontalExpand="True" ReservesSpace="False">
|
||||
|
||||
<controls:StripeBack>
|
||||
<PanelContainer>
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<RichTextLabel Name="SubtitleText" HorizontalAlignment="Center" Margin="0 5 0 0"/>
|
||||
<RichTextLabel Name="OptionsText" HorizontalAlignment="Center" Margin="0 0 0 5"/>
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
</controls:StripeBack>
|
||||
|
||||
<!-- Request the station AI or activate the holopad projector (only one of these should be active at a time) -->
|
||||
<BoxContainer Name="RequestStationAiContainer" Orientation="Vertical" ReservesSpace="False" Visible="False">
|
||||
<Button Name="RequestStationAiButton" Text="{Loc 'holopad-window-request-station-ai'}" Margin="10 5 10 5" Disabled="False"/>
|
||||
</BoxContainer>
|
||||
|
||||
<BoxContainer Name="ActivateProjectorContainer" Orientation="Vertical" ReservesSpace="False" Visible="False">
|
||||
<Button Name="ActivateProjectorButton" Text="{Loc 'holopad-window-activate-projector'}" Margin="10 5 10 5" Disabled="False"/>
|
||||
</BoxContainer>
|
||||
|
||||
<!-- List of contactable holopads (the list is created in C#) -->
|
||||
<BoxContainer Name="HolopadContactListContainer" Orientation="Vertical" Margin="10 0 10 5" ReservesSpace="False" Visible="False">
|
||||
<PanelContainer Name="HolopadContactListHeaderPanel">
|
||||
<Label Text="{Loc 'holopad-window-select-contact-from-list'}" HorizontalAlignment="Center" Margin="0 3 0 3"/>
|
||||
</PanelContainer>
|
||||
|
||||
<PanelContainer Name="HolopadContactListPanel">
|
||||
<ScrollContainer HorizontalExpand="True" VerticalExpand="True" Margin="8, 8, 8, 8" MinHeight="256">
|
||||
|
||||
<!-- If there is no data yet, this will be displayed -->
|
||||
<BoxContainer Name="FetchingAvailableHolopadsContainer" HorizontalAlignment="Center" HorizontalExpand="True" VerticalExpand="True" ReservesSpace="False">
|
||||
<Label Text="{Loc 'holopad-window-fetching-contacts-list'}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Container for the contacts -->
|
||||
<BoxContainer Name="ContactsList" Orientation="Vertical" HorizontalExpand="True" VerticalExpand="True" Margin="10 0 10 0"/>
|
||||
</ScrollContainer>
|
||||
</PanelContainer>
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Button to start an emergency broadcast (the user requires a certain level of access to interact with it) -->
|
||||
<BoxContainer Name="StartBroadcastContainer" Orientation="Vertical" ReservesSpace="False" Visible="False">
|
||||
<Button Name="StartBroadcastButton" Text="{Loc 'holopad-window-emergency-broadcast'}" Margin="10 0 10 5" Disabled="False" ReservesSpace="False"/>
|
||||
</BoxContainer>
|
||||
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Footer -->
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<PanelContainer StyleClasses="LowDivider" />
|
||||
<BoxContainer Orientation="Horizontal" Margin="10 2 5 0" VerticalAlignment="Bottom">
|
||||
<Label Text="{Loc 'holopad-window-flavor-left'}" StyleClasses="WindowFooterText" />
|
||||
<Label Text="{Loc 'holopad-window-flavor-right'}" StyleClasses="WindowFooterText"
|
||||
HorizontalAlignment="Right" HorizontalExpand="True" Margin="0 0 5 0" />
|
||||
<TextureRect StyleClasses="NTLogoDark" Stretch="KeepAspectCentered"
|
||||
VerticalAlignment="Center" HorizontalAlignment="Right" SetSize="19 19"/>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
|
||||
</controls:FancyWindow>
|
||||
338
Content.Client/Holopad/HolopadWindow.xaml.cs
Normal file
338
Content.Client/Holopad/HolopadWindow.xaml.cs
Normal file
@@ -0,0 +1,338 @@
|
||||
using Content.Client.Popups;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Shared.Access.Systems;
|
||||
using Content.Shared.Holopad;
|
||||
using Content.Shared.Telephone;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
using System.Linq;
|
||||
|
||||
namespace Content.Client.Holopad;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class HolopadWindow : FancyWindow
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
|
||||
private readonly SharedHolopadSystem _holopadSystem = default!;
|
||||
private readonly SharedTelephoneSystem _telephoneSystem = default!;
|
||||
private readonly AccessReaderSystem _accessReaderSystem = default!;
|
||||
private readonly PopupSystem _popupSystem = default!;
|
||||
|
||||
private EntityUid? _owner = null;
|
||||
private HolopadUiKey _currentUiKey;
|
||||
private TelephoneState _currentState;
|
||||
private TelephoneState _previousState;
|
||||
private TimeSpan _buttonUnlockTime;
|
||||
private float _updateTimer = 0.25f;
|
||||
|
||||
private const float UpdateTime = 0.25f;
|
||||
private TimeSpan _buttonUnlockDelay = TimeSpan.FromSeconds(0.5f);
|
||||
|
||||
public event Action<NetEntity>? SendHolopadStartNewCallMessageAction;
|
||||
public event Action? SendHolopadAnswerCallMessageAction;
|
||||
public event Action? SendHolopadEndCallMessageAction;
|
||||
public event Action? SendHolopadStartBroadcastMessageAction;
|
||||
public event Action? SendHolopadActivateProjectorMessageAction;
|
||||
public event Action? SendHolopadRequestStationAiMessageAction;
|
||||
|
||||
public HolopadWindow()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
_holopadSystem = _entManager.System<SharedHolopadSystem>();
|
||||
_telephoneSystem = _entManager.System<SharedTelephoneSystem>();
|
||||
_accessReaderSystem = _entManager.System<AccessReaderSystem>();
|
||||
_popupSystem = _entManager.System<PopupSystem>();
|
||||
|
||||
_buttonUnlockTime = _timing.CurTime + _buttonUnlockDelay;
|
||||
|
||||
// Assign button actions
|
||||
AnswerCallButton.OnPressed += args => { OnHolopadAnswerCallMessage(); };
|
||||
EndCallButton.OnPressed += args => { OnHolopadEndCallMessage(); };
|
||||
StartBroadcastButton.OnPressed += args => { OnHolopadStartBroadcastMessage(); };
|
||||
ActivateProjectorButton.OnPressed += args => { OnHolopadActivateProjectorMessage(); };
|
||||
RequestStationAiButton.OnPressed += args => { OnHolopadRequestStationAiMessage(); };
|
||||
|
||||
// XML formatting
|
||||
AnswerCallButton.AddStyleClass("ButtonAccept");
|
||||
EndCallButton.AddStyleClass("Caution");
|
||||
StartBroadcastButton.AddStyleClass("Caution");
|
||||
|
||||
HolopadContactListPanel.PanelOverride = new StyleBoxFlat
|
||||
{
|
||||
BackgroundColor = new Color(47, 47, 59) * Color.DarkGray,
|
||||
BorderColor = new Color(82, 82, 82), //new Color(70, 73, 102),
|
||||
BorderThickness = new Thickness(2),
|
||||
};
|
||||
|
||||
HolopadContactListHeaderPanel.PanelOverride = new StyleBoxFlat
|
||||
{
|
||||
BackgroundColor = new Color(82, 82, 82),
|
||||
};
|
||||
|
||||
EmergencyBroadcastText.SetMessage(FormattedMessage.FromMarkupOrThrow(Loc.GetString("holopad-window-emergency-broadcast-in-progress")));
|
||||
SubtitleText.SetMessage(FormattedMessage.FromMarkupOrThrow(Loc.GetString("holopad-window-subtitle")));
|
||||
OptionsText.SetMessage(FormattedMessage.FromMarkupOrThrow(Loc.GetString("holopad-window-options")));
|
||||
}
|
||||
|
||||
#region: Button actions
|
||||
|
||||
private void OnSendHolopadStartNewCallMessage(NetEntity receiver)
|
||||
{
|
||||
SendHolopadStartNewCallMessageAction?.Invoke(receiver);
|
||||
}
|
||||
|
||||
private void OnHolopadAnswerCallMessage()
|
||||
{
|
||||
SendHolopadAnswerCallMessageAction?.Invoke();
|
||||
}
|
||||
|
||||
private void OnHolopadEndCallMessage()
|
||||
{
|
||||
SendHolopadEndCallMessageAction?.Invoke();
|
||||
|
||||
if (_currentUiKey == HolopadUiKey.AiRequestWindow)
|
||||
Close();
|
||||
}
|
||||
|
||||
private void OnHolopadStartBroadcastMessage()
|
||||
{
|
||||
if (_playerManager.LocalSession?.AttachedEntity == null || _owner == null)
|
||||
return;
|
||||
|
||||
var player = _playerManager.LocalSession.AttachedEntity;
|
||||
|
||||
if (!_accessReaderSystem.IsAllowed(player.Value, _owner.Value))
|
||||
{
|
||||
_popupSystem.PopupClient(Loc.GetString("holopad-window-access-denied"), _owner.Value, player.Value);
|
||||
return;
|
||||
}
|
||||
|
||||
SendHolopadStartBroadcastMessageAction?.Invoke();
|
||||
}
|
||||
|
||||
private void OnHolopadActivateProjectorMessage()
|
||||
{
|
||||
SendHolopadActivateProjectorMessageAction?.Invoke();
|
||||
}
|
||||
|
||||
private void OnHolopadRequestStationAiMessage()
|
||||
{
|
||||
SendHolopadRequestStationAiMessageAction?.Invoke();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public void SetState(EntityUid owner, HolopadUiKey uiKey)
|
||||
{
|
||||
_owner = owner;
|
||||
_currentUiKey = uiKey;
|
||||
|
||||
// Determines what UI containers are available to the user.
|
||||
// Components of these will be toggled on and off when
|
||||
// UpdateAppearance() is called
|
||||
|
||||
switch (uiKey)
|
||||
{
|
||||
case HolopadUiKey.InteractionWindow:
|
||||
RequestStationAiContainer.Visible = true;
|
||||
HolopadContactListContainer.Visible = true;
|
||||
StartBroadcastContainer.Visible = true;
|
||||
break;
|
||||
|
||||
case HolopadUiKey.InteractionWindowForAi:
|
||||
ActivateProjectorContainer.Visible = true;
|
||||
StartBroadcastContainer.Visible = true;
|
||||
break;
|
||||
|
||||
case HolopadUiKey.AiActionWindow:
|
||||
HolopadContactListContainer.Visible = true;
|
||||
StartBroadcastContainer.Visible = true;
|
||||
break;
|
||||
|
||||
case HolopadUiKey.AiRequestWindow:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateState(Dictionary<NetEntity, string> holopads)
|
||||
{
|
||||
if (_owner == null || !_entManager.TryGetComponent<TelephoneComponent>(_owner.Value, out var telephone))
|
||||
return;
|
||||
|
||||
// Caller ID text
|
||||
var callerId = _telephoneSystem.GetFormattedCallerIdForEntity(telephone.LastCallerId.Item1, telephone.LastCallerId.Item2, Color.LightGray, "Default", 11);
|
||||
|
||||
CallerIdText.SetMessage(FormattedMessage.FromMarkupOrThrow(callerId));
|
||||
LockOutIdText.SetMessage(FormattedMessage.FromMarkupOrThrow(callerId));
|
||||
|
||||
// Sort holopads alphabetically
|
||||
var holopadArray = holopads.ToArray();
|
||||
Array.Sort(holopadArray, AlphabeticalSort);
|
||||
|
||||
// Clear excess children from the contact list
|
||||
while (ContactsList.ChildCount > holopadArray.Length)
|
||||
ContactsList.RemoveChild(ContactsList.GetChild(ContactsList.ChildCount - 1));
|
||||
|
||||
// Make / update required children
|
||||
for (int i = 0; i < holopadArray.Length; i++)
|
||||
{
|
||||
var (netEntity, label) = holopadArray[i];
|
||||
|
||||
if (i >= ContactsList.ChildCount)
|
||||
{
|
||||
var newContactButton = new HolopadContactButton();
|
||||
newContactButton.OnPressed += args => { OnSendHolopadStartNewCallMessage(newContactButton.NetEntity); };
|
||||
|
||||
ContactsList.AddChild(newContactButton);
|
||||
}
|
||||
|
||||
var child = ContactsList.GetChild(i);
|
||||
|
||||
if (child is not HolopadContactButton)
|
||||
continue;
|
||||
|
||||
var contactButton = (HolopadContactButton)child;
|
||||
contactButton.UpdateValues(netEntity, label);
|
||||
}
|
||||
|
||||
// Update buttons
|
||||
UpdateAppearance();
|
||||
}
|
||||
|
||||
private void UpdateAppearance()
|
||||
{
|
||||
if (_owner == null || !_entManager.TryGetComponent<TelephoneComponent>(_owner.Value, out var telephone))
|
||||
return;
|
||||
|
||||
if (_owner == null || !_entManager.TryGetComponent<HolopadComponent>(_owner.Value, out var holopad))
|
||||
return;
|
||||
|
||||
var hasBroadcastAccess = !_holopadSystem.IsHolopadBroadcastOnCoolDown((_owner.Value, holopad));
|
||||
var localPlayer = _playerManager.LocalSession?.AttachedEntity;
|
||||
|
||||
ControlsLockOutContainer.Visible = _holopadSystem.IsHolopadControlLocked((_owner.Value, holopad), localPlayer);
|
||||
ControlsContainer.Visible = !ControlsLockOutContainer.Visible;
|
||||
|
||||
// Temporarily disable the interface buttons when the call state changes to prevent any misclicks
|
||||
if (_currentState != telephone.CurrentState)
|
||||
{
|
||||
_previousState = _currentState;
|
||||
_currentState = telephone.CurrentState;
|
||||
_buttonUnlockTime = _timing.CurTime + _buttonUnlockDelay;
|
||||
}
|
||||
|
||||
var lockButtons = _timing.CurTime < _buttonUnlockTime;
|
||||
|
||||
// Make / update required children
|
||||
foreach (var child in ContactsList.Children)
|
||||
{
|
||||
if (child is not HolopadContactButton)
|
||||
continue;
|
||||
|
||||
var contactButton = (HolopadContactButton)child;
|
||||
contactButton.Disabled = (_currentState != TelephoneState.Idle || lockButtons);
|
||||
}
|
||||
|
||||
// Update control text
|
||||
var cooldown = _holopadSystem.GetHolopadBroadcastCoolDown((_owner.Value, holopad));
|
||||
var cooldownString = $"{cooldown.Minutes:00}:{cooldown.Seconds:00}";
|
||||
|
||||
StartBroadcastButton.Text = _holopadSystem.IsHolopadBroadcastOnCoolDown((_owner.Value, holopad)) ?
|
||||
Loc.GetString("holopad-window-emergency-broadcast-with-countdown", ("countdown", cooldownString)) :
|
||||
Loc.GetString("holopad-window-emergency-broadcast");
|
||||
|
||||
var lockout = _holopadSystem.GetHolopadControlLockedPeriod((_owner.Value, holopad));
|
||||
var lockoutString = $"{lockout.Minutes:00}:{lockout.Seconds:00}";
|
||||
|
||||
LockOutCountDownText.Text = Loc.GetString("holopad-window-controls-unlock-countdown", ("countdown", lockoutString));
|
||||
|
||||
switch (_currentState)
|
||||
{
|
||||
case TelephoneState.Idle:
|
||||
CallStatusText.Text = Loc.GetString("holopad-window-no-calls-in-progress"); break;
|
||||
|
||||
case TelephoneState.Calling:
|
||||
CallStatusText.Text = Loc.GetString("holopad-window-outgoing-call"); break;
|
||||
|
||||
case TelephoneState.Ringing:
|
||||
CallStatusText.Text = (_currentUiKey == HolopadUiKey.AiRequestWindow) ?
|
||||
Loc.GetString("holopad-window-ai-request") : Loc.GetString("holopad-window-incoming-call"); break;
|
||||
|
||||
case TelephoneState.InCall:
|
||||
CallStatusText.Text = Loc.GetString("holopad-window-call-in-progress"); break;
|
||||
|
||||
case TelephoneState.EndingCall:
|
||||
if (_previousState == TelephoneState.Calling || _previousState == TelephoneState.Idle)
|
||||
CallStatusText.Text = Loc.GetString("holopad-window-call-rejected");
|
||||
else
|
||||
CallStatusText.Text = Loc.GetString("holopad-window-call-ending");
|
||||
break;
|
||||
}
|
||||
|
||||
// Update control disability
|
||||
AnswerCallButton.Disabled = (_currentState != TelephoneState.Ringing || lockButtons);
|
||||
EndCallButton.Disabled = (_currentState == TelephoneState.Idle || _currentState == TelephoneState.EndingCall || lockButtons);
|
||||
StartBroadcastButton.Disabled = (_currentState != TelephoneState.Idle || !hasBroadcastAccess || lockButtons);
|
||||
RequestStationAiButton.Disabled = (_currentState != TelephoneState.Idle || lockButtons);
|
||||
ActivateProjectorButton.Disabled = (_currentState != TelephoneState.Idle || lockButtons);
|
||||
|
||||
// Update control visibility
|
||||
FetchingAvailableHolopadsContainer.Visible = (ContactsList.ChildCount == 0);
|
||||
ActiveCallControlsContainer.Visible = (_currentState != TelephoneState.Idle || _currentUiKey == HolopadUiKey.AiRequestWindow);
|
||||
CallPlacementControlsContainer.Visible = !ActiveCallControlsContainer.Visible;
|
||||
CallerIdText.Visible = (_currentState == TelephoneState.Ringing);
|
||||
AnswerCallButton.Visible = (_currentState == TelephoneState.Ringing);
|
||||
}
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
base.FrameUpdate(args);
|
||||
|
||||
_updateTimer += args.DeltaSeconds;
|
||||
|
||||
if (_updateTimer >= UpdateTime)
|
||||
{
|
||||
_updateTimer -= UpdateTime;
|
||||
UpdateAppearance();
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class HolopadContactButton : Button
|
||||
{
|
||||
public NetEntity NetEntity;
|
||||
|
||||
public HolopadContactButton()
|
||||
{
|
||||
HorizontalExpand = true;
|
||||
SetHeight = 32;
|
||||
Margin = new Thickness(0f, 1f, 0f, 1f);
|
||||
}
|
||||
|
||||
public void UpdateValues(NetEntity netEntity, string label)
|
||||
{
|
||||
NetEntity = netEntity;
|
||||
Text = Loc.GetString("holopad-window-contact-label", ("label", label));
|
||||
}
|
||||
}
|
||||
|
||||
private int AlphabeticalSort(KeyValuePair<NetEntity, string> x, KeyValuePair<NetEntity, string> y)
|
||||
{
|
||||
if (string.IsNullOrEmpty(x.Value))
|
||||
return -1;
|
||||
|
||||
if (string.IsNullOrEmpty(y.Value))
|
||||
return 1;
|
||||
|
||||
return x.Value.CompareTo(y.Value);
|
||||
}
|
||||
}
|
||||
@@ -455,7 +455,21 @@ public sealed class LobbyUIController : UIController, IOnStateEntered<LobbyState
|
||||
{
|
||||
EntityUid dummyEnt;
|
||||
|
||||
if (humanoid is not null)
|
||||
EntProtoId? previewEntity = null;
|
||||
if (humanoid != null && jobClothes)
|
||||
{
|
||||
job ??= GetPreferredJob(humanoid);
|
||||
|
||||
previewEntity = job.JobPreviewEntity ?? (EntProtoId?)job?.JobEntity;
|
||||
}
|
||||
|
||||
if (previewEntity != null)
|
||||
{
|
||||
// Special type like borg or AI, do not spawn a human just spawn the entity.
|
||||
dummyEnt = EntityManager.SpawnEntity(previewEntity, MapCoordinates.Nullspace);
|
||||
return dummyEnt;
|
||||
}
|
||||
else if (humanoid is not null)
|
||||
{
|
||||
var dummy = _prototypeManager.Index<SpeciesPrototype>(humanoid.Species).DollPrototype;
|
||||
dummyEnt = EntityManager.SpawnEntity(dummy, MapCoordinates.Nullspace);
|
||||
@@ -469,7 +483,8 @@ public sealed class LobbyUIController : UIController, IOnStateEntered<LobbyState
|
||||
|
||||
if (humanoid != null && jobClothes)
|
||||
{
|
||||
job ??= GetPreferredJob(humanoid);
|
||||
DebugTools.Assert(job != null);
|
||||
|
||||
GiveDummyJobClothes(dummyEnt, humanoid, job);
|
||||
|
||||
if (_prototypeManager.HasIndex<RoleLoadoutPrototype>(LoadoutSystem.GetJobPrototype(job.ID)))
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
SeparationOverride="0"
|
||||
Name="InternalHBox">
|
||||
<SpriteView Scale="2 2"
|
||||
Margin="0 4 4 4"
|
||||
OverrideDirection="South"
|
||||
Name="View"/>
|
||||
<Label Name="DescriptionLabel"
|
||||
|
||||
@@ -140,7 +140,7 @@
|
||||
</BoxContainer>
|
||||
<!-- Right side -->
|
||||
<BoxContainer Orientation="Vertical" VerticalExpand="True" VerticalAlignment="Center">
|
||||
<SpriteView Name="SpriteView" Scale="8 8" SizeFlagsStretchRatio="1" />
|
||||
<SpriteView Name="SpriteView" Scale="8 8" Margin="4" SizeFlagsStretchRatio="1" />
|
||||
<BoxContainer Orientation="Horizontal" HorizontalAlignment="Center" Margin="0 5">
|
||||
<Button Name="SpriteRotateLeft" Text="◀" StyleClasses="OpenRight" />
|
||||
<cc:VSeparator Margin="2 0 3 0" />
|
||||
|
||||
@@ -9,6 +9,7 @@ using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Client.UserInterface.RichText;
|
||||
using Content.Client.UserInterface.RichText;
|
||||
using Robust.Shared.Input;
|
||||
|
||||
namespace Content.Client.Paper.UI
|
||||
@@ -43,7 +44,8 @@ namespace Content.Client.Paper.UI
|
||||
typeof(BulletTag),
|
||||
typeof(ColorTag),
|
||||
typeof(HeadingTag),
|
||||
typeof(ItalicTag)
|
||||
typeof(ItalicTag),
|
||||
typeof(MonoTag)
|
||||
};
|
||||
|
||||
public event Action<string>? OnSaved;
|
||||
|
||||
@@ -385,26 +385,6 @@ public partial class NavMapControl : MapGridControl
|
||||
if (PostWallDrawingAction != null)
|
||||
PostWallDrawingAction.Invoke(handle);
|
||||
|
||||
// Beacons
|
||||
if (_beacons.Pressed)
|
||||
{
|
||||
var rectBuffer = new Vector2(5f, 3f);
|
||||
|
||||
// Calculate font size for current zoom level
|
||||
var fontSize = (int) Math.Round(1 / WorldRange * DefaultDisplayedRange * UIScale * _targetFontsize, 0);
|
||||
var font = new VectorFont(_cache.GetResource<FontResource>("/Fonts/NotoSans/NotoSans-Bold.ttf"), fontSize);
|
||||
|
||||
foreach (var beacon in _navMap.Beacons.Values)
|
||||
{
|
||||
var position = beacon.Position - offset;
|
||||
position = ScalePosition(position with { Y = -position.Y });
|
||||
|
||||
var textDimensions = handle.GetDimensions(font, beacon.Text, 1f);
|
||||
handle.DrawRect(new UIBox2(position - textDimensions / 2 - rectBuffer, position + textDimensions / 2 + rectBuffer), BackgroundColor);
|
||||
handle.DrawString(font, position - textDimensions / 2, beacon.Text, beacon.Color);
|
||||
}
|
||||
}
|
||||
|
||||
var curTime = Timing.RealTime;
|
||||
var blinkFrequency = 1f / 1f;
|
||||
var lit = curTime.TotalSeconds % blinkFrequency > blinkFrequency / 2f;
|
||||
@@ -443,11 +423,31 @@ public partial class NavMapControl : MapGridControl
|
||||
position = ScalePosition(new Vector2(position.X, -position.Y));
|
||||
|
||||
var scalingCoefficient = MinmapScaleModifier * float.Sqrt(MinimapScale);
|
||||
var positionOffset = new Vector2(scalingCoefficient * blip.Texture.Width, scalingCoefficient * blip.Texture.Height);
|
||||
var positionOffset = new Vector2(scalingCoefficient * blip.Scale * blip.Texture.Width, scalingCoefficient * blip.Scale * blip.Texture.Height);
|
||||
|
||||
handle.DrawTextureRect(blip.Texture, new UIBox2(position - positionOffset, position + positionOffset), blip.Color);
|
||||
}
|
||||
}
|
||||
|
||||
// Beacons
|
||||
if (_beacons.Pressed)
|
||||
{
|
||||
var rectBuffer = new Vector2(5f, 3f);
|
||||
|
||||
// Calculate font size for current zoom level
|
||||
var fontSize = (int)Math.Round(1 / WorldRange * DefaultDisplayedRange * UIScale * _targetFontsize, 0);
|
||||
var font = new VectorFont(_cache.GetResource<FontResource>("/Fonts/NotoSans/NotoSans-Bold.ttf"), fontSize);
|
||||
|
||||
foreach (var beacon in _navMap.Beacons.Values)
|
||||
{
|
||||
var position = beacon.Position - offset;
|
||||
position = ScalePosition(position with { Y = -position.Y });
|
||||
|
||||
var textDimensions = handle.GetDimensions(font, beacon.Text, 1f);
|
||||
handle.DrawRect(new UIBox2(position - textDimensions / 2 - rectBuffer, position + textDimensions / 2 + rectBuffer), BackgroundColor);
|
||||
handle.DrawString(font, position - textDimensions / 2, beacon.Text, beacon.Color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
@@ -689,6 +689,9 @@ public partial class NavMapControl : MapGridControl
|
||||
Vector2i foundTermius;
|
||||
Vector2i foundOrigin;
|
||||
|
||||
if (origin == terminus)
|
||||
return;
|
||||
|
||||
// Does our new line end at the beginning of an existing line?
|
||||
if (lookup.Remove(terminus, out foundTermius))
|
||||
{
|
||||
@@ -739,13 +742,15 @@ public struct NavMapBlip
|
||||
public Color Color;
|
||||
public bool Blinks;
|
||||
public bool Selectable;
|
||||
public float Scale;
|
||||
|
||||
public NavMapBlip(EntityCoordinates coordinates, Texture texture, Color color, bool blinks, bool selectable = true)
|
||||
public NavMapBlip(EntityCoordinates coordinates, Texture texture, Color color, bool blinks, bool selectable = true, float scale = 1f)
|
||||
{
|
||||
Coordinates = coordinates;
|
||||
Texture = texture;
|
||||
Color = color;
|
||||
Blinks = blinks;
|
||||
Selectable = selectable;
|
||||
Scale = scale;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,10 +157,5 @@ namespace Content.Client.Sandbox
|
||||
{
|
||||
_consoleHost.ExecuteCommand("physics shapes");
|
||||
}
|
||||
|
||||
public void MachineLinking()
|
||||
{
|
||||
_consoleHost.ExecuteCommand("signallink");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ public sealed partial class StoreMenu : DefaultWindow
|
||||
foreach (var ((_, amount), proto) in currency)
|
||||
{
|
||||
balanceStr += Loc.GetString("store-ui-balance-display", ("amount", amount),
|
||||
("currency", Loc.GetString(proto.DisplayName, ("amount", 1))));
|
||||
("currency", Loc.GetString(proto.DisplayName, ("amount", 1)))) + "\n";
|
||||
}
|
||||
|
||||
BalanceInfo.SetMarkup(balanceStr.TrimEnd());
|
||||
@@ -63,7 +63,10 @@ public sealed partial class StoreMenu : DefaultWindow
|
||||
foreach (var type in currency)
|
||||
{
|
||||
if (type.Value.CanWithdraw && type.Value.Cash != null && type.Key.Item2 > 0)
|
||||
{
|
||||
disabled = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
WithdrawButton.Disabled = disabled;
|
||||
|
||||
@@ -18,7 +18,7 @@ public sealed partial class StoreWithdrawWindow : DefaultWindow
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
|
||||
private Dictionary<FixedPoint2, CurrencyPrototype> _validCurrencies = new();
|
||||
private Dictionary<CurrencyPrototype, FixedPoint2> _validCurrencies = new();
|
||||
private HashSet<CurrencyWithdrawButton> _buttons = new();
|
||||
public event Action<BaseButton.ButtonEventArgs, string, int>? OnWithdrawAttempt;
|
||||
|
||||
@@ -36,7 +36,7 @@ public sealed partial class StoreWithdrawWindow : DefaultWindow
|
||||
if (!_prototypeManager.TryIndex(currency.Key, out var proto))
|
||||
continue;
|
||||
|
||||
_validCurrencies.Add(currency.Value, proto);
|
||||
_validCurrencies.Add(proto, currency.Value);
|
||||
}
|
||||
|
||||
//this shouldn't ever happen but w/e
|
||||
@@ -47,14 +47,17 @@ public sealed partial class StoreWithdrawWindow : DefaultWindow
|
||||
_buttons.Clear();
|
||||
foreach (var currency in _validCurrencies)
|
||||
{
|
||||
if (!currency.Key.CanWithdraw)
|
||||
continue;
|
||||
|
||||
var button = new CurrencyWithdrawButton()
|
||||
{
|
||||
Id = currency.Value.ID,
|
||||
Amount = currency.Key,
|
||||
Id = currency.Key.ID,
|
||||
Amount = currency.Value,
|
||||
MinHeight = 20,
|
||||
Text = Loc.GetString("store-withdraw-button-ui", ("currency",Loc.GetString(currency.Value.DisplayName, ("amount", currency.Key)))),
|
||||
Text = Loc.GetString("store-withdraw-button-ui", ("currency",Loc.GetString(currency.Key.DisplayName, ("amount", currency.Value)))),
|
||||
Disabled = false,
|
||||
};
|
||||
button.Disabled = false;
|
||||
button.OnPressed += args =>
|
||||
{
|
||||
OnWithdrawAttempt?.Invoke(args, button.Id, WithdrawSlider.Value);
|
||||
@@ -65,7 +68,7 @@ public sealed partial class StoreWithdrawWindow : DefaultWindow
|
||||
ButtonContainer.AddChild(button);
|
||||
}
|
||||
|
||||
var maxWithdrawAmount = _validCurrencies.Keys.Max().Int();
|
||||
var maxWithdrawAmount = _validCurrencies.Values.Max().Int();
|
||||
|
||||
// setup withdraw slider
|
||||
WithdrawSlider.MinValue = 1;
|
||||
|
||||
@@ -110,6 +110,7 @@ namespace Content.Client.Stylesheets
|
||||
|
||||
public static readonly Color ButtonColorGoodDefault = Color.FromHex("#3E6C45");
|
||||
public static readonly Color ButtonColorGoodHovered = Color.FromHex("#31843E");
|
||||
public static readonly Color ButtonColorGoodDisabled = Color.FromHex("#164420");
|
||||
|
||||
//NavMap
|
||||
public static readonly Color PointRed = Color.FromHex("#B02E26");
|
||||
@@ -1499,6 +1500,20 @@ namespace Content.Client.Stylesheets
|
||||
|
||||
Element<Button>().Class("ButtonColorGreen").Pseudo(ContainerButton.StylePseudoClassHover)
|
||||
.Prop(Control.StylePropertyModulateSelf, ButtonColorGoodHovered),
|
||||
|
||||
// Accept button (merge with green button?) ---
|
||||
Element<Button>().Class("ButtonAccept")
|
||||
.Prop(Control.StylePropertyModulateSelf, ButtonColorGoodDefault),
|
||||
|
||||
Element<Button>().Class("ButtonAccept").Pseudo(ContainerButton.StylePseudoClassNormal)
|
||||
.Prop(Control.StylePropertyModulateSelf, ButtonColorGoodDefault),
|
||||
|
||||
Element<Button>().Class("ButtonAccept").Pseudo(ContainerButton.StylePseudoClassHover)
|
||||
.Prop(Control.StylePropertyModulateSelf, ButtonColorGoodHovered),
|
||||
|
||||
Element<Button>().Class("ButtonAccept").Pseudo(ContainerButton.StylePseudoClassDisabled)
|
||||
.Prop(Control.StylePropertyModulateSelf, ButtonColorGoodDisabled),
|
||||
|
||||
// ---
|
||||
|
||||
// Small Button ---
|
||||
|
||||
8
Content.Client/Telephone/TelephoneSystem.cs
Normal file
8
Content.Client/Telephone/TelephoneSystem.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using Content.Shared.Telephone;
|
||||
|
||||
namespace Content.Client.Telephone;
|
||||
|
||||
public sealed class TelephoneSystem : SharedTelephoneSystem
|
||||
{
|
||||
|
||||
}
|
||||
34
Content.Client/UserInterface/RichText/MonoTag.cs
Normal file
34
Content.Client/UserInterface/RichText/MonoTag.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using System.Linq;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.UserInterface.RichText;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.UserInterface.RichText;
|
||||
|
||||
/// <summary>
|
||||
/// Sets the font to a monospaced variant
|
||||
/// </summary>
|
||||
public sealed class MonoTag : IMarkupTag
|
||||
{
|
||||
[ValidatePrototypeId<FontPrototype>] public const string MonoFont = "Monospace";
|
||||
|
||||
[Dependency] private readonly IResourceCache _resourceCache = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
|
||||
public string Name => "mono";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void PushDrawContext(MarkupNode node, MarkupDrawingContext context)
|
||||
{
|
||||
var font = FontTag.CreateFont(context.Font, node, _resourceCache, _prototypeManager, MonoFont);
|
||||
context.Font.Push(font);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void PopDrawContext(MarkupNode node, MarkupDrawingContext context)
|
||||
{
|
||||
context.Font.Pop();
|
||||
}
|
||||
}
|
||||
@@ -572,6 +572,10 @@ public sealed class UserAHelpUIHandler : IAHelpUIHandler
|
||||
_window.OnClose += () => { OnClose?.Invoke(); };
|
||||
_window.OnOpen += () => { OnOpen?.Invoke(); };
|
||||
_window.Contents.AddChild(_chatPanel);
|
||||
|
||||
var introText = Loc.GetString("bwoink-system-introductory-message");
|
||||
var introMessage = new SharedBwoinkSystem.BwoinkTextMessage( _ownerId, SharedBwoinkSystem.SystemUserId, introText);
|
||||
Receive(introMessage);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Content.Client.Administration.Managers;
|
||||
using System.Numerics;
|
||||
using Content.Client.Administration.Managers;
|
||||
using Content.Client.Gameplay;
|
||||
using Content.Client.Markers;
|
||||
using Content.Client.Sandbox;
|
||||
@@ -7,9 +8,7 @@ using Content.Client.UserInterface.Controls;
|
||||
using Content.Client.UserInterface.Systems.DecalPlacer;
|
||||
using Content.Client.UserInterface.Systems.Sandbox.Windows;
|
||||
using Content.Shared.Input;
|
||||
using Content.Shared.Silicons.StationAi;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Console;
|
||||
using Robust.Client.Debugging;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Input;
|
||||
@@ -109,9 +108,13 @@ public sealed class SandboxUIController : UIController, IOnStateChanged<Gameplay
|
||||
|
||||
private void EnsureWindow()
|
||||
{
|
||||
if(_window is { Disposed: false })
|
||||
if (_window is { Disposed: false })
|
||||
return;
|
||||
_window = UIManager.CreateWindow<SandboxWindow>();
|
||||
// Pre-center the window without forcing it to the center every time.
|
||||
_window.OpenCentered();
|
||||
_window.Close();
|
||||
|
||||
_window.OnOpen += () => { SandboxButton!.Pressed = true; };
|
||||
_window.OnClose += () => { SandboxButton!.Pressed = false; };
|
||||
_window.ToggleLightButton.Pressed = !_light.Enabled;
|
||||
@@ -149,7 +152,6 @@ public sealed class SandboxUIController : UIController, IOnStateChanged<Gameplay
|
||||
_window.ToggleSubfloorButton.OnPressed += _ => _sandbox.ToggleSubFloor();
|
||||
_window.ShowMarkersButton.OnPressed += _ => _sandbox.ShowMarkers();
|
||||
_window.ShowBbButton.OnPressed += _ => _sandbox.ShowBb();
|
||||
_window.MachineLinkingButton.OnPressed += _ => _sandbox.MachineLinking();
|
||||
}
|
||||
|
||||
private void CheckSandboxVisibility()
|
||||
@@ -164,7 +166,7 @@ public sealed class SandboxUIController : UIController, IOnStateChanged<Gameplay
|
||||
{
|
||||
if (_window != null)
|
||||
{
|
||||
_window.Dispose();
|
||||
_window.Close();
|
||||
_window = null;
|
||||
}
|
||||
|
||||
@@ -209,7 +211,7 @@ public sealed class SandboxUIController : UIController, IOnStateChanged<Gameplay
|
||||
if (_sandbox.SandboxAllowed && _window.IsOpen != true)
|
||||
{
|
||||
UIManager.ClickSound();
|
||||
_window.OpenCentered();
|
||||
_window.Open();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -4,20 +4,24 @@
|
||||
Title="{Loc sandbox-window-title}"
|
||||
Resizable="False">
|
||||
<BoxContainer Orientation="Vertical" SeparationOverride="4">
|
||||
<Button Name="AiOverlayButton" Access="Public" Text="{Loc sandbox-window-ai-overlay-button}" ToggleMode="True"/>
|
||||
<Button Name="RespawnButton" Access="Public" Text="{Loc sandbox-window-respawn-button}"/>
|
||||
<Button Name="SpawnEntitiesButton" Access="Public" Text="{Loc sandbox-window-spawn-entities-button}"/>
|
||||
<Label Text="{Loc sandbox-window-map-editing-label}"/>
|
||||
<Button Name="SpawnTilesButton" Access="Public" Text="{Loc sandbox-window-spawn-tiles-button}"/>
|
||||
<Button Name="SpawnEntitiesButton" Access="Public" Text="{Loc sandbox-window-spawn-entities-button}"/>
|
||||
<Button Name="SpawnDecalsButton" Access="Public" Text="{Loc sandbox-window-spawn-decals-button}"/>
|
||||
<Button Name="GiveFullAccessButton" Access="Public" Text="{Loc sandbox-window-grant-full-access-button}"/>
|
||||
<Button Name="GiveAghostButton" Access="Public" Text="{Loc sandbox-window-ghost-button}"/>
|
||||
|
||||
<Label Text="{Loc sandbox-window-visibility-label}"/>
|
||||
<Button Name="ToggleLightButton" Access="Public" Text="{Loc sandbox-window-toggle-lights-button}" ToggleMode="True"/>
|
||||
<Button Name="ToggleFovButton" Access="Public" Text="{Loc sandbox-window-toggle-fov-button}" ToggleMode="True"/>
|
||||
<Button Name="ToggleShadowsButton" Access="Public" Text="{Loc sandbox-window-toggle-shadows-button}" ToggleMode="True"/>
|
||||
<Button Name="ToggleSubfloorButton" Access="Public" Text="{Loc sandbox-window-toggle-subfloor-button}" ToggleMode="True"/>
|
||||
<Button Name="SuicideButton" Access="Public" Text="{Loc sandbox-window-toggle-suicide-button}" ToggleMode="True"/>
|
||||
<Button Name="AiOverlayButton" Access="Public" Text="{Loc sandbox-window-ai-overlay-button}" ToggleMode="True"/>
|
||||
<Button Name="ShowMarkersButton" Access="Public" Text="{Loc sandbox-window-show-spawns-button}" ToggleMode="True"/>
|
||||
<Button Name="ShowBbButton" Access="Public" Text="{Loc sandbox-window-show-bb-button}" ToggleMode="True"/>
|
||||
<Button Name="MachineLinkingButton" Access="Public" Text="{Loc sandbox-window-link-machines-button}" ToggleMode="True"/>
|
||||
|
||||
<Label Text="{Loc sandbox-window-your-character-label}"/>
|
||||
<Button Name="GiveAghostButton" Access="Public" Text="{Loc sandbox-window-ghost-button}"/>
|
||||
<Button Name="GiveFullAccessButton" Access="Public" Text="{Loc sandbox-window-grant-full-access-button}"/>
|
||||
<Button Name="SuicideButton" Access="Public" Text="{Loc sandbox-window-toggle-suicide-button}"/>
|
||||
<Button Name="RespawnButton" Access="Public" Text="{Loc sandbox-window-respawn-button}"/>
|
||||
</BoxContainer>
|
||||
</windows:SandboxWindow>
|
||||
|
||||
@@ -77,6 +77,7 @@ public sealed partial class GunSystem : SharedGunSystem
|
||||
base.Initialize();
|
||||
UpdatesOutsidePrediction = true;
|
||||
SubscribeLocalEvent<AmmoCounterComponent, ItemStatusCollectMessage>(OnAmmoCounterCollect);
|
||||
SubscribeLocalEvent<AmmoCounterComponent, UpdateClientAmmoEvent>(OnUpdateClientAmmo);
|
||||
SubscribeAllEvent<MuzzleFlashEvent>(OnMuzzleFlash);
|
||||
|
||||
// Plays animated effects on the client.
|
||||
@@ -86,6 +87,11 @@ public sealed partial class GunSystem : SharedGunSystem
|
||||
InitializeSpentAmmo();
|
||||
}
|
||||
|
||||
private void OnUpdateClientAmmo(EntityUid uid, AmmoCounterComponent ammoComp, ref UpdateClientAmmoEvent args)
|
||||
{
|
||||
UpdateAmmoCount(uid, ammoComp);
|
||||
}
|
||||
|
||||
private void OnMuzzleFlash(MuzzleFlashEvent args)
|
||||
{
|
||||
var gunUid = GetEntity(args.Uid);
|
||||
|
||||
132
Content.IntegrationTests/Tests/Lathe/LatheTest.cs
Normal file
132
Content.IntegrationTests/Tests/Lathe/LatheTest.cs
Normal file
@@ -0,0 +1,132 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Content.Shared.Lathe;
|
||||
using Content.Shared.Materials;
|
||||
using Content.Shared.Prototypes;
|
||||
using Content.Shared.Research.Prototypes;
|
||||
using Content.Shared.Whitelist;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.Lathe;
|
||||
|
||||
[TestFixture]
|
||||
public sealed class LatheTest
|
||||
{
|
||||
[Test]
|
||||
public async Task TestLatheRecipeIngredientsFitLathe()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient();
|
||||
var server = pair.Server;
|
||||
|
||||
var mapData = await pair.CreateTestMap();
|
||||
|
||||
var entMan = server.EntMan;
|
||||
var protoMan = server.ProtoMan;
|
||||
var compFactory = server.ResolveDependency<IComponentFactory>();
|
||||
var materialStorageSystem = server.System<SharedMaterialStorageSystem>();
|
||||
var whitelistSystem = server.System<EntityWhitelistSystem>();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
// Find all the lathes
|
||||
var latheProtos = protoMan.EnumeratePrototypes<EntityPrototype>()
|
||||
.Where(p => !p.Abstract)
|
||||
.Where(p => !pair.IsTestPrototype(p))
|
||||
.Where(p => p.HasComponent<LatheComponent>());
|
||||
|
||||
// Find every EntityPrototype that can be inserted into a MaterialStorage
|
||||
var materialEntityProtos = protoMan.EnumeratePrototypes<EntityPrototype>()
|
||||
.Where(p => !p.Abstract)
|
||||
.Where(p => !pair.IsTestPrototype(p))
|
||||
.Where(p => p.HasComponent<PhysicalCompositionComponent>());
|
||||
|
||||
// Spawn all of the above material EntityPrototypes - we need actual entities to do whitelist checks
|
||||
var materialEntities = new List<EntityUid>(materialEntityProtos.Count());
|
||||
foreach (var materialEntityProto in materialEntityProtos)
|
||||
{
|
||||
materialEntities.Add(entMan.SpawnEntity(materialEntityProto.ID, mapData.GridCoords));
|
||||
}
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
// Check each lathe individually
|
||||
foreach (var latheProto in latheProtos)
|
||||
{
|
||||
if (!latheProto.TryGetComponent<LatheComponent>(out var latheComp, compFactory))
|
||||
continue;
|
||||
|
||||
if (!latheProto.TryGetComponent<MaterialStorageComponent>(out var storageComp, compFactory))
|
||||
continue;
|
||||
|
||||
// Test which material-containing entities are accepted by this lathe
|
||||
var acceptedMaterials = new HashSet<ProtoId<MaterialPrototype>>();
|
||||
foreach (var materialEntity in materialEntities)
|
||||
{
|
||||
Assert.That(entMan.TryGetComponent<PhysicalCompositionComponent>(materialEntity, out var compositionComponent));
|
||||
if (whitelistSystem.IsWhitelistFail(storageComp.Whitelist, materialEntity))
|
||||
continue;
|
||||
|
||||
// Mark the lathe as accepting each material in the entity
|
||||
foreach (var (material, _) in compositionComponent.MaterialComposition)
|
||||
{
|
||||
acceptedMaterials.Add(material);
|
||||
}
|
||||
}
|
||||
|
||||
// Collect all the recipes assigned to this lathe
|
||||
var recipes = new List<ProtoId<LatheRecipePrototype>>();
|
||||
recipes.AddRange(latheComp.StaticRecipes);
|
||||
recipes.AddRange(latheComp.DynamicRecipes);
|
||||
if (latheProto.TryGetComponent<EmagLatheRecipesComponent>(out var emagRecipesComp, compFactory))
|
||||
{
|
||||
recipes.AddRange(emagRecipesComp.EmagStaticRecipes);
|
||||
recipes.AddRange(emagRecipesComp.EmagDynamicRecipes);
|
||||
}
|
||||
|
||||
// Check each recipe assigned to this lathe
|
||||
foreach (var recipeId in recipes)
|
||||
{
|
||||
Assert.That(protoMan.TryIndex(recipeId, out var recipeProto));
|
||||
|
||||
// Track the total material volume of the recipe
|
||||
var totalQuantity = 0;
|
||||
// Check each material called for by the recipe
|
||||
foreach (var (materialId, quantity) in recipeProto.Materials)
|
||||
{
|
||||
Assert.That(protoMan.TryIndex(materialId, out var materialProto));
|
||||
// Make sure the material is accepted by the lathe
|
||||
Assert.That(acceptedMaterials, Does.Contain(materialId), $"Lathe {latheProto.ID} has recipe {recipeId} but does not accept any materials containing {materialId}");
|
||||
totalQuantity += quantity;
|
||||
}
|
||||
// Make sure the recipe doesn't call for more material than the lathe can hold
|
||||
if (storageComp.StorageLimit != null)
|
||||
Assert.That(totalQuantity, Is.LessThanOrEqualTo(storageComp.StorageLimit), $"Lathe {latheProto.ID} has recipe {recipeId} which calls for {totalQuantity} units of materials but can only hold {storageComp.StorageLimit}");
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task AllLatheRecipesValidTest()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient();
|
||||
|
||||
var server = pair.Server;
|
||||
var proto = server.ProtoMan;
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
foreach (var recipe in proto.EnumeratePrototypes<LatheRecipePrototype>())
|
||||
{
|
||||
if (recipe.Result == null)
|
||||
Assert.That(recipe.ResultReagents, Is.Not.Null, $"Recipe '{recipe.ID}' has no result or result reagents.");
|
||||
}
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
}
|
||||
@@ -62,6 +62,7 @@ namespace Content.IntegrationTests.Tests
|
||||
"Train",
|
||||
"Oasis",
|
||||
"Cog",
|
||||
"Gate",
|
||||
"Amber"
|
||||
};
|
||||
|
||||
|
||||
@@ -98,24 +98,4 @@ public sealed class ResearchTest
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task AllLatheRecipesValidTest()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient();
|
||||
|
||||
var server = pair.Server;
|
||||
var proto = server.ResolveDependency<IPrototypeManager>();
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
foreach (var recipe in proto.EnumeratePrototypes<LatheRecipePrototype>())
|
||||
{
|
||||
if (recipe.Result == null)
|
||||
Assert.That(recipe.ResultReagents, Is.Not.Null, $"Recipe '{recipe.ID}' has no result or result reagents.");
|
||||
}
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,6 +204,7 @@ public sealed partial class AdminVerbSystem
|
||||
var recharger = EnsureComp<BatterySelfRechargerComponent>(args.Target);
|
||||
recharger.AutoRecharge = true;
|
||||
recharger.AutoRechargeRate = battery.MaxCharge; // Instant refill.
|
||||
recharger.AutoRechargePause = false; // No delay.
|
||||
},
|
||||
Impact = LogImpact.Medium,
|
||||
Message = Loc.GetString("admin-trick-infinite-battery-object-description"),
|
||||
@@ -603,6 +604,7 @@ public sealed partial class AdminVerbSystem
|
||||
|
||||
recharger.AutoRecharge = true;
|
||||
recharger.AutoRechargeRate = battery.MaxCharge; // Instant refill.
|
||||
recharger.AutoRechargePause = false; // No delay.
|
||||
}
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Content.Server.Animals.Systems;
|
||||
using Content.Shared.Storage;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Prototypes;
|
||||
@@ -9,44 +10,47 @@ namespace Content.Server.Animals.Components;
|
||||
/// It also grants an action to players who are controlling these entities, allowing them to do it manually.
|
||||
/// </summary>
|
||||
|
||||
[RegisterComponent]
|
||||
[RegisterComponent, Access(typeof(EggLayerSystem)), AutoGenerateComponentPause]
|
||||
public sealed partial class EggLayerComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The item that gets laid/spawned, retrieved from animal prototype.
|
||||
/// </summary>
|
||||
[DataField(required: true)]
|
||||
public List<EntitySpawnEntry> EggSpawn = new();
|
||||
|
||||
/// <summary>
|
||||
/// Player action.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public EntProtoId EggLayAction = "ActionAnimalLayEgg";
|
||||
|
||||
/// <summary>
|
||||
/// The amount of nutrient consumed on update.
|
||||
/// </summary>
|
||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
||||
public float HungerUsage = 60f;
|
||||
[DataField]
|
||||
public SoundSpecifier EggLaySound = new SoundPathSpecifier("/Audio/Effects/pop.ogg");
|
||||
|
||||
/// <summary>
|
||||
/// Minimum cooldown used for the automatic egg laying.
|
||||
/// </summary>
|
||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField]
|
||||
public float EggLayCooldownMin = 60f;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum cooldown used for the automatic egg laying.
|
||||
/// </summary>
|
||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField]
|
||||
public float EggLayCooldownMax = 120f;
|
||||
|
||||
/// <summary>
|
||||
/// Set during component init.
|
||||
/// The amount of nutrient consumed on update.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public float CurrentEggLayCooldown;
|
||||
|
||||
[DataField(required: true), ViewVariables(VVAccess.ReadWrite)]
|
||||
public List<EntitySpawnEntry> EggSpawn = default!;
|
||||
|
||||
[DataField]
|
||||
public SoundSpecifier EggLaySound = new SoundPathSpecifier("/Audio/Effects/pop.ogg");
|
||||
|
||||
[DataField]
|
||||
public float AccumulatedFrametime;
|
||||
public float HungerUsage = 60f;
|
||||
|
||||
[DataField] public EntityUid? Action;
|
||||
|
||||
/// <summary>
|
||||
/// When to next try to produce.
|
||||
/// </summary>
|
||||
[DataField, AutoPausedField]
|
||||
public TimeSpan NextGrowth = TimeSpan.Zero;
|
||||
}
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
using Content.Server.Animals.Systems;
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.Chemistry.Reagent;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
|
||||
|
||||
namespace Content.Server.Animals.Components
|
||||
|
||||
/// <summary>
|
||||
/// Lets an entity produce milk. Uses hunger if present.
|
||||
/// </summary>
|
||||
{
|
||||
[RegisterComponent, Access(typeof(UdderSystem))]
|
||||
internal sealed partial class UdderComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The reagent to produce.
|
||||
/// </summary>
|
||||
[DataField, ViewVariables(VVAccess.ReadOnly)]
|
||||
public ProtoId<ReagentPrototype> ReagentId = "Milk";
|
||||
|
||||
/// <summary>
|
||||
/// The name of <see cref="Solution"/>.
|
||||
/// </summary>
|
||||
[DataField, ViewVariables(VVAccess.ReadOnly)]
|
||||
public string SolutionName = "udder";
|
||||
|
||||
/// <summary>
|
||||
/// The solution to add reagent to.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public Entity<SolutionComponent>? Solution = null;
|
||||
|
||||
/// <summary>
|
||||
/// The amount of reagent to be generated on update.
|
||||
/// </summary>
|
||||
[DataField, ViewVariables(VVAccess.ReadOnly)]
|
||||
public FixedPoint2 QuantityPerUpdate = 25;
|
||||
|
||||
/// <summary>
|
||||
/// The amount of nutrient consumed on update.
|
||||
/// </summary>
|
||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
||||
public float HungerUsage = 10f;
|
||||
|
||||
/// <summary>
|
||||
/// How long to wait before producing.
|
||||
/// </summary>
|
||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
||||
public TimeSpan GrowthDelay = TimeSpan.FromMinutes(1);
|
||||
|
||||
/// <summary>
|
||||
/// When to next try to produce.
|
||||
/// </summary>
|
||||
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite)]
|
||||
public TimeSpan NextGrowth = TimeSpan.FromSeconds(0);
|
||||
}
|
||||
}
|
||||
@@ -7,15 +7,15 @@ using Content.Shared.Nutrition.Components;
|
||||
using Content.Shared.Nutrition.EntitySystems;
|
||||
using Content.Shared.Storage;
|
||||
using Robust.Server.Audio;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Server.Animals.Systems;
|
||||
|
||||
/// <summary>
|
||||
/// Gives ability to produce eggs, produces endless if the
|
||||
/// owner has no HungerComponent
|
||||
/// Gives the ability to lay eggs/other things;
|
||||
/// produces endlessly if the owner does not have a HungerComponent.
|
||||
/// </summary>
|
||||
public sealed class EggLayerSystem : EntitySystem
|
||||
{
|
||||
@@ -23,6 +23,7 @@ public sealed class EggLayerSystem : EntitySystem
|
||||
[Dependency] private readonly ActionsSystem _actions = default!;
|
||||
[Dependency] private readonly AudioSystem _audio = default!;
|
||||
[Dependency] private readonly HungerSystem _hunger = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly PopupSystem _popup = default!;
|
||||
[Dependency] private readonly MobStateSystem _mobState = default!;
|
||||
|
||||
@@ -37,7 +38,6 @@ public sealed class EggLayerSystem : EntitySystem
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
|
||||
var query = EntityQueryEnumerator<EggLayerComponent>();
|
||||
while (query.MoveNext(out var uid, out var eggLayer))
|
||||
{
|
||||
@@ -45,13 +45,17 @@ public sealed class EggLayerSystem : EntitySystem
|
||||
if (HasComp<ActorComponent>(uid))
|
||||
continue;
|
||||
|
||||
eggLayer.AccumulatedFrametime += frameTime;
|
||||
|
||||
if (eggLayer.AccumulatedFrametime < eggLayer.CurrentEggLayCooldown)
|
||||
if (_timing.CurTime < eggLayer.NextGrowth)
|
||||
continue;
|
||||
|
||||
eggLayer.AccumulatedFrametime -= eggLayer.CurrentEggLayCooldown;
|
||||
eggLayer.CurrentEggLayCooldown = _random.NextFloat(eggLayer.EggLayCooldownMin, eggLayer.EggLayCooldownMax);
|
||||
// Randomize next growth time for more organic egglaying.
|
||||
eggLayer.NextGrowth += TimeSpan.FromSeconds(_random.NextFloat(eggLayer.EggLayCooldownMin, eggLayer.EggLayCooldownMax));
|
||||
|
||||
if (_mobState.IsDead(uid))
|
||||
continue;
|
||||
|
||||
// Hungerlevel check/modification is done in TryLayEgg()
|
||||
// so it's used for player controlled chickens as well.
|
||||
|
||||
TryLayEgg(uid, eggLayer);
|
||||
}
|
||||
@@ -60,11 +64,12 @@ public sealed class EggLayerSystem : EntitySystem
|
||||
private void OnMapInit(EntityUid uid, EggLayerComponent component, MapInitEvent args)
|
||||
{
|
||||
_actions.AddAction(uid, ref component.Action, component.EggLayAction);
|
||||
component.CurrentEggLayCooldown = _random.NextFloat(component.EggLayCooldownMin, component.EggLayCooldownMax);
|
||||
component.NextGrowth = _timing.CurTime + TimeSpan.FromSeconds(_random.NextFloat(component.EggLayCooldownMin, component.EggLayCooldownMax));
|
||||
}
|
||||
|
||||
private void OnEggLayAction(EntityUid uid, EggLayerComponent egglayer, EggLayInstantActionEvent args)
|
||||
{
|
||||
// Cooldown is handeled by ActionAnimalLayEgg in types.yml.
|
||||
args.Handled = TryLayEgg(uid, egglayer);
|
||||
}
|
||||
|
||||
@@ -76,7 +81,7 @@ public sealed class EggLayerSystem : EntitySystem
|
||||
if (_mobState.IsDead(uid))
|
||||
return false;
|
||||
|
||||
// Allow infinitely laying eggs if they can't get hungry
|
||||
// Allow infinitely laying eggs if they can't get hungry.
|
||||
if (TryComp<HungerComponent>(uid, out var hunger))
|
||||
{
|
||||
if (hunger.CurrentHunger < egglayer.HungerUsage)
|
||||
|
||||
@@ -186,6 +186,11 @@ public sealed class InnerBodyAnomalySystem : SharedInnerBodyAnomalySystem
|
||||
if (args.NewMobState != MobState.Dead)
|
||||
return;
|
||||
|
||||
var ev = new BeforeRemoveAnomalyOnDeathEvent();
|
||||
RaiseLocalEvent(args.Target, ref ev);
|
||||
if (ev.Cancelled)
|
||||
return;
|
||||
|
||||
_anomaly.ChangeAnomalyHealth(ent, -2); //Shutdown it
|
||||
}
|
||||
|
||||
|
||||
542
Content.Server/Atmos/Consoles/AtmosMonitoringConsoleSystem.cs
Normal file
542
Content.Server/Atmos/Consoles/AtmosMonitoringConsoleSystem.cs
Normal file
@@ -0,0 +1,542 @@
|
||||
using Content.Server.Atmos.Components;
|
||||
using Content.Server.Atmos.Piping.Components;
|
||||
using Content.Server.DeviceNetwork.Components;
|
||||
using Content.Server.NodeContainer;
|
||||
using Content.Server.NodeContainer.EntitySystems;
|
||||
using Content.Server.NodeContainer.NodeGroups;
|
||||
using Content.Server.NodeContainer.Nodes;
|
||||
using Content.Server.Power.Components;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Atmos.Components;
|
||||
using Content.Shared.Atmos.Consoles;
|
||||
using Content.Shared.Labels.Components;
|
||||
using Content.Shared.Pinpointer;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Timing;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
|
||||
namespace Content.Server.Atmos.Consoles;
|
||||
|
||||
public sealed class AtmosMonitoringConsoleSystem : SharedAtmosMonitoringConsoleSystem
|
||||
{
|
||||
[Dependency] private readonly UserInterfaceSystem _userInterfaceSystem = default!;
|
||||
[Dependency] private readonly SharedMapSystem _sharedMapSystem = default!;
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
|
||||
// Private variables
|
||||
// Note: this data does not need to be saved
|
||||
private Dictionary<EntityUid, Dictionary<Vector2i, AtmosPipeChunk>> _gridAtmosPipeChunks = new();
|
||||
private float _updateTimer = 1.0f;
|
||||
|
||||
// Constants
|
||||
private const float UpdateTime = 1.0f;
|
||||
private const int ChunkSize = 4;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
// Console events
|
||||
SubscribeLocalEvent<AtmosMonitoringConsoleComponent, ComponentInit>(OnConsoleInit);
|
||||
SubscribeLocalEvent<AtmosMonitoringConsoleComponent, AnchorStateChangedEvent>(OnConsoleAnchorChanged);
|
||||
SubscribeLocalEvent<AtmosMonitoringConsoleComponent, EntParentChangedMessage>(OnConsoleParentChanged);
|
||||
|
||||
// Tracked device events
|
||||
SubscribeLocalEvent<AtmosMonitoringConsoleDeviceComponent, NodeGroupsRebuilt>(OnEntityNodeGroupsRebuilt);
|
||||
SubscribeLocalEvent<AtmosMonitoringConsoleDeviceComponent, AtmosPipeColorChangedEvent>(OnEntityPipeColorChanged);
|
||||
SubscribeLocalEvent<AtmosMonitoringConsoleDeviceComponent, EntityTerminatingEvent>(OnEntityShutdown);
|
||||
|
||||
// Grid events
|
||||
SubscribeLocalEvent<GridSplitEvent>(OnGridSplit);
|
||||
}
|
||||
|
||||
#region Event handling
|
||||
|
||||
private void OnConsoleInit(EntityUid uid, AtmosMonitoringConsoleComponent component, ComponentInit args)
|
||||
{
|
||||
InitializeAtmosMonitoringConsole(uid, component);
|
||||
}
|
||||
|
||||
private void OnConsoleAnchorChanged(EntityUid uid, AtmosMonitoringConsoleComponent component, AnchorStateChangedEvent args)
|
||||
{
|
||||
InitializeAtmosMonitoringConsole(uid, component);
|
||||
}
|
||||
|
||||
private void OnConsoleParentChanged(EntityUid uid, AtmosMonitoringConsoleComponent component, EntParentChangedMessage args)
|
||||
{
|
||||
component.ForceFullUpdate = true;
|
||||
InitializeAtmosMonitoringConsole(uid, component);
|
||||
}
|
||||
|
||||
private void OnEntityNodeGroupsRebuilt(EntityUid uid, AtmosMonitoringConsoleDeviceComponent component, NodeGroupsRebuilt args)
|
||||
{
|
||||
InitializeAtmosMonitoringDevice(uid, component);
|
||||
}
|
||||
|
||||
private void OnEntityPipeColorChanged(EntityUid uid, AtmosMonitoringConsoleDeviceComponent component, AtmosPipeColorChangedEvent args)
|
||||
{
|
||||
InitializeAtmosMonitoringDevice(uid, component);
|
||||
}
|
||||
|
||||
private void OnEntityShutdown(EntityUid uid, AtmosMonitoringConsoleDeviceComponent component, EntityTerminatingEvent args)
|
||||
{
|
||||
ShutDownAtmosMonitoringEntity(uid, component);
|
||||
}
|
||||
|
||||
private void OnGridSplit(ref GridSplitEvent args)
|
||||
{
|
||||
// Collect grids
|
||||
var allGrids = args.NewGrids.ToList();
|
||||
|
||||
if (!allGrids.Contains(args.Grid))
|
||||
allGrids.Add(args.Grid);
|
||||
|
||||
// Rebuild the pipe networks on the affected grids
|
||||
foreach (var ent in allGrids)
|
||||
{
|
||||
if (!TryComp<MapGridComponent>(ent, out var grid))
|
||||
continue;
|
||||
|
||||
RebuildAtmosPipeGrid(ent, grid);
|
||||
}
|
||||
|
||||
// Update atmos monitoring consoles that stand upon an updated grid
|
||||
var query = AllEntityQuery<AtmosMonitoringConsoleComponent, TransformComponent>();
|
||||
while (query.MoveNext(out var ent, out var entConsole, out var entXform))
|
||||
{
|
||||
if (entXform.GridUid == null)
|
||||
continue;
|
||||
|
||||
if (!allGrids.Contains(entXform.GridUid.Value))
|
||||
continue;
|
||||
|
||||
InitializeAtmosMonitoringConsole(ent, entConsole);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region UI updates
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
|
||||
_updateTimer += frameTime;
|
||||
|
||||
if (_updateTimer >= UpdateTime)
|
||||
{
|
||||
_updateTimer -= UpdateTime;
|
||||
|
||||
var query = AllEntityQuery<AtmosMonitoringConsoleComponent, TransformComponent>();
|
||||
while (query.MoveNext(out var ent, out var entConsole, out var entXform))
|
||||
{
|
||||
if (entXform?.GridUid == null)
|
||||
continue;
|
||||
|
||||
UpdateUIState(ent, entConsole, entXform);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateUIState
|
||||
(EntityUid uid,
|
||||
AtmosMonitoringConsoleComponent component,
|
||||
TransformComponent xform)
|
||||
{
|
||||
if (!_userInterfaceSystem.IsUiOpen(uid, AtmosMonitoringConsoleUiKey.Key))
|
||||
return;
|
||||
|
||||
var gridUid = xform.GridUid!.Value;
|
||||
|
||||
if (!TryComp<MapGridComponent>(gridUid, out var mapGrid))
|
||||
return;
|
||||
|
||||
if (!TryComp<GridAtmosphereComponent>(gridUid, out var atmosphere))
|
||||
return;
|
||||
|
||||
// The grid must have a NavMapComponent to visualize the map in the UI
|
||||
EnsureComp<NavMapComponent>(gridUid);
|
||||
|
||||
// Gathering data to be send to the client
|
||||
var atmosNetworks = new List<AtmosMonitoringConsoleEntry>();
|
||||
var query = AllEntityQuery<GasPipeSensorComponent, TransformComponent>();
|
||||
|
||||
while (query.MoveNext(out var ent, out var entSensor, out var entXform))
|
||||
{
|
||||
if (entXform?.GridUid != xform.GridUid)
|
||||
continue;
|
||||
|
||||
if (!entXform.Anchored)
|
||||
continue;
|
||||
|
||||
var entry = CreateAtmosMonitoringConsoleEntry(ent, entXform);
|
||||
|
||||
if (entry != null)
|
||||
atmosNetworks.Add(entry.Value);
|
||||
}
|
||||
|
||||
// Set the UI state
|
||||
_userInterfaceSystem.SetUiState(uid, AtmosMonitoringConsoleUiKey.Key,
|
||||
new AtmosMonitoringConsoleBoundInterfaceState(atmosNetworks.ToArray()));
|
||||
}
|
||||
|
||||
private AtmosMonitoringConsoleEntry? CreateAtmosMonitoringConsoleEntry(EntityUid uid, TransformComponent xform)
|
||||
{
|
||||
AtmosMonitoringConsoleEntry? entry = null;
|
||||
|
||||
var netEnt = GetNetEntity(uid);
|
||||
var name = MetaData(uid).EntityName;
|
||||
var address = string.Empty;
|
||||
|
||||
if (xform.GridUid == null)
|
||||
return null;
|
||||
|
||||
if (!TryGettingFirstPipeNode(uid, out var pipeNode, out var netId) ||
|
||||
pipeNode == null ||
|
||||
netId == null)
|
||||
return null;
|
||||
|
||||
var pipeColor = TryComp<AtmosPipeColorComponent>(uid, out var colorComponent) ? colorComponent.Color : Color.White;
|
||||
|
||||
// Name the entity based on its label, if available
|
||||
if (TryComp<LabelComponent>(uid, out var label) && label.CurrentLabel != null)
|
||||
name = label.CurrentLabel;
|
||||
|
||||
// Otherwise use its base name and network address
|
||||
else if (TryComp<DeviceNetworkComponent>(uid, out var deviceNet))
|
||||
address = deviceNet.Address;
|
||||
|
||||
// Entry for unpowered devices
|
||||
if (TryComp<ApcPowerReceiverComponent>(uid, out var apcPowerReceiver) && !apcPowerReceiver.Powered)
|
||||
{
|
||||
entry = new AtmosMonitoringConsoleEntry(netEnt, GetNetCoordinates(xform.Coordinates), netId.Value, name, address)
|
||||
{
|
||||
IsPowered = false,
|
||||
Color = pipeColor
|
||||
};
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
// Entry for powered devices
|
||||
var gasData = new Dictionary<Gas, float>();
|
||||
var isAirPresent = pipeNode.Air.TotalMoles > 0;
|
||||
|
||||
if (isAirPresent)
|
||||
{
|
||||
foreach (var gas in Enum.GetValues<Gas>())
|
||||
{
|
||||
if (pipeNode.Air[(int)gas] > 0)
|
||||
gasData.Add(gas, pipeNode.Air[(int)gas] / pipeNode.Air.TotalMoles);
|
||||
}
|
||||
}
|
||||
|
||||
entry = new AtmosMonitoringConsoleEntry(netEnt, GetNetCoordinates(xform.Coordinates), netId.Value, name, address)
|
||||
{
|
||||
TemperatureData = isAirPresent ? pipeNode.Air.Temperature : 0f,
|
||||
PressureData = pipeNode.Air.Pressure,
|
||||
TotalMolData = pipeNode.Air.TotalMoles,
|
||||
GasData = gasData,
|
||||
Color = pipeColor
|
||||
};
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
private Dictionary<NetEntity, AtmosDeviceNavMapData> GetAllAtmosDeviceNavMapData(EntityUid gridUid)
|
||||
{
|
||||
var atmosDeviceNavMapData = new Dictionary<NetEntity, AtmosDeviceNavMapData>();
|
||||
|
||||
var query = AllEntityQuery<AtmosMonitoringConsoleDeviceComponent, TransformComponent>();
|
||||
while (query.MoveNext(out var ent, out var entComponent, out var entXform))
|
||||
{
|
||||
if (TryGetAtmosDeviceNavMapData(ent, entComponent, entXform, gridUid, out var data))
|
||||
atmosDeviceNavMapData.Add(data.Value.NetEntity, data.Value);
|
||||
}
|
||||
|
||||
return atmosDeviceNavMapData;
|
||||
}
|
||||
|
||||
private bool TryGetAtmosDeviceNavMapData
|
||||
(EntityUid uid,
|
||||
AtmosMonitoringConsoleDeviceComponent component,
|
||||
TransformComponent xform,
|
||||
EntityUid gridUid,
|
||||
[NotNullWhen(true)] out AtmosDeviceNavMapData? device)
|
||||
{
|
||||
device = null;
|
||||
|
||||
if (component.NavMapBlip == null)
|
||||
return false;
|
||||
|
||||
if (xform.GridUid != gridUid)
|
||||
return false;
|
||||
|
||||
if (!xform.Anchored)
|
||||
return false;
|
||||
|
||||
var direction = xform.LocalRotation.GetCardinalDir();
|
||||
|
||||
if (!TryGettingFirstPipeNode(uid, out var _, out var netId))
|
||||
netId = -1;
|
||||
|
||||
var color = Color.White;
|
||||
|
||||
if (TryComp<AtmosPipeColorComponent>(uid, out var atmosPipeColor))
|
||||
color = atmosPipeColor.Color;
|
||||
|
||||
device = new AtmosDeviceNavMapData(GetNetEntity(uid), GetNetCoordinates(xform.Coordinates), netId.Value, component.NavMapBlip.Value, direction, color);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Pipe net functions
|
||||
|
||||
private void RebuildAtmosPipeGrid(EntityUid gridUid, MapGridComponent grid)
|
||||
{
|
||||
var allChunks = new Dictionary<Vector2i, AtmosPipeChunk>();
|
||||
|
||||
// Adds all atmos pipes to the nav map via bit mask chunks
|
||||
var queryPipes = AllEntityQuery<AtmosPipeColorComponent, NodeContainerComponent, TransformComponent>();
|
||||
while (queryPipes.MoveNext(out var ent, out var entAtmosPipeColor, out var entNodeContainer, out var entXform))
|
||||
{
|
||||
if (entXform.GridUid != gridUid)
|
||||
continue;
|
||||
|
||||
if (!entXform.Anchored)
|
||||
continue;
|
||||
|
||||
var tile = _sharedMapSystem.GetTileRef(gridUid, grid, entXform.Coordinates);
|
||||
var chunkOrigin = SharedMapSystem.GetChunkIndices(tile.GridIndices, ChunkSize);
|
||||
var relative = SharedMapSystem.GetChunkRelative(tile.GridIndices, ChunkSize);
|
||||
|
||||
if (!allChunks.TryGetValue(chunkOrigin, out var chunk))
|
||||
{
|
||||
chunk = new AtmosPipeChunk(chunkOrigin);
|
||||
allChunks[chunkOrigin] = chunk;
|
||||
}
|
||||
|
||||
UpdateAtmosPipeChunk(ent, entNodeContainer, entAtmosPipeColor, GetTileIndex(relative), ref chunk);
|
||||
}
|
||||
|
||||
// Add or update the chunks on the associated grid
|
||||
_gridAtmosPipeChunks[gridUid] = allChunks;
|
||||
|
||||
// Update the consoles that are on the same grid
|
||||
var queryConsoles = AllEntityQuery<AtmosMonitoringConsoleComponent, TransformComponent>();
|
||||
while (queryConsoles.MoveNext(out var ent, out var entConsole, out var entXform))
|
||||
{
|
||||
if (gridUid != entXform.GridUid)
|
||||
continue;
|
||||
|
||||
entConsole.AtmosPipeChunks = allChunks;
|
||||
Dirty(ent, entConsole);
|
||||
}
|
||||
}
|
||||
|
||||
private void RebuildSingleTileOfPipeNetwork(EntityUid gridUid, MapGridComponent grid, EntityCoordinates coords)
|
||||
{
|
||||
if (!_gridAtmosPipeChunks.TryGetValue(gridUid, out var allChunks))
|
||||
allChunks = new Dictionary<Vector2i, AtmosPipeChunk>();
|
||||
|
||||
var tile = _sharedMapSystem.GetTileRef(gridUid, grid, coords);
|
||||
var chunkOrigin = SharedMapSystem.GetChunkIndices(tile.GridIndices, ChunkSize);
|
||||
var relative = SharedMapSystem.GetChunkRelative(tile.GridIndices, ChunkSize);
|
||||
var tileIdx = GetTileIndex(relative);
|
||||
|
||||
if (!allChunks.TryGetValue(chunkOrigin, out var chunk))
|
||||
chunk = new AtmosPipeChunk(chunkOrigin);
|
||||
|
||||
// Remove all stale values for the tile
|
||||
foreach (var (index, atmosPipeData) in chunk.AtmosPipeData)
|
||||
{
|
||||
var mask = (ulong)SharedNavMapSystem.AllDirMask << tileIdx * SharedNavMapSystem.Directions;
|
||||
chunk.AtmosPipeData[index] = atmosPipeData & ~mask;
|
||||
}
|
||||
|
||||
// Rebuild the tile's pipe data
|
||||
foreach (var ent in _sharedMapSystem.GetAnchoredEntities(gridUid, grid, coords))
|
||||
{
|
||||
if (!TryComp<AtmosPipeColorComponent>(ent, out var entAtmosPipeColor))
|
||||
continue;
|
||||
|
||||
if (!TryComp<NodeContainerComponent>(ent, out var entNodeContainer))
|
||||
continue;
|
||||
|
||||
UpdateAtmosPipeChunk(ent, entNodeContainer, entAtmosPipeColor, tileIdx, ref chunk);
|
||||
}
|
||||
|
||||
// Add or update the chunk on the associated grid
|
||||
// Only the modified chunk will be sent to the client
|
||||
chunk.LastUpdate = _gameTiming.CurTick;
|
||||
allChunks[chunkOrigin] = chunk;
|
||||
_gridAtmosPipeChunks[gridUid] = allChunks;
|
||||
|
||||
// Update the components of the monitoring consoles that are attached to the same grid
|
||||
var query = AllEntityQuery<AtmosMonitoringConsoleComponent, TransformComponent>();
|
||||
|
||||
while (query.MoveNext(out var ent, out var entConsole, out var entXform))
|
||||
{
|
||||
if (gridUid != entXform.GridUid)
|
||||
continue;
|
||||
|
||||
entConsole.AtmosPipeChunks = allChunks;
|
||||
Dirty(ent, entConsole);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateAtmosPipeChunk(EntityUid uid, NodeContainerComponent nodeContainer, AtmosPipeColorComponent pipeColor, int tileIdx, ref AtmosPipeChunk chunk)
|
||||
{
|
||||
// Entities that are actively being deleted are not to be drawn
|
||||
if (MetaData(uid).EntityLifeStage >= EntityLifeStage.Terminating)
|
||||
return;
|
||||
|
||||
foreach ((var id, var node) in nodeContainer.Nodes)
|
||||
{
|
||||
if (node is not PipeNode)
|
||||
continue;
|
||||
|
||||
var pipeNode = (PipeNode)node;
|
||||
var netId = GetPipeNodeNetId(pipeNode);
|
||||
var pipeDirection = pipeNode.CurrentPipeDirection;
|
||||
|
||||
chunk.AtmosPipeData.TryGetValue((netId, pipeColor.Color.ToHex()), out var atmosPipeData);
|
||||
atmosPipeData |= (ulong)pipeDirection << tileIdx * SharedNavMapSystem.Directions;
|
||||
chunk.AtmosPipeData[(netId, pipeColor.Color.ToHex())] = atmosPipeData;
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryGettingFirstPipeNode(EntityUid uid, [NotNullWhen(true)] out PipeNode? pipeNode, [NotNullWhen(true)] out int? netId)
|
||||
{
|
||||
pipeNode = null;
|
||||
netId = null;
|
||||
|
||||
if (!TryComp<NodeContainerComponent>(uid, out var nodeContainer))
|
||||
return false;
|
||||
|
||||
foreach (var node in nodeContainer.Nodes.Values)
|
||||
{
|
||||
if (node is PipeNode)
|
||||
{
|
||||
pipeNode = (PipeNode)node;
|
||||
netId = GetPipeNodeNetId(pipeNode);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private int GetPipeNodeNetId(PipeNode pipeNode)
|
||||
{
|
||||
if (pipeNode.NodeGroup is BaseNodeGroup)
|
||||
{
|
||||
var nodeGroup = (BaseNodeGroup)pipeNode.NodeGroup;
|
||||
|
||||
return nodeGroup.NetId;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Initialization functions
|
||||
|
||||
private void InitializeAtmosMonitoringConsole(EntityUid uid, AtmosMonitoringConsoleComponent component)
|
||||
{
|
||||
var xform = Transform(uid);
|
||||
|
||||
if (xform.GridUid == null)
|
||||
return;
|
||||
|
||||
var grid = xform.GridUid.Value;
|
||||
|
||||
if (!TryComp<MapGridComponent>(grid, out var map))
|
||||
return;
|
||||
|
||||
component.AtmosDevices = GetAllAtmosDeviceNavMapData(grid);
|
||||
|
||||
if (!_gridAtmosPipeChunks.TryGetValue(grid, out var chunks))
|
||||
{
|
||||
RebuildAtmosPipeGrid(grid, map);
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
component.AtmosPipeChunks = chunks;
|
||||
Dirty(uid, component);
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeAtmosMonitoringDevice(EntityUid uid, AtmosMonitoringConsoleDeviceComponent component)
|
||||
{
|
||||
// Rebuild tile
|
||||
var xform = Transform(uid);
|
||||
var gridUid = xform.GridUid;
|
||||
|
||||
if (gridUid != null && TryComp<MapGridComponent>(gridUid, out var grid))
|
||||
RebuildSingleTileOfPipeNetwork(gridUid.Value, grid, xform.Coordinates);
|
||||
|
||||
// Update blips on affected consoles
|
||||
if (component.NavMapBlip == null)
|
||||
return;
|
||||
|
||||
var netEntity = EntityManager.GetNetEntity(uid);
|
||||
var query = AllEntityQuery<AtmosMonitoringConsoleComponent, TransformComponent>();
|
||||
|
||||
while (query.MoveNext(out var ent, out var entConsole, out var entXform))
|
||||
{
|
||||
var isDirty = entConsole.AtmosDevices.Remove(netEntity);
|
||||
|
||||
if (gridUid != null &&
|
||||
gridUid == entXform.GridUid &&
|
||||
xform.Anchored &&
|
||||
TryGetAtmosDeviceNavMapData(uid, component, xform, gridUid.Value, out var data))
|
||||
{
|
||||
entConsole.AtmosDevices.Add(netEntity, data.Value);
|
||||
isDirty = true;
|
||||
}
|
||||
|
||||
if (isDirty)
|
||||
Dirty(ent, entConsole);
|
||||
}
|
||||
}
|
||||
|
||||
private void ShutDownAtmosMonitoringEntity(EntityUid uid, AtmosMonitoringConsoleDeviceComponent component)
|
||||
{
|
||||
// Rebuild tile
|
||||
var xform = Transform(uid);
|
||||
var gridUid = xform.GridUid;
|
||||
|
||||
if (gridUid != null && TryComp<MapGridComponent>(gridUid, out var grid))
|
||||
RebuildSingleTileOfPipeNetwork(gridUid.Value, grid, xform.Coordinates);
|
||||
|
||||
// Update blips on affected consoles
|
||||
if (component.NavMapBlip == null)
|
||||
return;
|
||||
|
||||
var netEntity = EntityManager.GetNetEntity(uid);
|
||||
var query = AllEntityQuery<AtmosMonitoringConsoleComponent>();
|
||||
|
||||
while (query.MoveNext(out var ent, out var entConsole))
|
||||
{
|
||||
if (entConsole.AtmosDevices.Remove(netEntity))
|
||||
Dirty(ent, entConsole);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private int GetTileIndex(Vector2i relativeTile)
|
||||
{
|
||||
return relativeTile.X * ChunkSize + relativeTile.Y;
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ using Content.Server.Power.Components;
|
||||
using Content.Server.Power.EntitySystems;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Atmos.Monitor;
|
||||
using Content.Shared.Atmos.Piping.Components;
|
||||
using Content.Shared.DeviceNetwork;
|
||||
using Content.Shared.Power;
|
||||
using Content.Shared.Tag;
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
using Content.Shared.Atmos;
|
||||
|
||||
namespace Content.Server.Atmos.Piping.Binary.Components
|
||||
{
|
||||
[RegisterComponent]
|
||||
public sealed partial class GasPressurePumpComponent : Component
|
||||
{
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("enabled")]
|
||||
public bool Enabled { get; set; } = true;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("inlet")]
|
||||
public string InletName { get; set; } = "inlet";
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("outlet")]
|
||||
public string OutletName { get; set; } = "outlet";
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("targetPressure")]
|
||||
public float TargetPressure { get; set; } = Atmospherics.OneAtmosphere;
|
||||
|
||||
/// <summary>
|
||||
/// Max pressure of the target gas (NOT relative to source).
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("maxTargetPressure")]
|
||||
public float MaxTargetPressure = Atmospherics.MaxOutputPressure;
|
||||
}
|
||||
}
|
||||
@@ -1,169 +1,57 @@
|
||||
using Content.Server.Administration.Logs;
|
||||
using Content.Server.Atmos.EntitySystems;
|
||||
using Content.Server.Atmos.Piping.Binary.Components;
|
||||
using Content.Server.Atmos.Piping.Components;
|
||||
using Content.Server.NodeContainer.EntitySystems;
|
||||
using Content.Server.NodeContainer.Nodes;
|
||||
using Content.Server.Power.Components;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Atmos.Piping;
|
||||
using Content.Shared.Atmos.Piping.Binary.Components;
|
||||
using Content.Shared.Atmos.Components;
|
||||
using Content.Shared.Atmos.EntitySystems;
|
||||
using Content.Shared.Audio;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Power;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Content.Server.Atmos.Piping.Binary.EntitySystems
|
||||
namespace Content.Server.Atmos.Piping.Binary.EntitySystems;
|
||||
|
||||
[UsedImplicitly]
|
||||
public sealed class GasPressurePumpSystem : SharedGasPressurePumpSystem
|
||||
{
|
||||
[UsedImplicitly]
|
||||
public sealed class GasPressurePumpSystem : EntitySystem
|
||||
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
|
||||
[Dependency] private readonly SharedAmbientSoundSystem _ambientSoundSystem = default!;
|
||||
[Dependency] private readonly NodeContainerSystem _nodeContainer = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
[Dependency] private readonly UserInterfaceSystem _userInterfaceSystem = default!;
|
||||
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
|
||||
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
|
||||
[Dependency] private readonly SharedAmbientSoundSystem _ambientSoundSystem = default!;
|
||||
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
|
||||
[Dependency] private readonly NodeContainerSystem _nodeContainer = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
||||
base.Initialize();
|
||||
|
||||
public override void Initialize()
|
||||
SubscribeLocalEvent<GasPressurePumpComponent, AtmosDeviceUpdateEvent>(OnPumpUpdated);
|
||||
}
|
||||
|
||||
private void OnPumpUpdated(EntityUid uid, GasPressurePumpComponent pump, ref AtmosDeviceUpdateEvent args)
|
||||
{
|
||||
if (!pump.Enabled
|
||||
|| (TryComp<ApcPowerReceiverComponent>(uid, out var power) && !power.Powered)
|
||||
|| !_nodeContainer.TryGetNodes(uid, pump.InletName, pump.OutletName, out PipeNode? inlet, out PipeNode? outlet))
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<GasPressurePumpComponent, ComponentInit>(OnInit);
|
||||
SubscribeLocalEvent<GasPressurePumpComponent, AtmosDeviceUpdateEvent>(OnPumpUpdated);
|
||||
SubscribeLocalEvent<GasPressurePumpComponent, AtmosDeviceDisabledEvent>(OnPumpLeaveAtmosphere);
|
||||
SubscribeLocalEvent<GasPressurePumpComponent, ExaminedEvent>(OnExamined);
|
||||
SubscribeLocalEvent<GasPressurePumpComponent, ActivateInWorldEvent>(OnPumpActivate);
|
||||
SubscribeLocalEvent<GasPressurePumpComponent, PowerChangedEvent>(OnPowerChanged);
|
||||
// Bound UI subscriptions
|
||||
SubscribeLocalEvent<GasPressurePumpComponent, GasPressurePumpChangeOutputPressureMessage>(OnOutputPressureChangeMessage);
|
||||
SubscribeLocalEvent<GasPressurePumpComponent, GasPressurePumpToggleStatusMessage>(OnToggleStatusMessage);
|
||||
_ambientSoundSystem.SetAmbience(uid, false);
|
||||
return;
|
||||
}
|
||||
|
||||
private void OnInit(EntityUid uid, GasPressurePumpComponent pump, ComponentInit args)
|
||||
var outputStartingPressure = outlet.Air.Pressure;
|
||||
|
||||
if (outputStartingPressure >= pump.TargetPressure)
|
||||
{
|
||||
UpdateAppearance(uid, pump);
|
||||
_ambientSoundSystem.SetAmbience(uid, false);
|
||||
return; // No need to pump gas if target has been reached.
|
||||
}
|
||||
|
||||
private void OnExamined(EntityUid uid, GasPressurePumpComponent pump, ExaminedEvent args)
|
||||
if (inlet.Air.TotalMoles > 0 && inlet.Air.Temperature > 0)
|
||||
{
|
||||
if (!EntityManager.GetComponent<TransformComponent>(uid).Anchored || !args.IsInDetailsRange) // Not anchored? Out of range? No status.
|
||||
return;
|
||||
// We calculate the necessary moles to transfer using our good ol' friend PV=nRT.
|
||||
var pressureDelta = pump.TargetPressure - outputStartingPressure;
|
||||
var transferMoles = (pressureDelta * outlet.Air.Volume) / (inlet.Air.Temperature * Atmospherics.R);
|
||||
|
||||
if (Loc.TryGetString("gas-pressure-pump-system-examined", out var str,
|
||||
("statusColor", "lightblue"), // TODO: change with pressure?
|
||||
("pressure", pump.TargetPressure)
|
||||
))
|
||||
{
|
||||
args.PushMarkup(str);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPowerChanged(EntityUid uid, GasPressurePumpComponent component, ref PowerChangedEvent args)
|
||||
{
|
||||
UpdateAppearance(uid, component);
|
||||
}
|
||||
|
||||
private void OnPumpUpdated(EntityUid uid, GasPressurePumpComponent pump, ref AtmosDeviceUpdateEvent args)
|
||||
{
|
||||
if (!pump.Enabled
|
||||
|| (TryComp<ApcPowerReceiverComponent>(uid, out var power) && !power.Powered)
|
||||
|| !_nodeContainer.TryGetNodes(uid, pump.InletName, pump.OutletName, out PipeNode? inlet, out PipeNode? outlet))
|
||||
{
|
||||
_ambientSoundSystem.SetAmbience(uid, false);
|
||||
return;
|
||||
}
|
||||
|
||||
var outputStartingPressure = outlet.Air.Pressure;
|
||||
|
||||
if (outputStartingPressure >= pump.TargetPressure)
|
||||
{
|
||||
_ambientSoundSystem.SetAmbience(uid, false);
|
||||
return; // No need to pump gas if target has been reached.
|
||||
}
|
||||
|
||||
if (inlet.Air.TotalMoles > 0 && inlet.Air.Temperature > 0)
|
||||
{
|
||||
// We calculate the necessary moles to transfer using our good ol' friend PV=nRT.
|
||||
var pressureDelta = pump.TargetPressure - outputStartingPressure;
|
||||
var transferMoles = (pressureDelta * outlet.Air.Volume) / (inlet.Air.Temperature * Atmospherics.R);
|
||||
|
||||
var removed = inlet.Air.Remove(transferMoles);
|
||||
_atmosphereSystem.Merge(outlet.Air, removed);
|
||||
_ambientSoundSystem.SetAmbience(uid, removed.TotalMoles > 0f);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPumpLeaveAtmosphere(EntityUid uid, GasPressurePumpComponent pump, ref AtmosDeviceDisabledEvent args)
|
||||
{
|
||||
pump.Enabled = false;
|
||||
UpdateAppearance(uid, pump);
|
||||
|
||||
DirtyUI(uid, pump);
|
||||
_userInterfaceSystem.CloseUi(uid, GasPressurePumpUiKey.Key);
|
||||
}
|
||||
|
||||
private void OnPumpActivate(EntityUid uid, GasPressurePumpComponent pump, ActivateInWorldEvent args)
|
||||
{
|
||||
if (args.Handled || !args.Complex)
|
||||
return;
|
||||
|
||||
if (!EntityManager.TryGetComponent(args.User, out ActorComponent? actor))
|
||||
return;
|
||||
|
||||
if (Transform(uid).Anchored)
|
||||
{
|
||||
_userInterfaceSystem.OpenUi(uid, GasPressurePumpUiKey.Key, actor.PlayerSession);
|
||||
DirtyUI(uid, pump);
|
||||
}
|
||||
else
|
||||
{
|
||||
_popup.PopupCursor(Loc.GetString("comp-gas-pump-ui-needs-anchor"), args.User);
|
||||
}
|
||||
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
private void OnToggleStatusMessage(EntityUid uid, GasPressurePumpComponent pump, GasPressurePumpToggleStatusMessage args)
|
||||
{
|
||||
pump.Enabled = args.Enabled;
|
||||
_adminLogger.Add(LogType.AtmosPowerChanged, LogImpact.Medium,
|
||||
$"{ToPrettyString(args.Actor):player} set the power on {ToPrettyString(uid):device} to {args.Enabled}");
|
||||
DirtyUI(uid, pump);
|
||||
UpdateAppearance(uid, pump);
|
||||
}
|
||||
|
||||
private void OnOutputPressureChangeMessage(EntityUid uid, GasPressurePumpComponent pump, GasPressurePumpChangeOutputPressureMessage args)
|
||||
{
|
||||
pump.TargetPressure = Math.Clamp(args.Pressure, 0f, Atmospherics.MaxOutputPressure);
|
||||
_adminLogger.Add(LogType.AtmosPressureChanged, LogImpact.Medium,
|
||||
$"{ToPrettyString(args.Actor):player} set the pressure on {ToPrettyString(uid):device} to {args.Pressure}kPa");
|
||||
DirtyUI(uid, pump);
|
||||
|
||||
}
|
||||
|
||||
private void DirtyUI(EntityUid uid, GasPressurePumpComponent? pump)
|
||||
{
|
||||
if (!Resolve(uid, ref pump))
|
||||
return;
|
||||
|
||||
_userInterfaceSystem.SetUiState(uid, GasPressurePumpUiKey.Key,
|
||||
new GasPressurePumpBoundUserInterfaceState(EntityManager.GetComponent<MetaDataComponent>(uid).EntityName, pump.TargetPressure, pump.Enabled));
|
||||
}
|
||||
|
||||
private void UpdateAppearance(EntityUid uid, GasPressurePumpComponent? pump = null, AppearanceComponent? appearance = null)
|
||||
{
|
||||
if (!Resolve(uid, ref pump, ref appearance, false))
|
||||
return;
|
||||
|
||||
bool pumpOn = pump.Enabled && (TryComp<ApcPowerReceiverComponent>(uid, out var power) && power.Powered);
|
||||
_appearance.SetData(uid, PumpVisuals.Enabled, pumpOn, appearance);
|
||||
var removed = inlet.Air.Remove(transferMoles);
|
||||
_atmosphereSystem.Merge(outlet.Air, removed);
|
||||
_ambientSoundSystem.SetAmbience(uid, removed.TotalMoles > 0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ using Content.Server.NodeContainer.EntitySystems;
|
||||
using Content.Server.NodeContainer.Nodes;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Atmos.Piping;
|
||||
using Content.Shared.Atmos.Piping.Components;
|
||||
using Content.Shared.Audio;
|
||||
using Content.Shared.Examine;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
@@ -10,6 +10,7 @@ using Content.Server.NodeContainer.EntitySystems;
|
||||
using Content.Server.NodeContainer.Nodes;
|
||||
using Content.Server.Power.Components;
|
||||
using Content.Shared.Atmos.Piping.Binary.Components;
|
||||
using Content.Shared.Atmos.Piping.Components;
|
||||
using Content.Shared.Atmos.Visuals;
|
||||
using Content.Shared.Audio;
|
||||
using Content.Shared.Database;
|
||||
|
||||
@@ -67,15 +67,3 @@ public readonly struct AtmosDeviceUpdateEvent(float dt, Entity<GridAtmosphereCom
|
||||
/// </summary>
|
||||
public readonly Entity<MapAtmosphereComponent?>? Map = map;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raised directed on an atmos device when it is enabled.
|
||||
/// </summary>
|
||||
[ByRefEvent]
|
||||
public record struct AtmosDeviceEnabledEvent;
|
||||
|
||||
/// <summary>
|
||||
/// Raised directed on an atmos device when it is enabled.
|
||||
/// </summary>
|
||||
[ByRefEvent]
|
||||
public record struct AtmosDeviceDisabledEvent;
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
using Content.Server.Atmos.Piping.EntitySystems;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Content.Server.Atmos.Piping.Components
|
||||
{
|
||||
[RegisterComponent]
|
||||
public sealed partial class AtmosPipeColorComponent : Component
|
||||
{
|
||||
[DataField("color")]
|
||||
public Color Color { get; set; } = Color.White;
|
||||
namespace Content.Server.Atmos.Piping.Components;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite), UsedImplicitly]
|
||||
public Color ColorVV
|
||||
{
|
||||
get => Color;
|
||||
set => IoCManager.Resolve<IEntityManager>().System<AtmosPipeColorSystem>().SetColor(Owner, this, value);
|
||||
}
|
||||
[RegisterComponent]
|
||||
public sealed partial class AtmosPipeColorComponent : Component
|
||||
{
|
||||
[DataField]
|
||||
public Color Color { get; set; } = Color.White;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite), UsedImplicitly]
|
||||
public Color ColorVV
|
||||
{
|
||||
get => Color;
|
||||
set => IoCManager.Resolve<IEntityManager>().System<AtmosPipeColorSystem>().SetColor(Owner, this, value);
|
||||
}
|
||||
}
|
||||
|
||||
[ByRefEvent]
|
||||
public record struct AtmosPipeColorChangedEvent(Color color)
|
||||
{
|
||||
public Color Color = color;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Content.Server.Atmos.Components;
|
||||
using Content.Server.Atmos.EntitySystems;
|
||||
using Content.Server.Atmos.Piping.Components;
|
||||
using Content.Shared.Atmos.Piping.Components;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
@@ -40,6 +40,9 @@ namespace Content.Server.Atmos.Piping.EntitySystems
|
||||
return;
|
||||
|
||||
_appearance.SetData(uid, PipeColorVisuals.Color, color, appearance);
|
||||
|
||||
var ev = new AtmosPipeColorChangedEvent(color);
|
||||
RaiseLocalEvent(uid, ref ev);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Content.Server.Atmos.EntitySystems;
|
||||
using Content.Server.Atmos.Piping.Components;
|
||||
using Content.Server.NodeContainer;
|
||||
using Content.Server.NodeContainer.EntitySystems;
|
||||
using Content.Server.NodeContainer.Nodes;
|
||||
using Content.Server.Popups;
|
||||
using Content.Shared.Atmos;
|
||||
@@ -8,7 +9,6 @@ using Content.Shared.Construction.Components;
|
||||
using Content.Shared.Destructible;
|
||||
using Content.Shared.Popups;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Content.Server.Atmos.Piping.EntitySystems
|
||||
{
|
||||
@@ -16,11 +16,12 @@ namespace Content.Server.Atmos.Piping.EntitySystems
|
||||
public sealed class AtmosUnsafeUnanchorSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly AtmosphereSystem _atmosphere = default!;
|
||||
[Dependency] private readonly NodeGroupSystem _group = default!;
|
||||
[Dependency] private readonly PopupSystem _popup = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
SubscribeLocalEvent<AtmosUnsafeUnanchorComponent, BeforeUnanchoredEvent>(OnBeforeUnanchored);
|
||||
SubscribeLocalEvent<AtmosUnsafeUnanchorComponent, UserUnanchoredEvent>(OnUserUnanchored);
|
||||
SubscribeLocalEvent<AtmosUnsafeUnanchorComponent, UnanchorAttemptEvent>(OnUnanchorAttempt);
|
||||
SubscribeLocalEvent<AtmosUnsafeUnanchorComponent, BreakageEventArgs>(OnBreak);
|
||||
}
|
||||
@@ -48,15 +49,22 @@ namespace Content.Server.Atmos.Piping.EntitySystems
|
||||
}
|
||||
}
|
||||
|
||||
private void OnBeforeUnanchored(EntityUid uid, AtmosUnsafeUnanchorComponent component, BeforeUnanchoredEvent args)
|
||||
// When unanchoring a pipe, leak the gas that was inside the pipe element.
|
||||
// At this point the pipe has been scheduled to be removed from the group, but that won't happen until the next Update() call in NodeGroupSystem,
|
||||
// so we have to force an update.
|
||||
// This way the gas inside other connected pipes stays unchanged, while the removed pipe is completely emptied.
|
||||
private void OnUserUnanchored(EntityUid uid, AtmosUnsafeUnanchorComponent component, UserUnanchoredEvent args)
|
||||
{
|
||||
if (component.Enabled)
|
||||
{
|
||||
_group.ForceUpdate();
|
||||
LeakGas(uid);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnBreak(EntityUid uid, AtmosUnsafeUnanchorComponent component, BreakageEventArgs args)
|
||||
{
|
||||
LeakGas(uid);
|
||||
LeakGas(uid, false);
|
||||
// Can't use DoActsBehavior["Destruction"] in the same trigger because that would prevent us
|
||||
// from leaking. So we make up for this by queueing deletion here.
|
||||
QueueDel(uid);
|
||||
@@ -64,32 +72,17 @@ namespace Content.Server.Atmos.Piping.EntitySystems
|
||||
|
||||
/// <summary>
|
||||
/// Leak gas from the uid's NodeContainer into the tile atmosphere.
|
||||
/// Setting removeFromPipe to false will duplicate the gas inside the pipe intead of moving it.
|
||||
/// This is needed to properly handle the gas in the pipe getting deleted with the pipe.
|
||||
/// </summary>
|
||||
public void LeakGas(EntityUid uid)
|
||||
public void LeakGas(EntityUid uid, bool removeFromPipe = true)
|
||||
{
|
||||
if (!EntityManager.TryGetComponent(uid, out NodeContainerComponent? nodes))
|
||||
return;
|
||||
|
||||
if (_atmosphere.GetContainingMixture(uid, true, true) is not {} environment)
|
||||
if (_atmosphere.GetContainingMixture(uid, true, true) is not { } environment)
|
||||
environment = GasMixture.SpaceGas;
|
||||
|
||||
var lost = 0f;
|
||||
var timesLost = 0;
|
||||
|
||||
foreach (var node in nodes.Nodes.Values)
|
||||
{
|
||||
if (node is not PipeNode pipe)
|
||||
continue;
|
||||
|
||||
var difference = pipe.Air.Pressure - environment.Pressure;
|
||||
lost += Math.Min(
|
||||
pipe.Volume / pipe.Air.Volume * pipe.Air.TotalMoles,
|
||||
difference * environment.Volume / (environment.Temperature * Atmospherics.R)
|
||||
);
|
||||
timesLost++;
|
||||
}
|
||||
|
||||
var sharedLoss = lost / timesLost;
|
||||
var buffer = new GasMixture();
|
||||
|
||||
foreach (var node in nodes.Nodes.Values)
|
||||
@@ -97,7 +90,13 @@ namespace Content.Server.Atmos.Piping.EntitySystems
|
||||
if (node is not PipeNode pipe)
|
||||
continue;
|
||||
|
||||
_atmosphere.Merge(buffer, pipe.Air.Remove(sharedLoss));
|
||||
if (removeFromPipe)
|
||||
_atmosphere.Merge(buffer, pipe.Air.RemoveVolume(pipe.Volume));
|
||||
else
|
||||
{
|
||||
var copy = new GasMixture(pipe.Air); //clone, then remove to keep the original untouched
|
||||
_atmosphere.Merge(buffer, copy.RemoveVolume(pipe.Volume));
|
||||
}
|
||||
}
|
||||
|
||||
_atmosphere.Merge(environment, buffer);
|
||||
|
||||
@@ -7,6 +7,7 @@ using Content.Server.NodeContainer.EntitySystems;
|
||||
using Content.Server.NodeContainer.Nodes;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Atmos.Piping;
|
||||
using Content.Shared.Atmos.Piping.Components;
|
||||
using Content.Shared.Atmos.Piping.Trinary.Components;
|
||||
using Content.Shared.Audio;
|
||||
using Content.Shared.Database;
|
||||
|
||||
@@ -7,6 +7,7 @@ using Content.Server.NodeContainer.EntitySystems;
|
||||
using Content.Server.NodeContainer.Nodes;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Atmos.Piping;
|
||||
using Content.Shared.Atmos.Piping.Components;
|
||||
using Content.Shared.Atmos.Piping.Trinary.Components;
|
||||
using Content.Shared.Audio;
|
||||
using Content.Shared.Database;
|
||||
|
||||
@@ -5,6 +5,7 @@ using Content.Server.NodeContainer;
|
||||
using Content.Server.NodeContainer.EntitySystems;
|
||||
using Content.Server.NodeContainer.Nodes;
|
||||
using Content.Shared.Atmos.Piping;
|
||||
using Content.Shared.Atmos.Piping.Components;
|
||||
using Content.Shared.Audio;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
|
||||
@@ -44,11 +44,6 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
|
||||
return;
|
||||
|
||||
portableNode.ConnectionsEnabled = args.Anchored;
|
||||
|
||||
if (EntityManager.TryGetComponent(uid, out AppearanceComponent? appearance))
|
||||
{
|
||||
_appearance.SetData(uid, GasPortableVisuals.ConnectedState, args.Anchored, appearance);
|
||||
}
|
||||
}
|
||||
|
||||
public bool FindGasPortIn(EntityUid? gridId, EntityCoordinates coordinates, [NotNullWhen(true)] out GasPortComponent? port)
|
||||
|
||||
@@ -11,6 +11,7 @@ using Content.Server.NodeContainer.EntitySystems;
|
||||
using Content.Server.NodeContainer.Nodes;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Atmos.Monitor;
|
||||
using Content.Shared.Atmos.Piping.Components;
|
||||
using Content.Shared.Atmos.Piping.Unary;
|
||||
using Content.Shared.Atmos.Piping.Unary.Components;
|
||||
using Content.Shared.Atmos.Visuals;
|
||||
|
||||
@@ -13,6 +13,7 @@ using Content.Server.Power.Components;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Atmos.Piping.Unary.Visuals;
|
||||
using Content.Shared.Atmos.Monitor;
|
||||
using Content.Shared.Atmos.Piping.Components;
|
||||
using Content.Shared.Atmos.Piping.Unary.Components;
|
||||
using Content.Shared.Audio;
|
||||
using Content.Shared.DeviceNetwork;
|
||||
|
||||
17
Content.Server/Chat/SpeakOnTriggerComponent.cs
Normal file
17
Content.Server/Chat/SpeakOnTriggerComponent.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using Content.Shared.Dataset;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.Chat;
|
||||
|
||||
/// <summary>
|
||||
/// Makes the entity speak when triggered. If the item has UseDelay component, the system will respect that cooldown.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed partial class SpeakOnTriggerComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The identifier for the dataset prototype containing messages to be spoken by this entity.
|
||||
/// </summary>
|
||||
[DataField(required: true)]
|
||||
public ProtoId<LocalizedDatasetPrototype> Pack = string.Empty;
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
using Content.Shared.Dataset;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.Chat;
|
||||
|
||||
/// <summary>
|
||||
/// Entity will say the things when activated
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed partial class SpeakOnUseComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The identifier for the dataset prototype containing messages to be spoken by this entity.
|
||||
/// </summary>
|
||||
[DataField(required: true)]
|
||||
public ProtoId<LocalizedDatasetPrototype> Pack { get; private set; }
|
||||
|
||||
}
|
||||
42
Content.Server/Chat/Systems/SpeakOnTriggerSystem.cs
Normal file
42
Content.Server/Chat/Systems/SpeakOnTriggerSystem.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using Content.Server.Explosion.EntitySystems;
|
||||
using Content.Shared.Timing;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Server.Chat.Systems;
|
||||
|
||||
public sealed class SpeakOnTriggerSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly UseDelaySystem _useDelay = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly ChatSystem _chat = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<SpeakOnTriggerComponent, TriggerEvent>(OnTrigger);
|
||||
}
|
||||
|
||||
private void OnTrigger(Entity<SpeakOnTriggerComponent> ent, ref TriggerEvent args)
|
||||
{
|
||||
TrySpeak(ent);
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
private void TrySpeak(Entity<SpeakOnTriggerComponent> ent)
|
||||
{
|
||||
// If it doesn't have the use delay component, still send the message.
|
||||
if (TryComp<UseDelayComponent>(ent.Owner, out var useDelay) && _useDelay.IsDelayed((ent.Owner, useDelay)))
|
||||
return;
|
||||
|
||||
if (!_prototypeManager.TryIndex(ent.Comp.Pack, out var messagePack))
|
||||
return;
|
||||
|
||||
var message = Loc.GetString(_random.Pick(messagePack.Values));
|
||||
_chat.TrySendInGameICMessage(ent.Owner, message, InGameICChatType.Speak, true);
|
||||
|
||||
if (useDelay != null)
|
||||
_useDelay.TryResetDelay((ent.Owner, useDelay));
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
using Content.Server.Chat;
|
||||
using Content.Shared.Dataset;
|
||||
using Content.Shared.Interaction.Events;
|
||||
using Content.Shared.Timing;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Server.Chat.Systems;
|
||||
|
||||
/// <summary>
|
||||
/// Handles the speech on activating an entity
|
||||
/// </summary>
|
||||
public sealed partial class SpeakOnUIClosedSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly UseDelaySystem _useDelay = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly ChatSystem _chat = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<SpeakOnUseComponent, UseInHandEvent>(OnUseInHand);
|
||||
}
|
||||
|
||||
public void OnUseInHand(EntityUid uid, SpeakOnUseComponent? component, UseInHandEvent args)
|
||||
{
|
||||
if (!Resolve(uid, ref component))
|
||||
return;
|
||||
|
||||
// Yes it won't work without UseDelayComponent, but we don't want any kind of spam
|
||||
if (!TryComp(uid, out UseDelayComponent? useDelay) || _useDelay.IsDelayed((uid, useDelay)))
|
||||
return;
|
||||
|
||||
if (!_prototypeManager.TryIndex(component.Pack, out var messagePack))
|
||||
return;
|
||||
|
||||
var message = Loc.GetString(_random.Pick(messagePack.Values));
|
||||
_chat.TrySendInGameICMessage(uid, message, InGameICChatType.Speak, true);
|
||||
_useDelay.TryResetDelay((uid, useDelay));
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Runtime.InteropServices;
|
||||
using Content.Server.Administration.Managers;
|
||||
using Content.Server.Chat.Managers;
|
||||
using Content.Server.Database;
|
||||
using Content.Server.GameTicking;
|
||||
@@ -56,6 +57,7 @@ namespace Content.Server.Connection
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly ILogManager _logManager = default!;
|
||||
[Dependency] private readonly IChatManager _chatManager = default!;
|
||||
[Dependency] private readonly IAdminManager _adminManager = default!;
|
||||
|
||||
private ISawmill _sawmill = default!;
|
||||
private readonly Dictionary<NetUserId, TimeSpan> _temporaryBypasses = [];
|
||||
@@ -270,7 +272,14 @@ namespace Content.Server.Connection
|
||||
ticker.PlayerGameStatuses.TryGetValue(userId, out var status) &&
|
||||
status == PlayerGameStatus.JoinedGame;
|
||||
var adminBypass = _cfg.GetCVar(CCVars.AdminBypassMaxPlayers) && adminData != null;
|
||||
if ((_plyMgr.PlayerCount >= _cfg.GetCVar(CCVars.SoftMaxPlayers) && !adminBypass) && !wasInGame)
|
||||
var softPlayerCount = _plyMgr.PlayerCount;
|
||||
|
||||
if (!_cfg.GetCVar(CCVars.AdminsCountForMaxPlayers))
|
||||
{
|
||||
softPlayerCount -= _adminManager.ActiveAdmins.Count();
|
||||
}
|
||||
|
||||
if ((softPlayerCount >= _cfg.GetCVar(CCVars.SoftMaxPlayers) && !adminBypass) && !wasInGame)
|
||||
{
|
||||
return (ConnectionDenyReason.Full, Loc.GetString("soft-player-cap-full"), null);
|
||||
}
|
||||
@@ -287,7 +296,7 @@ namespace Content.Server.Connection
|
||||
|
||||
foreach (var whitelist in _whitelists)
|
||||
{
|
||||
if (!IsValid(whitelist, _plyMgr.PlayerCount))
|
||||
if (!IsValid(whitelist, softPlayerCount))
|
||||
{
|
||||
// Not valid for current player count.
|
||||
continue;
|
||||
|
||||
@@ -1,53 +1,58 @@
|
||||
using Content.Shared.DeviceLinking;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
|
||||
namespace Content.Server.DeviceLinking.Components;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed partial class SignalTimerComponent : Component
|
||||
{
|
||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField]
|
||||
public double Delay = 5;
|
||||
|
||||
/// <summary>
|
||||
/// This shows the Label: text box in the UI.
|
||||
/// </summary>
|
||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField]
|
||||
public bool CanEditLabel = true;
|
||||
|
||||
/// <summary>
|
||||
/// The label, used for TextScreen visuals currently.
|
||||
/// </summary>
|
||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField]
|
||||
public string Label = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Default max width of a label (how many letters can this render?)
|
||||
/// </summary>
|
||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField]
|
||||
public int MaxLength = 5;
|
||||
|
||||
/// <summary>
|
||||
/// The port that gets signaled when the timer triggers.
|
||||
/// </summary>
|
||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField]
|
||||
public ProtoId<SourcePortPrototype> TriggerPort = "Timer";
|
||||
|
||||
/// <summary>
|
||||
/// The port that gets signaled when the timer starts.
|
||||
/// </summary>
|
||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField]
|
||||
public ProtoId<SourcePortPrototype> StartPort = "Start";
|
||||
|
||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField]
|
||||
public ProtoId<SinkPortPrototype> Trigger = "Trigger";
|
||||
|
||||
/// <summary>
|
||||
/// If not null, this timer will play this sound when done.
|
||||
/// </summary>
|
||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField]
|
||||
public SoundSpecifier? DoneSound;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The maximum duration in seconds
|
||||
/// When a larger number is in the input box, the display will start counting down from this one instead
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public Double MaxDuration = 3599; // 59m 59s
|
||||
}
|
||||
|
||||
@@ -152,7 +152,7 @@ public sealed class SignalTimerSystem : EntitySystem
|
||||
if (!IsMessageValid(uid, args))
|
||||
return;
|
||||
|
||||
component.Delay = args.Delay.TotalSeconds;
|
||||
component.Delay = Math.Min(args.Delay.TotalSeconds, component.MaxDuration);
|
||||
_appearanceSystem.SetData(uid, TextScreenVisuals.TargetTime, component.Delay);
|
||||
}
|
||||
|
||||
|
||||
@@ -135,12 +135,13 @@ namespace Content.Server.Disposal.Unit.EntitySystems
|
||||
else
|
||||
{
|
||||
_xformSystem.AttachToGridOrMap(entity, xform);
|
||||
var direction = holder.CurrentDirection == Direction.Invalid ? holder.PreviousDirection : holder.CurrentDirection;
|
||||
|
||||
if (holder.PreviousDirection != Direction.Invalid && _xformQuery.TryGetComponent(xform.ParentUid, out var parentXform))
|
||||
if (direction != Direction.Invalid && _xformQuery.TryGetComponent(gridUid, out var gridXform))
|
||||
{
|
||||
var direction = holder.PreviousDirection.ToAngle();
|
||||
direction += _xformSystem.GetWorldRotation(parentXform);
|
||||
_throwing.TryThrow(entity, direction.ToWorldVec() * 3f, 10f);
|
||||
var directionAngle = direction.ToAngle();
|
||||
directionAngle += _xformSystem.GetWorldRotation(gridXform);
|
||||
_throwing.TryThrow(entity, directionAngle.ToWorldVec() * 3f, 10f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
using Content.Server.Explosion.EntitySystems;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.Explosion.Components
|
||||
{
|
||||
[RegisterComponent, Access(typeof(ClusterGrenadeSystem))]
|
||||
public sealed partial class ClusterGrenadeComponent : Component
|
||||
{
|
||||
public Container GrenadesContainer = default!;
|
||||
|
||||
/// <summary>
|
||||
/// What we fill our prototype with if we want to pre-spawn with grenades.
|
||||
/// </summary>
|
||||
[DataField("fillPrototype")]
|
||||
public EntProtoId? FillPrototype;
|
||||
|
||||
/// <summary>
|
||||
/// If we have a pre-fill how many more can we spawn.
|
||||
/// </summary>
|
||||
public int UnspawnedCount;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum grenades in the container.
|
||||
/// </summary>
|
||||
[DataField("maxGrenadesCount")]
|
||||
public int MaxGrenades = 3;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum delay in seconds between individual grenade triggers
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("grenadeTriggerIntervalMax")]
|
||||
public float GrenadeTriggerIntervalMax = 0f;
|
||||
|
||||
/// <summary>
|
||||
/// Minimum delay in seconds between individual grenade triggers
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("grenadeTriggerIntervalMin")]
|
||||
public float GrenadeTriggerIntervalMin = 0f;
|
||||
|
||||
/// <summary>
|
||||
/// Minimum delay in seconds before any grenades start to be triggered.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("baseTriggerDelay")]
|
||||
public float BaseTriggerDelay = 1.0f;
|
||||
|
||||
/// <summary>
|
||||
/// Decides if grenades trigger after getting launched
|
||||
/// </summary>
|
||||
[DataField("triggerGrenades")]
|
||||
public bool TriggerGrenades = true;
|
||||
|
||||
/// <summary>
|
||||
/// Does the cluster grenade shoot or throw
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("grenadeType")]
|
||||
public Enum GrenadeType = Components.GrenadeType.Throw;
|
||||
|
||||
/// <summary>
|
||||
/// The speed at which grenades get thrown
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("velocity")]
|
||||
public float Velocity = 5;
|
||||
|
||||
/// <summary>
|
||||
/// Should the spread be random
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("randomSpread")]
|
||||
public bool RandomSpread = false;
|
||||
|
||||
/// <summary>
|
||||
/// Should the angle be random
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("randomAngle")]
|
||||
public bool RandomAngle = false;
|
||||
|
||||
/// <summary>
|
||||
/// Static distance grenades will be thrown to.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("distance")]
|
||||
public float Distance = 1f;
|
||||
|
||||
/// <summary>
|
||||
/// Max distance grenades should randomly be thrown to.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("maxSpreadDistance")]
|
||||
public float MaxSpreadDistance = 2.5f;
|
||||
|
||||
/// <summary>
|
||||
/// Minimal distance grenades should randomly be thrown to.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("minSpreadDistance")]
|
||||
public float MinSpreadDistance = 0f;
|
||||
|
||||
/// <summary>
|
||||
/// This is the end.
|
||||
/// </summary>
|
||||
public bool CountDown;
|
||||
}
|
||||
|
||||
public enum GrenadeType
|
||||
{
|
||||
Throw,
|
||||
Shoot
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
using Content.Server.Explosion.EntitySystems;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.Explosion.Components;
|
||||
/// <summary>
|
||||
/// Grenades that, when triggered, explode into projectiles
|
||||
/// </summary>
|
||||
[RegisterComponent, Access(typeof(ProjectileGrenadeSystem))]
|
||||
public sealed partial class ProjectileGrenadeComponent : Component
|
||||
{
|
||||
public Container Container = default!;
|
||||
|
||||
/// <summary>
|
||||
/// The kind of projectile that the prototype is filled with.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public EntProtoId? FillPrototype;
|
||||
|
||||
/// <summary>
|
||||
/// If we have a pre-fill how many more can we spawn.
|
||||
/// </summary>
|
||||
public int UnspawnedCount;
|
||||
|
||||
/// <summary>
|
||||
/// Total amount of projectiles
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public int Capacity = 3;
|
||||
|
||||
/// <summary>
|
||||
/// Should the angle of the projectiles be uneven?
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool RandomAngle = false;
|
||||
|
||||
/// <summary>
|
||||
/// The minimum speed the projectiles may come out at
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float MinVelocity = 2f;
|
||||
|
||||
/// <summary>
|
||||
/// The maximum speed the projectiles may come out at
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float MaxVelocity = 6f;
|
||||
}
|
||||
@@ -1,177 +0,0 @@
|
||||
using Content.Server.Explosion.Components;
|
||||
using Content.Shared.Flash.Components;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Throwing;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Random;
|
||||
using Content.Server.Weapons.Ranged.Systems;
|
||||
using System.Numerics;
|
||||
using Content.Shared.Explosion.Components;
|
||||
using Robust.Server.Containers;
|
||||
using Robust.Server.GameObjects;
|
||||
|
||||
namespace Content.Server.Explosion.EntitySystems;
|
||||
|
||||
public sealed class ClusterGrenadeSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly SharedContainerSystem _container = default!;
|
||||
[Dependency] private readonly ThrowingSystem _throwingSystem = default!;
|
||||
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
|
||||
[Dependency] private readonly GunSystem _gun = default!;
|
||||
[Dependency] private readonly TransformSystem _transformSystem = default!;
|
||||
[Dependency] private readonly ContainerSystem _containerSystem = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<ClusterGrenadeComponent, ComponentInit>(OnClugInit);
|
||||
SubscribeLocalEvent<ClusterGrenadeComponent, ComponentStartup>(OnClugStartup);
|
||||
SubscribeLocalEvent<ClusterGrenadeComponent, InteractUsingEvent>(OnClugUsing);
|
||||
SubscribeLocalEvent<ClusterGrenadeComponent, TriggerEvent>(OnClugTrigger);
|
||||
}
|
||||
|
||||
private void OnClugInit(EntityUid uid, ClusterGrenadeComponent component, ComponentInit args)
|
||||
{
|
||||
component.GrenadesContainer = _container.EnsureContainer<Container>(uid, "cluster-payload");
|
||||
}
|
||||
|
||||
private void OnClugStartup(Entity<ClusterGrenadeComponent> clug, ref ComponentStartup args)
|
||||
{
|
||||
var component = clug.Comp;
|
||||
if (component.FillPrototype != null)
|
||||
{
|
||||
component.UnspawnedCount = Math.Max(0, component.MaxGrenades - component.GrenadesContainer.ContainedEntities.Count);
|
||||
UpdateAppearance(clug);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnClugUsing(Entity<ClusterGrenadeComponent> clug, ref InteractUsingEvent args)
|
||||
{
|
||||
if (args.Handled)
|
||||
return;
|
||||
|
||||
var component = clug.Comp;
|
||||
|
||||
// TODO: Should use whitelist.
|
||||
if (component.GrenadesContainer.ContainedEntities.Count >= component.MaxGrenades ||
|
||||
!HasComp<FlashOnTriggerComponent>(args.Used))
|
||||
return;
|
||||
|
||||
_containerSystem.Insert(args.Used, component.GrenadesContainer);
|
||||
UpdateAppearance(clug);
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
private void OnClugTrigger(Entity<ClusterGrenadeComponent> clug, ref TriggerEvent args)
|
||||
{
|
||||
var component = clug.Comp;
|
||||
component.CountDown = true;
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
var query = EntityQueryEnumerator<ClusterGrenadeComponent>();
|
||||
|
||||
while (query.MoveNext(out var uid, out var clug))
|
||||
{
|
||||
if (clug.CountDown && clug.UnspawnedCount > 0)
|
||||
{
|
||||
var grenadesInserted = clug.GrenadesContainer.ContainedEntities.Count + clug.UnspawnedCount;
|
||||
var thrownCount = 0;
|
||||
var segmentAngle = 360 / grenadesInserted;
|
||||
var grenadeDelay = 0f;
|
||||
|
||||
while (TryGetGrenade(uid, clug, out var grenade))
|
||||
{
|
||||
// var distance = random.NextFloat() * _throwDistance;
|
||||
var angleMin = segmentAngle * thrownCount;
|
||||
var angleMax = segmentAngle * (thrownCount + 1);
|
||||
var angle = Angle.FromDegrees(_random.Next(angleMin, angleMax));
|
||||
if (clug.RandomAngle)
|
||||
angle = _random.NextAngle();
|
||||
thrownCount++;
|
||||
|
||||
switch (clug.GrenadeType)
|
||||
{
|
||||
case GrenadeType.Shoot:
|
||||
ShootProjectile(grenade, angle, clug, uid);
|
||||
break;
|
||||
case GrenadeType.Throw:
|
||||
ThrowGrenade(grenade, angle, clug);
|
||||
break;
|
||||
}
|
||||
|
||||
// give an active timer trigger to the contained grenades when they get launched
|
||||
if (clug.TriggerGrenades)
|
||||
{
|
||||
grenadeDelay += _random.NextFloat(clug.GrenadeTriggerIntervalMin, clug.GrenadeTriggerIntervalMax);
|
||||
var grenadeTimer = EnsureComp<ActiveTimerTriggerComponent>(grenade);
|
||||
grenadeTimer.TimeRemaining = (clug.BaseTriggerDelay + grenadeDelay);
|
||||
var ev = new ActiveTimerTriggerEvent(grenade, uid);
|
||||
RaiseLocalEvent(uid, ref ev);
|
||||
}
|
||||
}
|
||||
// delete the empty shell of the clusterbomb
|
||||
Del(uid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ShootProjectile(EntityUid grenade, Angle angle, ClusterGrenadeComponent clug, EntityUid clugUid)
|
||||
{
|
||||
var direction = angle.ToVec().Normalized();
|
||||
|
||||
if (clug.RandomSpread)
|
||||
direction = _random.NextVector2().Normalized();
|
||||
|
||||
_gun.ShootProjectile(grenade, direction, Vector2.One.Normalized(), clugUid);
|
||||
|
||||
}
|
||||
|
||||
private void ThrowGrenade(EntityUid grenade, Angle angle, ClusterGrenadeComponent clug)
|
||||
{
|
||||
var direction = angle.ToVec().Normalized() * clug.Distance;
|
||||
|
||||
if (clug.RandomSpread)
|
||||
direction = angle.ToVec().Normalized() * _random.NextFloat(clug.MinSpreadDistance, clug.MaxSpreadDistance);
|
||||
|
||||
_throwingSystem.TryThrow(grenade, direction, clug.Velocity);
|
||||
}
|
||||
|
||||
private bool TryGetGrenade(EntityUid clugUid, ClusterGrenadeComponent component, out EntityUid grenade)
|
||||
{
|
||||
grenade = default;
|
||||
|
||||
if (component.UnspawnedCount > 0)
|
||||
{
|
||||
component.UnspawnedCount--;
|
||||
grenade = Spawn(component.FillPrototype, _transformSystem.GetMapCoordinates(clugUid));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (component.GrenadesContainer.ContainedEntities.Count > 0)
|
||||
{
|
||||
grenade = component.GrenadesContainer.ContainedEntities[0];
|
||||
|
||||
// This shouldn't happen but you never know.
|
||||
if (!_containerSystem.Remove(grenade, component.GrenadesContainer))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void UpdateAppearance(Entity<ClusterGrenadeComponent> clug)
|
||||
{
|
||||
var component = clug.Comp;
|
||||
if (!TryComp<AppearanceComponent>(clug, out var appearance))
|
||||
return;
|
||||
|
||||
_appearance.SetData(clug, ClusterGrenadeVisuals.GrenadesCounter, component.GrenadesContainer.ContainedEntities.Count + component.UnspawnedCount, appearance);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
using Content.Server.Explosion.Components;
|
||||
using Content.Server.Weapons.Ranged.Systems;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Server.Explosion.EntitySystems;
|
||||
|
||||
public sealed class ProjectileGrenadeSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly GunSystem _gun = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly SharedContainerSystem _container = default!;
|
||||
[Dependency] private readonly TransformSystem _transformSystem = default!;
|
||||
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<ProjectileGrenadeComponent, ComponentInit>(OnFragInit);
|
||||
SubscribeLocalEvent<ProjectileGrenadeComponent, ComponentStartup>(OnFragStartup);
|
||||
SubscribeLocalEvent<ProjectileGrenadeComponent, TriggerEvent>(OnFragTrigger);
|
||||
}
|
||||
|
||||
private void OnFragInit(Entity<ProjectileGrenadeComponent> entity, ref ComponentInit args)
|
||||
{
|
||||
entity.Comp.Container = _container.EnsureContainer<Container>(entity.Owner, "cluster-payload");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Setting the unspawned count based on capacity so we know how many new entities to spawn
|
||||
/// </summary>
|
||||
private void OnFragStartup(Entity<ProjectileGrenadeComponent> entity, ref ComponentStartup args)
|
||||
{
|
||||
if (entity.Comp.FillPrototype == null)
|
||||
return;
|
||||
|
||||
entity.Comp.UnspawnedCount = Math.Max(0, entity.Comp.Capacity - entity.Comp.Container.ContainedEntities.Count);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Can be triggered either by damage or the use in hand timer
|
||||
/// </summary>
|
||||
private void OnFragTrigger(Entity<ProjectileGrenadeComponent> entity, ref TriggerEvent args)
|
||||
{
|
||||
FragmentIntoProjectiles(entity.Owner, entity.Comp);
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spawns projectiles at the coordinates of the grenade upon triggering
|
||||
/// Can customize the angle and velocity the projectiles come out at
|
||||
/// </summary>
|
||||
private void FragmentIntoProjectiles(EntityUid uid, ProjectileGrenadeComponent component)
|
||||
{
|
||||
var grenadeCoord = _transformSystem.GetMapCoordinates(uid);
|
||||
var shootCount = 0;
|
||||
var totalCount = component.Container.ContainedEntities.Count + component.UnspawnedCount;
|
||||
var segmentAngle = 360 / totalCount;
|
||||
|
||||
while (TrySpawnContents(grenadeCoord, component, out var contentUid))
|
||||
{
|
||||
Angle angle;
|
||||
if (component.RandomAngle)
|
||||
angle = _random.NextAngle();
|
||||
else
|
||||
{
|
||||
var angleMin = segmentAngle * shootCount;
|
||||
var angleMax = segmentAngle * (shootCount + 1);
|
||||
angle = Angle.FromDegrees(_random.Next(angleMin, angleMax));
|
||||
shootCount++;
|
||||
}
|
||||
|
||||
// velocity is randomized to make the projectiles look
|
||||
// slightly uneven, doesn't really change much, but it looks better
|
||||
var direction = angle.ToVec().Normalized();
|
||||
var velocity = _random.NextVector2(component.MinVelocity, component.MaxVelocity);
|
||||
_gun.ShootProjectile(contentUid, direction, velocity, uid, null);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spawns one instance of the fill prototype or contained entity at the coordinate indicated
|
||||
/// </summary>
|
||||
private bool TrySpawnContents(MapCoordinates spawnCoordinates, ProjectileGrenadeComponent component, out EntityUid contentUid)
|
||||
{
|
||||
contentUid = default;
|
||||
|
||||
if (component.UnspawnedCount > 0)
|
||||
{
|
||||
component.UnspawnedCount--;
|
||||
contentUid = Spawn(component.FillPrototype, spawnCoordinates);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (component.Container.ContainedEntities.Count > 0)
|
||||
{
|
||||
contentUid = component.Container.ContainedEntities[0];
|
||||
|
||||
if (!_container.Remove(contentUid, component.Container))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
using Content.Shared.Explosion.Components;
|
||||
using Content.Shared.Throwing;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Random;
|
||||
using System.Numerics;
|
||||
using Content.Shared.Explosion.EntitySystems;
|
||||
|
||||
namespace Content.Server.Explosion.EntitySystems;
|
||||
|
||||
public sealed class ScatteringGrenadeSystem : SharedScatteringGrenadeSystem
|
||||
{
|
||||
[Dependency] private readonly SharedContainerSystem _container = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly ThrowingSystem _throwingSystem = default!;
|
||||
[Dependency] private readonly TransformSystem _transformSystem = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<ScatteringGrenadeComponent, TriggerEvent>(OnScatteringTrigger);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Can be triggered either by damage or the use in hand timer, either way
|
||||
/// will store the event happening in IsTriggered for the next frame update rather than
|
||||
/// handling it here to prevent crashing the game
|
||||
/// </summary>
|
||||
private void OnScatteringTrigger(Entity<ScatteringGrenadeComponent> entity, ref TriggerEvent args)
|
||||
{
|
||||
entity.Comp.IsTriggered = true;
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Every frame update we look for scattering grenades that were triggered (by damage or timer)
|
||||
/// Then we spawn the contents, throw them, optionally trigger them, then delete the original scatter grenade entity
|
||||
/// </summary>
|
||||
public override void Update(float frametime)
|
||||
{
|
||||
base.Update(frametime);
|
||||
var query = EntityQueryEnumerator<ScatteringGrenadeComponent>();
|
||||
|
||||
while (query.MoveNext(out var uid, out var component))
|
||||
{
|
||||
var totalCount = component.Container.ContainedEntities.Count + component.UnspawnedCount;
|
||||
|
||||
// if triggered while empty, (if it's blown up while empty) it'll just delete itself
|
||||
if (component.IsTriggered && totalCount > 0)
|
||||
{
|
||||
var grenadeCoord = _transformSystem.GetMapCoordinates(uid);
|
||||
var thrownCount = 0;
|
||||
var segmentAngle = 360 / totalCount;
|
||||
var additionalIntervalDelay = 0f;
|
||||
|
||||
while (TrySpawnContents(grenadeCoord, component, out var contentUid))
|
||||
{
|
||||
Angle angle;
|
||||
if (component.RandomAngle)
|
||||
angle = _random.NextAngle();
|
||||
else
|
||||
{
|
||||
var angleMin = segmentAngle * thrownCount;
|
||||
var angleMax = segmentAngle * (thrownCount + 1);
|
||||
angle = Angle.FromDegrees(_random.Next(angleMin, angleMax));
|
||||
thrownCount++;
|
||||
}
|
||||
|
||||
Vector2 direction = angle.ToVec().Normalized();
|
||||
if (component.RandomDistance)
|
||||
direction *= _random.NextFloat(component.RandomThrowDistanceMin, component.RandomThrowDistanceMax);
|
||||
else
|
||||
direction *= component.Distance;
|
||||
|
||||
_throwingSystem.TryThrow(contentUid, direction, component.Velocity);
|
||||
|
||||
if (component.TriggerContents)
|
||||
{
|
||||
additionalIntervalDelay += _random.NextFloat(component.IntervalBetweenTriggersMin, component.IntervalBetweenTriggersMax);
|
||||
var contentTimer = EnsureComp<ActiveTimerTriggerComponent>(contentUid);
|
||||
contentTimer.TimeRemaining = component.DelayBeforeTriggerContents + additionalIntervalDelay;
|
||||
var ev = new ActiveTimerTriggerEvent(contentUid, uid);
|
||||
RaiseLocalEvent(contentUid, ref ev);
|
||||
}
|
||||
}
|
||||
|
||||
// Normally we'd use DeleteOnTrigger but because we need to wait for the frame update
|
||||
// we have to delete it here instead
|
||||
Del(uid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spawns one instance of the fill prototype or contained entity at the coordinate indicated
|
||||
/// </summary>
|
||||
private bool TrySpawnContents(MapCoordinates spawnCoordinates, ScatteringGrenadeComponent component, out EntityUid contentUid)
|
||||
{
|
||||
contentUid = default;
|
||||
|
||||
if (component.UnspawnedCount > 0)
|
||||
{
|
||||
component.UnspawnedCount--;
|
||||
contentUid = Spawn(component.FillPrototype, spawnCoordinates);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (component.Container.ContainedEntities.Count > 0)
|
||||
{
|
||||
contentUid = component.Container.ContainedEntities[0];
|
||||
|
||||
if (!_container.Remove(contentUid, component.Container))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -71,6 +71,12 @@ public sealed partial class NukeopsRuleComponent : Component
|
||||
[DataField]
|
||||
public TimeSpan WarNukieArriveDelay = TimeSpan.FromMinutes(15);
|
||||
|
||||
/// <summary>
|
||||
/// Time crew can't call emergency shuttle after war declaration.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public TimeSpan WarEvacShuttleDisabled = TimeSpan.FromMinutes(25);
|
||||
|
||||
/// <summary>
|
||||
/// Minimal operatives count for war declaration
|
||||
/// </summary>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Server.GridPreloader;
|
||||
using Content.Server.StationEvents.Events;
|
||||
using Content.Shared.GameTicking.Components;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.Maps;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.GameTicking.Rules;
|
||||
|
||||
public sealed class LoadMapRuleSystem : GameRuleSystem<LoadMapRuleComponent>
|
||||
public sealed class LoadMapRuleSystem : StationEventSystem<LoadMapRuleComponent>
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly MapSystem _map = default!;
|
||||
@@ -75,5 +75,7 @@ public sealed class LoadMapRuleSystem : GameRuleSystem<LoadMapRuleComponent>
|
||||
|
||||
var ev = new RuleLoadedGridsEvent(mapId, grids);
|
||||
RaiseLocalEvent(uid, ref ev);
|
||||
|
||||
base.Added(uid, comp, rule, args);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -312,7 +312,7 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
|
||||
{
|
||||
// Nukies must wait some time after declaration of war to get on the station
|
||||
var warTime = Timing.CurTime.Subtract(nukeops.WarDeclaredTime.Value);
|
||||
if (warTime < nukeops.WarNukieArriveDelay)
|
||||
if (warTime < nukeops.WarEvacShuttleDisabled)
|
||||
{
|
||||
ev.Cancelled = true;
|
||||
ev.Reason = Loc.GetString("war-ops-shuttle-call-unavailable");
|
||||
|
||||
@@ -29,7 +29,7 @@ namespace Content.Server.Ghost
|
||||
if (!gameTicker.PlayerGameStatuses.TryGetValue(player.UserId, out var playerStatus) ||
|
||||
playerStatus is not PlayerGameStatus.JoinedGame)
|
||||
{
|
||||
shell.WriteLine("ghost-command-error-lobby");
|
||||
shell.WriteLine(Loc.GetString("ghost-command-error-lobby"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
761
Content.Server/Holopad/HolopadSystem.cs
Normal file
761
Content.Server/Holopad/HolopadSystem.cs
Normal file
@@ -0,0 +1,761 @@
|
||||
using Content.Server.Chat.Systems;
|
||||
using Content.Server.Power.EntitySystems;
|
||||
using Content.Server.Speech.Components;
|
||||
using Content.Server.Telephone;
|
||||
using Content.Shared.Access.Systems;
|
||||
using Content.Shared.Audio;
|
||||
using Content.Shared.Chat.TypingIndicator;
|
||||
using Content.Shared.Holopad;
|
||||
using Content.Shared.IdentityManagement;
|
||||
using Content.Shared.Labels.Components;
|
||||
using Content.Shared.Silicons.StationAi;
|
||||
using Content.Shared.Telephone;
|
||||
using Content.Shared.UserInterface;
|
||||
using Content.Shared.Verbs;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
using System.Linq;
|
||||
|
||||
namespace Content.Server.Holopad;
|
||||
|
||||
public sealed class HolopadSystem : SharedHolopadSystem
|
||||
{
|
||||
[Dependency] private readonly TelephoneSystem _telephoneSystem = default!;
|
||||
[Dependency] private readonly UserInterfaceSystem _userInterfaceSystem = default!;
|
||||
[Dependency] private readonly TransformSystem _xformSystem = default!;
|
||||
[Dependency] private readonly AppearanceSystem _appearanceSystem = default!;
|
||||
[Dependency] private readonly SharedPointLightSystem _pointLightSystem = default!;
|
||||
[Dependency] private readonly SharedAmbientSoundSystem _ambientSoundSystem = default!;
|
||||
[Dependency] private readonly SharedStationAiSystem _stationAiSystem = default!;
|
||||
[Dependency] private readonly AccessReaderSystem _accessReaderSystem = default!;
|
||||
[Dependency] private readonly ChatSystem _chatSystem = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
|
||||
private float _updateTimer = 1.0f;
|
||||
|
||||
private const float UpdateTime = 1.0f;
|
||||
private const float MinTimeBetweenSyncRequests = 0.5f;
|
||||
private TimeSpan _minTimeSpanBetweenSyncRequests;
|
||||
|
||||
private HashSet<EntityUid> _pendingRequestsForSpriteState = new();
|
||||
private HashSet<EntityUid> _recentlyUpdatedHolograms = new();
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
_minTimeSpanBetweenSyncRequests = TimeSpan.FromSeconds(MinTimeBetweenSyncRequests);
|
||||
|
||||
// Holopad UI and bound user interface messages
|
||||
SubscribeLocalEvent<HolopadComponent, BeforeActivatableUIOpenEvent>(OnUIOpen);
|
||||
SubscribeLocalEvent<HolopadComponent, HolopadStartNewCallMessage>(OnHolopadStartNewCall);
|
||||
SubscribeLocalEvent<HolopadComponent, HolopadAnswerCallMessage>(OnHolopadAnswerCall);
|
||||
SubscribeLocalEvent<HolopadComponent, HolopadEndCallMessage>(OnHolopadEndCall);
|
||||
SubscribeLocalEvent<HolopadComponent, HolopadActivateProjectorMessage>(OnHolopadActivateProjector);
|
||||
SubscribeLocalEvent<HolopadComponent, HolopadStartBroadcastMessage>(OnHolopadStartBroadcast);
|
||||
SubscribeLocalEvent<HolopadComponent, HolopadStationAiRequestMessage>(OnHolopadStationAiRequest);
|
||||
|
||||
// Holopad telephone events
|
||||
SubscribeLocalEvent<HolopadComponent, TelephoneStateChangeEvent>(OnTelephoneStateChange);
|
||||
SubscribeLocalEvent<HolopadComponent, TelephoneCallCommencedEvent>(OnHoloCallCommenced);
|
||||
SubscribeLocalEvent<HolopadComponent, TelephoneCallEndedEvent>(OnHoloCallEnded);
|
||||
SubscribeLocalEvent<HolopadComponent, TelephoneMessageSentEvent>(OnTelephoneMessageSent);
|
||||
|
||||
// Networked events
|
||||
SubscribeNetworkEvent<HolopadUserTypingChangedEvent>(OnTypingChanged);
|
||||
SubscribeNetworkEvent<PlayerSpriteStateMessage>(OnPlayerSpriteStateMessage);
|
||||
|
||||
// Component start/shutdown events
|
||||
SubscribeLocalEvent<HolopadComponent, ComponentInit>(OnHolopadInit);
|
||||
SubscribeLocalEvent<HolopadComponent, ComponentShutdown>(OnHolopadShutdown);
|
||||
SubscribeLocalEvent<HolopadUserComponent, ComponentInit>(OnHolopadUserInit);
|
||||
SubscribeLocalEvent<HolopadUserComponent, ComponentShutdown>(OnHolopadUserShutdown);
|
||||
|
||||
// Misc events
|
||||
SubscribeLocalEvent<HolopadUserComponent, EmoteEvent>(OnEmote);
|
||||
SubscribeLocalEvent<HolopadUserComponent, JumpToCoreEvent>(OnJumpToCore);
|
||||
SubscribeLocalEvent<HolopadComponent, GetVerbsEvent<AlternativeVerb>>(AddToggleProjectorVerb);
|
||||
SubscribeLocalEvent<HolopadComponent, EntRemovedFromContainerMessage>(OnAiRemove);
|
||||
}
|
||||
|
||||
#region: Holopad UI bound user interface messages
|
||||
|
||||
private void OnUIOpen(Entity<HolopadComponent> entity, ref BeforeActivatableUIOpenEvent args)
|
||||
{
|
||||
UpdateUIState(entity);
|
||||
}
|
||||
|
||||
private void OnHolopadStartNewCall(Entity<HolopadComponent> source, ref HolopadStartNewCallMessage args)
|
||||
{
|
||||
if (IsHolopadControlLocked(source, args.Actor))
|
||||
return;
|
||||
|
||||
if (!TryComp<TelephoneComponent>(source, out var sourceTelephone))
|
||||
return;
|
||||
|
||||
var receiver = GetEntity(args.Receiver);
|
||||
|
||||
if (!TryComp<TelephoneComponent>(receiver, out var receiverTelephone))
|
||||
return;
|
||||
|
||||
LinkHolopadToUser(source, args.Actor);
|
||||
_telephoneSystem.CallTelephone((source, sourceTelephone), (receiver, receiverTelephone), args.Actor);
|
||||
}
|
||||
|
||||
private void OnHolopadAnswerCall(Entity<HolopadComponent> receiver, ref HolopadAnswerCallMessage args)
|
||||
{
|
||||
if (IsHolopadControlLocked(receiver, args.Actor))
|
||||
return;
|
||||
|
||||
if (!TryComp<TelephoneComponent>(receiver, out var receiverTelephone))
|
||||
return;
|
||||
|
||||
if (TryComp<StationAiHeldComponent>(args.Actor, out var userAiHeld))
|
||||
{
|
||||
var source = GetLinkedHolopads(receiver).FirstOrNull();
|
||||
|
||||
if (source != null)
|
||||
ActivateProjector(source.Value, args.Actor);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
LinkHolopadToUser(receiver, args.Actor);
|
||||
_telephoneSystem.AnswerTelephone((receiver, receiverTelephone), args.Actor);
|
||||
}
|
||||
|
||||
private void OnHolopadEndCall(Entity<HolopadComponent> entity, ref HolopadEndCallMessage args)
|
||||
{
|
||||
if (!TryComp<TelephoneComponent>(entity, out var entityTelephone))
|
||||
return;
|
||||
|
||||
if (IsHolopadControlLocked(entity, args.Actor))
|
||||
return;
|
||||
|
||||
_telephoneSystem.EndTelephoneCalls((entity, entityTelephone));
|
||||
|
||||
// If the user is an AI, end all calls originating from its
|
||||
// associated core to ensure that any broadcasts will end
|
||||
if (!TryComp<StationAiHeldComponent>(args.Actor, out var stationAiHeld) ||
|
||||
!_stationAiSystem.TryGetStationAiCore((args.Actor, stationAiHeld), out var stationAiCore))
|
||||
return;
|
||||
|
||||
if (TryComp<TelephoneComponent>(stationAiCore, out var telephone))
|
||||
_telephoneSystem.EndTelephoneCalls((stationAiCore.Value, telephone));
|
||||
}
|
||||
|
||||
private void OnHolopadActivateProjector(Entity<HolopadComponent> entity, ref HolopadActivateProjectorMessage args)
|
||||
{
|
||||
ActivateProjector(entity, args.Actor);
|
||||
}
|
||||
|
||||
private void OnHolopadStartBroadcast(Entity<HolopadComponent> source, ref HolopadStartBroadcastMessage args)
|
||||
{
|
||||
if (IsHolopadControlLocked(source, args.Actor) || IsHolopadBroadcastOnCoolDown(source))
|
||||
return;
|
||||
|
||||
if (!_accessReaderSystem.IsAllowed(args.Actor, source))
|
||||
return;
|
||||
|
||||
// AI broadcasting
|
||||
if (TryComp<StationAiHeldComponent>(args.Actor, out var stationAiHeld))
|
||||
{
|
||||
if (!_stationAiSystem.TryGetStationAiCore((args.Actor, stationAiHeld), out var stationAiCore) ||
|
||||
stationAiCore.Value.Comp.RemoteEntity == null ||
|
||||
!TryComp<HolopadComponent>(stationAiCore, out var stationAiCoreHolopad))
|
||||
return;
|
||||
|
||||
ExecuteBroadcast((stationAiCore.Value, stationAiCoreHolopad), args.Actor);
|
||||
|
||||
// Switch the AI's perspective from free roaming to the target holopad
|
||||
_xformSystem.SetCoordinates(stationAiCore.Value.Comp.RemoteEntity.Value, Transform(source).Coordinates);
|
||||
_stationAiSystem.SwitchRemoteEntityMode(stationAiCore.Value, false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Crew broadcasting
|
||||
ExecuteBroadcast(source, args.Actor);
|
||||
}
|
||||
|
||||
private void OnHolopadStationAiRequest(Entity<HolopadComponent> entity, ref HolopadStationAiRequestMessage args)
|
||||
{
|
||||
if (IsHolopadControlLocked(entity, args.Actor))
|
||||
return;
|
||||
|
||||
if (!TryComp<TelephoneComponent>(entity, out var telephone))
|
||||
return;
|
||||
|
||||
var source = new Entity<TelephoneComponent>(entity, telephone);
|
||||
var query = AllEntityQuery<StationAiCoreComponent, TelephoneComponent>();
|
||||
var reachableAiCores = new HashSet<Entity<TelephoneComponent>>();
|
||||
|
||||
while (query.MoveNext(out var receiverUid, out var receiverStationAiCore, out var receiverTelephone))
|
||||
{
|
||||
var receiver = new Entity<TelephoneComponent>(receiverUid, receiverTelephone);
|
||||
|
||||
if (!_telephoneSystem.IsSourceAbleToReachReceiver(source, receiver))
|
||||
continue;
|
||||
|
||||
if (_telephoneSystem.IsTelephoneEngaged(receiver))
|
||||
continue;
|
||||
|
||||
reachableAiCores.Add((receiverUid, receiverTelephone));
|
||||
|
||||
if (!_stationAiSystem.TryGetInsertedAI((receiver, receiverStationAiCore), out var insertedAi))
|
||||
continue;
|
||||
|
||||
if (_userInterfaceSystem.TryOpenUi(receiverUid, HolopadUiKey.AiRequestWindow, insertedAi.Value.Owner))
|
||||
LinkHolopadToUser(entity, args.Actor);
|
||||
}
|
||||
|
||||
if (!reachableAiCores.Any())
|
||||
return;
|
||||
|
||||
_telephoneSystem.BroadcastCallToTelephones(source, reachableAiCores, args.Actor);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region: Holopad telephone events
|
||||
|
||||
private void OnTelephoneStateChange(Entity<HolopadComponent> holopad, ref TelephoneStateChangeEvent args)
|
||||
{
|
||||
// Update holopad visual and ambient states
|
||||
switch (args.NewState)
|
||||
{
|
||||
case TelephoneState.Idle:
|
||||
ShutDownHolopad(holopad);
|
||||
SetHolopadAmbientState(holopad, false);
|
||||
break;
|
||||
|
||||
case TelephoneState.EndingCall:
|
||||
ShutDownHolopad(holopad);
|
||||
break;
|
||||
|
||||
default:
|
||||
SetHolopadAmbientState(holopad, this.IsPowered(holopad, EntityManager));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnHoloCallCommenced(Entity<HolopadComponent> source, ref TelephoneCallCommencedEvent args)
|
||||
{
|
||||
if (source.Comp.Hologram == null)
|
||||
GenerateHologram(source);
|
||||
|
||||
// Receiver holopad holograms have to be generated now instead of waiting for their own event
|
||||
// to fire because holographic avatars get synced immediately
|
||||
if (TryComp<HolopadComponent>(args.Receiver, out var receivingHolopad) && receivingHolopad.Hologram == null)
|
||||
GenerateHologram((args.Receiver, receivingHolopad));
|
||||
|
||||
if (source.Comp.User != null)
|
||||
{
|
||||
// Re-link the user to refresh the sprite data
|
||||
LinkHolopadToUser(source, source.Comp.User.Value);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnHoloCallEnded(Entity<HolopadComponent> entity, ref TelephoneCallEndedEvent args)
|
||||
{
|
||||
if (!TryComp<StationAiCoreComponent>(entity, out var stationAiCore))
|
||||
return;
|
||||
|
||||
// Auto-close the AI request window
|
||||
if (_stationAiSystem.TryGetInsertedAI((entity, stationAiCore), out var insertedAi))
|
||||
_userInterfaceSystem.CloseUi(entity.Owner, HolopadUiKey.AiRequestWindow, insertedAi.Value.Owner);
|
||||
}
|
||||
|
||||
private void OnTelephoneMessageSent(Entity<HolopadComponent> holopad, ref TelephoneMessageSentEvent args)
|
||||
{
|
||||
LinkHolopadToUser(holopad, args.MessageSource);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region: Networked events
|
||||
|
||||
private void OnTypingChanged(HolopadUserTypingChangedEvent ev, EntitySessionEventArgs args)
|
||||
{
|
||||
var uid = args.SenderSession.AttachedEntity;
|
||||
|
||||
if (!Exists(uid))
|
||||
return;
|
||||
|
||||
if (!TryComp<HolopadUserComponent>(uid, out var holopadUser))
|
||||
return;
|
||||
|
||||
foreach (var linkedHolopad in holopadUser.LinkedHolopads)
|
||||
{
|
||||
var receiverHolopads = GetLinkedHolopads(linkedHolopad);
|
||||
|
||||
foreach (var receiverHolopad in receiverHolopads)
|
||||
{
|
||||
if (receiverHolopad.Comp.Hologram == null)
|
||||
continue;
|
||||
|
||||
_appearanceSystem.SetData(receiverHolopad.Comp.Hologram.Value.Owner, TypingIndicatorVisuals.IsTyping, ev.IsTyping);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPlayerSpriteStateMessage(PlayerSpriteStateMessage ev, EntitySessionEventArgs args)
|
||||
{
|
||||
var uid = args.SenderSession.AttachedEntity;
|
||||
|
||||
if (!Exists(uid))
|
||||
return;
|
||||
|
||||
if (!_pendingRequestsForSpriteState.Remove(uid.Value))
|
||||
return;
|
||||
|
||||
if (!TryComp<HolopadUserComponent>(uid, out var holopadUser))
|
||||
return;
|
||||
|
||||
SyncHolopadUserWithLinkedHolograms((uid.Value, holopadUser), ev.SpriteLayerData);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region: Component start/shutdown events
|
||||
|
||||
private void OnHolopadInit(Entity<HolopadComponent> entity, ref ComponentInit args)
|
||||
{
|
||||
if (entity.Comp.User != null)
|
||||
LinkHolopadToUser(entity, entity.Comp.User.Value);
|
||||
}
|
||||
|
||||
private void OnHolopadUserInit(Entity<HolopadUserComponent> entity, ref ComponentInit args)
|
||||
{
|
||||
foreach (var linkedHolopad in entity.Comp.LinkedHolopads)
|
||||
LinkHolopadToUser(linkedHolopad, entity);
|
||||
}
|
||||
|
||||
private void OnHolopadShutdown(Entity<HolopadComponent> entity, ref ComponentShutdown args)
|
||||
{
|
||||
ShutDownHolopad(entity);
|
||||
SetHolopadAmbientState(entity, false);
|
||||
}
|
||||
|
||||
private void OnHolopadUserShutdown(Entity<HolopadUserComponent> entity, ref ComponentShutdown args)
|
||||
{
|
||||
foreach (var linkedHolopad in entity.Comp.LinkedHolopads)
|
||||
UnlinkHolopadFromUser(linkedHolopad, entity);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region: Misc events
|
||||
|
||||
private void OnEmote(Entity<HolopadUserComponent> entity, ref EmoteEvent args)
|
||||
{
|
||||
foreach (var linkedHolopad in entity.Comp.LinkedHolopads)
|
||||
{
|
||||
// Treat the ability to hear speech as the ability to also perceive emotes
|
||||
// (these are almost always going to be linked)
|
||||
if (!HasComp<ActiveListenerComponent>(linkedHolopad))
|
||||
continue;
|
||||
|
||||
if (TryComp<TelephoneComponent>(linkedHolopad, out var linkedHolopadTelephone) && linkedHolopadTelephone.Muted)
|
||||
continue;
|
||||
|
||||
foreach (var receiver in GetLinkedHolopads(linkedHolopad))
|
||||
{
|
||||
if (receiver.Comp.Hologram == null)
|
||||
continue;
|
||||
|
||||
// Name is based on the physical identity of the user
|
||||
var ent = Identity.Entity(entity, EntityManager);
|
||||
var name = Loc.GetString("holopad-hologram-name", ("name", ent));
|
||||
|
||||
// Force the emote, because if the user can do it, the hologram can too
|
||||
_chatSystem.TryEmoteWithChat(receiver.Comp.Hologram.Value, args.Emote, ChatTransmitRange.Normal, false, name, true, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnJumpToCore(Entity<HolopadUserComponent> entity, ref JumpToCoreEvent args)
|
||||
{
|
||||
if (!TryComp<StationAiHeldComponent>(entity, out var entityStationAiHeld))
|
||||
return;
|
||||
|
||||
if (!_stationAiSystem.TryGetStationAiCore((entity, entityStationAiHeld), out var stationAiCore))
|
||||
return;
|
||||
|
||||
if (!TryComp<TelephoneComponent>(stationAiCore, out var stationAiCoreTelephone))
|
||||
return;
|
||||
|
||||
_telephoneSystem.EndTelephoneCalls((stationAiCore.Value, stationAiCoreTelephone));
|
||||
}
|
||||
|
||||
private void AddToggleProjectorVerb(Entity<HolopadComponent> entity, ref GetVerbsEvent<AlternativeVerb> args)
|
||||
{
|
||||
if (!args.CanAccess || !args.CanInteract)
|
||||
return;
|
||||
|
||||
if (!this.IsPowered(entity, EntityManager))
|
||||
return;
|
||||
|
||||
if (!TryComp<TelephoneComponent>(entity, out var entityTelephone) ||
|
||||
_telephoneSystem.IsTelephoneEngaged((entity, entityTelephone)))
|
||||
return;
|
||||
|
||||
var user = args.User;
|
||||
|
||||
if (!TryComp<StationAiHeldComponent>(user, out var userAiHeld))
|
||||
return;
|
||||
|
||||
if (!_stationAiSystem.TryGetStationAiCore((user, userAiHeld), out var stationAiCore) ||
|
||||
stationAiCore.Value.Comp.RemoteEntity == null)
|
||||
return;
|
||||
|
||||
AlternativeVerb verb = new()
|
||||
{
|
||||
Act = () => ActivateProjector(entity, user),
|
||||
Text = Loc.GetString("activate-holopad-projector-verb"),
|
||||
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/vv.svg.192dpi.png")),
|
||||
};
|
||||
|
||||
args.Verbs.Add(verb);
|
||||
}
|
||||
|
||||
private void OnAiRemove(Entity<HolopadComponent> entity, ref EntRemovedFromContainerMessage args)
|
||||
{
|
||||
if (!HasComp<StationAiCoreComponent>(entity))
|
||||
return;
|
||||
|
||||
if (!TryComp<TelephoneComponent>(entity, out var entityTelephone))
|
||||
return;
|
||||
|
||||
_telephoneSystem.EndTelephoneCalls((entity, entityTelephone));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
|
||||
_updateTimer += frameTime;
|
||||
|
||||
if (_updateTimer >= UpdateTime)
|
||||
{
|
||||
_updateTimer -= UpdateTime;
|
||||
|
||||
var query = AllEntityQuery<HolopadComponent, TelephoneComponent, TransformComponent>();
|
||||
while (query.MoveNext(out var uid, out var holopad, out var telephone, out var xform))
|
||||
{
|
||||
UpdateUIState((uid, holopad), telephone);
|
||||
|
||||
if (holopad.User != null &&
|
||||
!HasComp<IgnoreUIRangeComponent>(holopad.User) &&
|
||||
!_xformSystem.InRange((holopad.User.Value, Transform(holopad.User.Value)), (uid, xform), telephone.ListeningRange))
|
||||
{
|
||||
UnlinkHolopadFromUser((uid, holopad), holopad.User.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_recentlyUpdatedHolograms.Clear();
|
||||
}
|
||||
|
||||
public void UpdateUIState(Entity<HolopadComponent> entity, TelephoneComponent? telephone = null)
|
||||
{
|
||||
if (!Resolve(entity.Owner, ref telephone, false))
|
||||
return;
|
||||
|
||||
var source = new Entity<TelephoneComponent>(entity, telephone);
|
||||
var holopads = new Dictionary<NetEntity, string>();
|
||||
|
||||
var query = AllEntityQuery<HolopadComponent, TelephoneComponent>();
|
||||
while (query.MoveNext(out var receiverUid, out var _, out var receiverTelephone))
|
||||
{
|
||||
var receiver = new Entity<TelephoneComponent>(receiverUid, receiverTelephone);
|
||||
|
||||
if (receiverTelephone.UnlistedNumber)
|
||||
continue;
|
||||
|
||||
if (source == receiver)
|
||||
continue;
|
||||
|
||||
if (!_telephoneSystem.IsSourceInRangeOfReceiver(source, receiver))
|
||||
continue;
|
||||
|
||||
var name = MetaData(receiverUid).EntityName;
|
||||
|
||||
if (TryComp<LabelComponent>(receiverUid, out var label) && !string.IsNullOrEmpty(label.CurrentLabel))
|
||||
name = label.CurrentLabel;
|
||||
|
||||
holopads.Add(GetNetEntity(receiverUid), name);
|
||||
}
|
||||
|
||||
var uiKey = HasComp<StationAiCoreComponent>(entity) ? HolopadUiKey.AiActionWindow : HolopadUiKey.InteractionWindow;
|
||||
_userInterfaceSystem.SetUiState(entity.Owner, uiKey, new HolopadBoundInterfaceState(holopads));
|
||||
}
|
||||
|
||||
private void GenerateHologram(Entity<HolopadComponent> entity)
|
||||
{
|
||||
if (entity.Comp.Hologram != null ||
|
||||
entity.Comp.HologramProtoId == null)
|
||||
return;
|
||||
|
||||
var uid = Spawn(entity.Comp.HologramProtoId, Transform(entity).Coordinates);
|
||||
|
||||
// Safeguard - spawned holograms must have this component
|
||||
if (!TryComp<HolopadHologramComponent>(uid, out var component))
|
||||
{
|
||||
Del(uid);
|
||||
return;
|
||||
}
|
||||
|
||||
entity.Comp.Hologram = new Entity<HolopadHologramComponent>(uid, component);
|
||||
}
|
||||
|
||||
private void DeleteHologram(Entity<HolopadHologramComponent> hologram, Entity<HolopadComponent> attachedHolopad)
|
||||
{
|
||||
attachedHolopad.Comp.Hologram = null;
|
||||
|
||||
QueueDel(hologram);
|
||||
}
|
||||
|
||||
private void LinkHolopadToUser(Entity<HolopadComponent> entity, EntityUid user)
|
||||
{
|
||||
if (!TryComp<HolopadUserComponent>(user, out var holopadUser))
|
||||
holopadUser = AddComp<HolopadUserComponent>(user);
|
||||
|
||||
if (user != entity.Comp.User?.Owner)
|
||||
{
|
||||
// Removes the old user from the holopad
|
||||
UnlinkHolopadFromUser(entity, entity.Comp.User);
|
||||
|
||||
// Assigns the new user in their place
|
||||
holopadUser.LinkedHolopads.Add(entity);
|
||||
entity.Comp.User = (user, holopadUser);
|
||||
}
|
||||
|
||||
if (TryComp<HolographicAvatarComponent>(user, out var avatar))
|
||||
{
|
||||
SyncHolopadUserWithLinkedHolograms((user, holopadUser), avatar.LayerData);
|
||||
return;
|
||||
}
|
||||
|
||||
// We have no apriori sprite data for the hologram, request
|
||||
// the current appearance of the user from the client
|
||||
RequestHolopadUserSpriteUpdate((user, holopadUser));
|
||||
}
|
||||
|
||||
private void UnlinkHolopadFromUser(Entity<HolopadComponent> entity, Entity<HolopadUserComponent>? user)
|
||||
{
|
||||
if (user == null)
|
||||
return;
|
||||
|
||||
entity.Comp.User = null;
|
||||
|
||||
foreach (var linkedHolopad in GetLinkedHolopads(entity))
|
||||
{
|
||||
if (linkedHolopad.Comp.Hologram != null)
|
||||
{
|
||||
_appearanceSystem.SetData(linkedHolopad.Comp.Hologram.Value.Owner, TypingIndicatorVisuals.IsTyping, false);
|
||||
|
||||
// Send message with no sprite data to the client
|
||||
// This will set the holgram sprite to a generic icon
|
||||
var ev = new PlayerSpriteStateMessage(GetNetEntity(linkedHolopad.Comp.Hologram.Value));
|
||||
RaiseNetworkEvent(ev);
|
||||
}
|
||||
}
|
||||
|
||||
if (!HasComp<HolopadUserComponent>(user))
|
||||
return;
|
||||
|
||||
user.Value.Comp.LinkedHolopads.Remove(entity);
|
||||
|
||||
if (!user.Value.Comp.LinkedHolopads.Any())
|
||||
{
|
||||
_pendingRequestsForSpriteState.Remove(user.Value);
|
||||
|
||||
if (user.Value.Comp.LifeStage < ComponentLifeStage.Stopping)
|
||||
RemComp<HolopadUserComponent>(user.Value);
|
||||
}
|
||||
}
|
||||
|
||||
private void ShutDownHolopad(Entity<HolopadComponent> entity)
|
||||
{
|
||||
entity.Comp.ControlLockoutOwner = null;
|
||||
|
||||
if (entity.Comp.Hologram != null)
|
||||
DeleteHologram(entity.Comp.Hologram.Value, entity);
|
||||
|
||||
if (entity.Comp.User != null)
|
||||
UnlinkHolopadFromUser(entity, entity.Comp.User.Value);
|
||||
|
||||
if (TryComp<StationAiCoreComponent>(entity, out var stationAiCore))
|
||||
{
|
||||
_stationAiSystem.SwitchRemoteEntityMode((entity.Owner, stationAiCore), true);
|
||||
|
||||
if (TryComp<TelephoneComponent>(entity, out var stationAiCoreTelphone))
|
||||
_telephoneSystem.EndTelephoneCalls((entity, stationAiCoreTelphone));
|
||||
}
|
||||
|
||||
Dirty(entity);
|
||||
}
|
||||
|
||||
private void RequestHolopadUserSpriteUpdate(Entity<HolopadUserComponent> user)
|
||||
{
|
||||
if (!_pendingRequestsForSpriteState.Add(user))
|
||||
return;
|
||||
|
||||
var ev = new PlayerSpriteStateRequest(GetNetEntity(user));
|
||||
RaiseNetworkEvent(ev);
|
||||
}
|
||||
|
||||
private void SyncHolopadUserWithLinkedHolograms(Entity<HolopadUserComponent> entity, PrototypeLayerData[]? spriteLayerData)
|
||||
{
|
||||
foreach (var linkedHolopad in entity.Comp.LinkedHolopads)
|
||||
{
|
||||
foreach (var receivingHolopad in GetLinkedHolopads(linkedHolopad))
|
||||
{
|
||||
if (receivingHolopad.Comp.Hologram == null || !_recentlyUpdatedHolograms.Add(receivingHolopad.Comp.Hologram.Value))
|
||||
continue;
|
||||
|
||||
var netHologram = GetNetEntity(receivingHolopad.Comp.Hologram.Value);
|
||||
var ev = new PlayerSpriteStateMessage(netHologram, spriteLayerData);
|
||||
RaiseNetworkEvent(ev);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ActivateProjector(Entity<HolopadComponent> entity, EntityUid user)
|
||||
{
|
||||
if (!TryComp<TelephoneComponent>(entity, out var receiverTelephone))
|
||||
return;
|
||||
|
||||
var receiver = new Entity<TelephoneComponent>(entity, receiverTelephone);
|
||||
|
||||
if (!TryComp<StationAiHeldComponent>(user, out var userAiHeld))
|
||||
return;
|
||||
|
||||
if (!_stationAiSystem.TryGetStationAiCore((user, userAiHeld), out var stationAiCore) ||
|
||||
stationAiCore.Value.Comp.RemoteEntity == null)
|
||||
return;
|
||||
|
||||
if (!TryComp<TelephoneComponent>(stationAiCore, out var stationAiTelephone))
|
||||
return;
|
||||
|
||||
if (!TryComp<HolopadComponent>(stationAiCore, out var stationAiHolopad))
|
||||
return;
|
||||
|
||||
var source = new Entity<TelephoneComponent>(stationAiCore.Value, stationAiTelephone);
|
||||
|
||||
// Terminate any calls that the core is hosting and immediately connect to the receiver
|
||||
_telephoneSystem.TerminateTelephoneCalls(source);
|
||||
|
||||
var callOptions = new TelephoneCallOptions()
|
||||
{
|
||||
ForceConnect = true,
|
||||
MuteReceiver = true
|
||||
};
|
||||
|
||||
_telephoneSystem.CallTelephone(source, receiver, user, callOptions);
|
||||
|
||||
if (!_telephoneSystem.IsSourceConnectedToReceiver(source, receiver))
|
||||
return;
|
||||
|
||||
LinkHolopadToUser((stationAiCore.Value, stationAiHolopad), user);
|
||||
|
||||
// Switch the AI's perspective from free roaming to the target holopad
|
||||
_xformSystem.SetCoordinates(stationAiCore.Value.Comp.RemoteEntity.Value, Transform(entity).Coordinates);
|
||||
_stationAiSystem.SwitchRemoteEntityMode(stationAiCore.Value, false);
|
||||
|
||||
// Open the holopad UI if it hasn't been opened yet
|
||||
if (TryComp<UserInterfaceComponent>(entity, out var entityUserInterfaceComponent))
|
||||
_userInterfaceSystem.OpenUi((entity, entityUserInterfaceComponent), HolopadUiKey.InteractionWindow, user);
|
||||
}
|
||||
|
||||
private void ExecuteBroadcast(Entity<HolopadComponent> source, EntityUid user)
|
||||
{
|
||||
if (!TryComp<TelephoneComponent>(source, out var sourceTelephone))
|
||||
return;
|
||||
|
||||
var sourceTelephoneEntity = new Entity<TelephoneComponent>(source, sourceTelephone);
|
||||
_telephoneSystem.TerminateTelephoneCalls(sourceTelephoneEntity);
|
||||
|
||||
// Find all holopads in range of the source
|
||||
var sourceXform = Transform(source);
|
||||
var receivers = new HashSet<Entity<TelephoneComponent>>();
|
||||
|
||||
var query = AllEntityQuery<HolopadComponent, TelephoneComponent, TransformComponent>();
|
||||
while (query.MoveNext(out var receiver, out var receiverHolopad, out var receiverTelephone, out var receiverXform))
|
||||
{
|
||||
var receiverTelephoneEntity = new Entity<TelephoneComponent>(receiver, receiverTelephone);
|
||||
|
||||
if (sourceTelephoneEntity == receiverTelephoneEntity ||
|
||||
receiverTelephone.UnlistedNumber ||
|
||||
!_telephoneSystem.IsSourceAbleToReachReceiver(sourceTelephoneEntity, receiverTelephoneEntity))
|
||||
continue;
|
||||
|
||||
// If any holopads in range are on broadcast cooldown, exit
|
||||
if (IsHolopadBroadcastOnCoolDown((receiver, receiverHolopad)))
|
||||
return;
|
||||
|
||||
receivers.Add(receiverTelephoneEntity);
|
||||
}
|
||||
|
||||
var options = new TelephoneCallOptions()
|
||||
{
|
||||
ForceConnect = true,
|
||||
MuteReceiver = true,
|
||||
};
|
||||
|
||||
_telephoneSystem.BroadcastCallToTelephones(sourceTelephoneEntity, receivers, user, options);
|
||||
|
||||
if (!_telephoneSystem.IsTelephoneEngaged(sourceTelephoneEntity))
|
||||
return;
|
||||
|
||||
// Link to the user after all the calls have been placed,
|
||||
// so we only need to sync all the holograms once
|
||||
LinkHolopadToUser(source, user);
|
||||
|
||||
// Lock out the controls of all involved holopads for a set duration
|
||||
source.Comp.ControlLockoutOwner = user;
|
||||
source.Comp.ControlLockoutStartTime = _timing.CurTime;
|
||||
|
||||
Dirty(source);
|
||||
|
||||
foreach (var receiver in GetLinkedHolopads(source))
|
||||
{
|
||||
receiver.Comp.ControlLockoutOwner = user;
|
||||
receiver.Comp.ControlLockoutStartTime = _timing.CurTime;
|
||||
|
||||
Dirty(receiver);
|
||||
}
|
||||
}
|
||||
|
||||
private HashSet<Entity<HolopadComponent>> GetLinkedHolopads(Entity<HolopadComponent> entity)
|
||||
{
|
||||
var linkedHolopads = new HashSet<Entity<HolopadComponent>>();
|
||||
|
||||
if (!TryComp<TelephoneComponent>(entity, out var holopadTelephone))
|
||||
return linkedHolopads;
|
||||
|
||||
foreach (var linkedEnt in holopadTelephone.LinkedTelephones)
|
||||
{
|
||||
if (!TryComp<HolopadComponent>(linkedEnt, out var linkedHolopad))
|
||||
continue;
|
||||
|
||||
linkedHolopads.Add((linkedEnt, linkedHolopad));
|
||||
}
|
||||
|
||||
return linkedHolopads;
|
||||
}
|
||||
|
||||
private void SetHolopadAmbientState(Entity<HolopadComponent> entity, bool isEnabled)
|
||||
{
|
||||
if (TryComp<PointLightComponent>(entity, out var pointLight))
|
||||
_pointLightSystem.SetEnabled(entity, isEnabled, pointLight);
|
||||
|
||||
if (TryComp<AmbientSoundComponent>(entity, out var ambientSound))
|
||||
_ambientSoundSystem.SetAmbience(entity, isEnabled, ambientSound);
|
||||
}
|
||||
}
|
||||
@@ -109,6 +109,10 @@ public sealed class SubdermalImplantSystem : SharedSubdermalImplantSystem
|
||||
if (TryComp<PullableComponent>(ent, out var pull) && _pullingSystem.IsPulled(ent, pull))
|
||||
_pullingSystem.TryStopPull(ent, pull);
|
||||
|
||||
// Check if the user is pulling anything, and drop it if so
|
||||
if (TryComp<PullerComponent>(ent, out var puller) && TryComp<PullableComponent>(puller.Pulling, out var pullable))
|
||||
_pullingSystem.TryStopPull(puller.Pulling.Value, pullable);
|
||||
|
||||
var xform = Transform(ent);
|
||||
var targetCoords = SelectRandomTileInRange(xform, implant.TeleportRadius);
|
||||
|
||||
|
||||
@@ -149,6 +149,13 @@ namespace Content.Server.NodeContainer.EntitySystems
|
||||
}
|
||||
}
|
||||
|
||||
// used to manually force an update for the groups
|
||||
// the VisDoUpdate will be done with the next scheduled update
|
||||
public void ForceUpdate()
|
||||
{
|
||||
DoGroupUpdates();
|
||||
}
|
||||
|
||||
private void DoGroupUpdates()
|
||||
{
|
||||
// "Why is there a separate queue for group remakes and node refloods when they both cause eachother"
|
||||
|
||||
@@ -2,7 +2,6 @@ using Content.Server.Actions;
|
||||
using Content.Server.Humanoid;
|
||||
using Content.Server.Inventory;
|
||||
using Content.Server.Mind.Commands;
|
||||
using Content.Server.Nutrition;
|
||||
using Content.Server.Polymorph.Components;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Buckle;
|
||||
@@ -13,6 +12,7 @@ using Content.Shared.IdentityManagement;
|
||||
using Content.Shared.Mind;
|
||||
using Content.Shared.Mobs.Components;
|
||||
using Content.Shared.Mobs.Systems;
|
||||
using Content.Shared.Nutrition;
|
||||
using Content.Shared.Polymorph;
|
||||
using Content.Shared.Popups;
|
||||
using Robust.Server.Audio;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using System;
|
||||
|
||||
namespace Content.Server.Power.Components
|
||||
{
|
||||
/// <summary>
|
||||
@@ -6,8 +8,29 @@ namespace Content.Server.Power.Components
|
||||
[RegisterComponent]
|
||||
public sealed partial class BatterySelfRechargerComponent : Component
|
||||
{
|
||||
[ViewVariables(VVAccess.ReadWrite)] [DataField("autoRecharge")] public bool AutoRecharge { get; set; }
|
||||
/// <summary>
|
||||
/// Does the entity auto recharge?
|
||||
/// </summary>
|
||||
[DataField] public bool AutoRecharge;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)] [DataField("autoRechargeRate")] public float AutoRechargeRate { get; set; }
|
||||
/// <summary>
|
||||
/// At what rate does the entity automatically recharge?
|
||||
/// </summary>
|
||||
[DataField] public float AutoRechargeRate;
|
||||
|
||||
/// <summary>
|
||||
/// Should this entity stop automatically recharging if a charge is used?
|
||||
/// </summary>
|
||||
[DataField] public bool AutoRechargePause = false;
|
||||
|
||||
/// <summary>
|
||||
/// How long should the entity stop automatically recharging if a charge is used?
|
||||
/// </summary>
|
||||
[DataField] public float AutoRechargePauseTime = 0f;
|
||||
|
||||
/// <summary>
|
||||
/// Do not auto recharge if this timestamp has yet to happen, set for the auto recharge pause system.
|
||||
/// </summary>
|
||||
[DataField] public TimeSpan NextAutoRecharge = TimeSpan.FromSeconds(0f);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,14 +3,18 @@ using Content.Server.Emp;
|
||||
using Content.Server.Power.Components;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.Rejuvenate;
|
||||
using Content.Shared.Timing;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Server.Power.EntitySystems
|
||||
{
|
||||
[UsedImplicitly]
|
||||
public sealed class BatterySystem : EntitySystem
|
||||
{
|
||||
[Dependency] protected readonly IGameTiming Timing = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
@@ -84,6 +88,14 @@ namespace Content.Server.Power.EntitySystems
|
||||
while (query.MoveNext(out var uid, out var comp, out var batt))
|
||||
{
|
||||
if (!comp.AutoRecharge) continue;
|
||||
if (batt.IsFullyCharged) continue;
|
||||
|
||||
if (comp.AutoRechargePause)
|
||||
{
|
||||
if (comp.NextAutoRecharge > Timing.CurTime)
|
||||
continue;
|
||||
}
|
||||
|
||||
SetCharge(uid, batt.CurrentCharge + comp.AutoRechargeRate * frameTime, batt);
|
||||
}
|
||||
}
|
||||
@@ -100,6 +112,8 @@ namespace Content.Server.Power.EntitySystems
|
||||
{
|
||||
args.Affected = true;
|
||||
UseCharge(uid, args.EnergyConsumption, component);
|
||||
// Apply a cooldown to the entity's self recharge if needed to avoid it immediately self recharging after an EMP.
|
||||
TrySetChargeCooldown(uid);
|
||||
}
|
||||
|
||||
public float UseCharge(EntityUid uid, float value, BatteryComponent? battery = null)
|
||||
@@ -110,6 +124,10 @@ namespace Content.Server.Power.EntitySystems
|
||||
var newValue = Math.Clamp(0, battery.CurrentCharge - value, battery.MaxCharge);
|
||||
var delta = newValue - battery.CurrentCharge;
|
||||
battery.CurrentCharge = newValue;
|
||||
|
||||
// Apply a cooldown to the entity's self recharge if needed.
|
||||
TrySetChargeCooldown(uid);
|
||||
|
||||
var ev = new ChargeChangedEvent(battery.CurrentCharge, battery.MaxCharge);
|
||||
RaiseLocalEvent(uid, ref ev);
|
||||
return delta;
|
||||
@@ -139,11 +157,47 @@ namespace Content.Server.Power.EntitySystems
|
||||
battery.CurrentCharge = MathHelper.Clamp(value, 0, battery.MaxCharge);
|
||||
if (MathHelper.CloseTo(battery.CurrentCharge, old) &&
|
||||
!(old != battery.CurrentCharge && battery.CurrentCharge == battery.MaxCharge))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var ev = new ChargeChangedEvent(battery.CurrentCharge, battery.MaxCharge);
|
||||
RaiseLocalEvent(uid, ref ev);
|
||||
}
|
||||
/// <summary>
|
||||
/// Checks if the entity has a self recharge and puts it on cooldown if applicable.
|
||||
/// </summary>
|
||||
public void TrySetChargeCooldown(EntityUid uid, float value = -1)
|
||||
{
|
||||
if (!TryComp<BatterySelfRechargerComponent>(uid, out var batteryself))
|
||||
return;
|
||||
|
||||
if (!batteryself.AutoRechargePause)
|
||||
return;
|
||||
|
||||
// If no answer or a negative is given for value, use the default from AutoRechargePauseTime.
|
||||
if (value < 0)
|
||||
value = batteryself.AutoRechargePauseTime;
|
||||
|
||||
if (Timing.CurTime + TimeSpan.FromSeconds(value) <= batteryself.NextAutoRecharge)
|
||||
return;
|
||||
|
||||
SetChargeCooldown(uid, batteryself.AutoRechargePauseTime, batteryself);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Puts the entity's self recharge on cooldown for the specified time.
|
||||
/// </summary>
|
||||
public void SetChargeCooldown(EntityUid uid, float value, BatterySelfRechargerComponent? batteryself = null)
|
||||
{
|
||||
if (!Resolve(uid, ref batteryself))
|
||||
return;
|
||||
|
||||
if (value >= 0)
|
||||
batteryself.NextAutoRecharge = Timing.CurTime + TimeSpan.FromSeconds(value);
|
||||
else
|
||||
batteryself.NextAutoRecharge = Timing.CurTime;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If sufficient charge is available on the battery, use it. Otherwise, don't.
|
||||
|
||||
@@ -67,7 +67,7 @@ public sealed class SiliconLawSystem : SharedSiliconLawSystem
|
||||
|
||||
var msg = Loc.GetString("laws-notify");
|
||||
var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", msg));
|
||||
_chatManager.ChatMessageToOne(ChatChannel.Server, msg, wrappedMessage, default, false, actor.PlayerSession.Channel, colorOverride: Color.FromHex("#2ed2fd"));
|
||||
_chatManager.ChatMessageToOne(ChatChannel.Server, msg, wrappedMessage, default, false, actor.PlayerSession.Channel, colorOverride: Color.FromHex("#5ed7aa"));
|
||||
|
||||
if (!TryComp<SiliconLawProviderComponent>(uid, out var lawcomp))
|
||||
return;
|
||||
|
||||
@@ -12,8 +12,8 @@ namespace Content.Server.Silicons.StationAi;
|
||||
public sealed partial class AiVisionWireAction : ComponentWireAction<StationAiVisionComponent>
|
||||
{
|
||||
public override string Name { get; set; } = "wire-name-ai-vision-light";
|
||||
public override Color Color { get; set; } = Color.DeepSkyBlue;
|
||||
public override object StatusKey => AirlockWireStatus.AiControlIndicator;
|
||||
public override Color Color { get; set; } = Color.White;
|
||||
public override object StatusKey => AirlockWireStatus.AiVisionIndicator;
|
||||
|
||||
public override StatusLightState? GetLightState(Wire wire, StationAiVisionComponent component)
|
||||
{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Chat.Managers;
|
||||
using Content.Server.Chat.Systems;
|
||||
using Content.Shared.Chat;
|
||||
using Content.Shared.Mind;
|
||||
using Content.Shared.Roles;
|
||||
@@ -8,6 +9,7 @@ using Content.Shared.StationAi;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Player;
|
||||
using static Content.Server.Chat.Systems.ChatSystem;
|
||||
|
||||
namespace Content.Server.Silicons.StationAi;
|
||||
|
||||
@@ -15,11 +17,50 @@ public sealed class StationAiSystem : SharedStationAiSystem
|
||||
{
|
||||
[Dependency] private readonly IChatManager _chats = default!;
|
||||
[Dependency] private readonly EntityLookupSystem _lookup = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _xforms = default!;
|
||||
[Dependency] private readonly SharedMindSystem _mind = default!;
|
||||
[Dependency] private readonly SharedRoleSystem _roles = default!;
|
||||
|
||||
private readonly HashSet<Entity<StationAiCoreComponent>> _ais = new();
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<ExpandICChatRecipientsEvent>(OnExpandICChatRecipients);
|
||||
}
|
||||
|
||||
private void OnExpandICChatRecipients(ExpandICChatRecipientsEvent ev)
|
||||
{
|
||||
var xformQuery = GetEntityQuery<TransformComponent>();
|
||||
var sourceXform = Transform(ev.Source);
|
||||
var sourcePos = _xforms.GetWorldPosition(sourceXform, xformQuery);
|
||||
|
||||
// This function ensures that chat popups appear on camera views that have connected microphones.
|
||||
var query = EntityManager.EntityQueryEnumerator<StationAiCoreComponent, TransformComponent>();
|
||||
while (query.MoveNext(out var ent, out var entStationAiCore, out var entXform))
|
||||
{
|
||||
var stationAiCore = new Entity<StationAiCoreComponent>(ent, entStationAiCore);
|
||||
|
||||
if (!TryGetInsertedAI(stationAiCore, out var insertedAi) || !TryComp(insertedAi, out ActorComponent? actor))
|
||||
return;
|
||||
|
||||
if (stationAiCore.Comp.RemoteEntity == null || stationAiCore.Comp.Remote)
|
||||
return;
|
||||
|
||||
var xform = Transform(stationAiCore.Comp.RemoteEntity.Value);
|
||||
|
||||
var range = (xform.MapID != sourceXform.MapID)
|
||||
? -1
|
||||
: (sourcePos - _xforms.GetWorldPosition(xform, xformQuery)).Length();
|
||||
|
||||
if (range < 0 || range > ev.VoiceRange)
|
||||
continue;
|
||||
|
||||
ev.Recipients.TryAdd(actor.PlayerSession, new ICChatRecipientData(range, false));
|
||||
}
|
||||
}
|
||||
|
||||
public override bool SetVisionEnabled(Entity<StationAiVisionComponent> entity, bool enabled, bool announce = false)
|
||||
{
|
||||
if (!base.SetVisionEnabled(entity, enabled, announce))
|
||||
|
||||
@@ -31,7 +31,7 @@ public sealed class ContainmentFieldGeneratorSystem : EntitySystem
|
||||
|
||||
SubscribeLocalEvent<ContainmentFieldGeneratorComponent, StartCollideEvent>(HandleGeneratorCollide);
|
||||
SubscribeLocalEvent<ContainmentFieldGeneratorComponent, ExaminedEvent>(OnExamine);
|
||||
SubscribeLocalEvent<ContainmentFieldGeneratorComponent, InteractHandEvent>(OnInteract);
|
||||
SubscribeLocalEvent<ContainmentFieldGeneratorComponent, ActivateInWorldEvent>(OnActivate);
|
||||
SubscribeLocalEvent<ContainmentFieldGeneratorComponent, AnchorStateChangedEvent>(OnAnchorChanged);
|
||||
SubscribeLocalEvent<ContainmentFieldGeneratorComponent, ReAnchorEvent>(OnReanchorEvent);
|
||||
SubscribeLocalEvent<ContainmentFieldGeneratorComponent, UnanchorAttemptEvent>(OnUnanchorAttempt);
|
||||
@@ -90,7 +90,7 @@ public sealed class ContainmentFieldGeneratorSystem : EntitySystem
|
||||
args.PushMarkup(Loc.GetString("comp-containment-off"));
|
||||
}
|
||||
|
||||
private void OnInteract(Entity<ContainmentFieldGeneratorComponent> generator, ref InteractHandEvent args)
|
||||
private void OnActivate(Entity<ContainmentFieldGeneratorComponent> generator, ref ActivateInWorldEvent args)
|
||||
{
|
||||
if (args.Handled)
|
||||
return;
|
||||
|
||||
@@ -45,7 +45,7 @@ namespace Content.Server.Singularity.EntitySystems
|
||||
|
||||
SubscribeLocalEvent<EmitterComponent, PowerConsumerReceivedChanged>(ReceivedChanged);
|
||||
SubscribeLocalEvent<EmitterComponent, PowerChangedEvent>(OnApcChanged);
|
||||
SubscribeLocalEvent<EmitterComponent, InteractHandEvent>(OnInteractHand);
|
||||
SubscribeLocalEvent<EmitterComponent, ActivateInWorldEvent>(OnActivate);
|
||||
SubscribeLocalEvent<EmitterComponent, GetVerbsEvent<Verb>>(OnGetVerb);
|
||||
SubscribeLocalEvent<EmitterComponent, ExaminedEvent>(OnExamined);
|
||||
SubscribeLocalEvent<EmitterComponent, AnchorStateChangedEvent>(OnAnchorStateChanged);
|
||||
@@ -60,7 +60,7 @@ namespace Content.Server.Singularity.EntitySystems
|
||||
SwitchOff(uid, component);
|
||||
}
|
||||
|
||||
private void OnInteractHand(EntityUid uid, EmitterComponent component, InteractHandEvent args)
|
||||
private void OnActivate(EntityUid uid, EmitterComponent component, ActivateInWorldEvent args)
|
||||
{
|
||||
if (args.Handled)
|
||||
return;
|
||||
|
||||
@@ -30,7 +30,7 @@ public sealed class RadiationCollectorSystem : EntitySystem
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<RadiationCollectorComponent, InteractHandEvent>(OnInteractHand);
|
||||
SubscribeLocalEvent<RadiationCollectorComponent, ActivateInWorldEvent>(OnActivate);
|
||||
SubscribeLocalEvent<RadiationCollectorComponent, OnIrradiatedEvent>(OnRadiation);
|
||||
SubscribeLocalEvent<RadiationCollectorComponent, ExaminedEvent>(OnExamined);
|
||||
SubscribeLocalEvent<RadiationCollectorComponent, GasAnalyzerScanEvent>(OnAnalyzed);
|
||||
@@ -65,7 +65,7 @@ public sealed class RadiationCollectorSystem : EntitySystem
|
||||
UpdateTankAppearance(uid, component, gasTank);
|
||||
}
|
||||
|
||||
private void OnInteractHand(EntityUid uid, RadiationCollectorComponent component, InteractHandEvent args)
|
||||
private void OnActivate(EntityUid uid, RadiationCollectorComponent component, ActivateInWorldEvent args)
|
||||
{
|
||||
if (TryComp(uid, out UseDelayComponent? useDelay) && !_useDelay.TryResetDelay((uid, useDelay), true))
|
||||
return;
|
||||
|
||||
@@ -6,6 +6,7 @@ using Content.Shared.Doors.Components;
|
||||
using Content.Shared.Doors.Systems;
|
||||
using Content.Shared.Lock;
|
||||
using Content.Shared.GameTicking.Components;
|
||||
using Content.Shared.Station.Components;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
@@ -43,6 +44,9 @@ public sealed class GreytideVirusRule : StationEventSystem<GreytideVirusRuleComp
|
||||
if (virusComp.Severity == null)
|
||||
return;
|
||||
|
||||
if (!TryGetRandomStation(out var chosenStation))
|
||||
return;
|
||||
|
||||
// pick random access groups
|
||||
var chosen = _random.GetItems(virusComp.AccessGroups, virusComp.Severity.Value, allowDuplicates: false);
|
||||
|
||||
@@ -57,12 +61,16 @@ public sealed class GreytideVirusRule : StationEventSystem<GreytideVirusRuleComp
|
||||
var firelockQuery = GetEntityQuery<FirelockComponent>();
|
||||
var accessQuery = GetEntityQuery<AccessReaderComponent>();
|
||||
|
||||
var lockQuery = AllEntityQuery<LockComponent>();
|
||||
while (lockQuery.MoveNext(out var lockUid, out var lockComp))
|
||||
var lockQuery = AllEntityQuery<LockComponent, TransformComponent>();
|
||||
while (lockQuery.MoveNext(out var lockUid, out var lockComp, out var xform))
|
||||
{
|
||||
if (!accessQuery.TryComp(lockUid, out var accessComp))
|
||||
continue;
|
||||
|
||||
// make sure not to hit CentCom or other maps
|
||||
if (CompOrNull<StationMemberComponent>(xform.GridUid)?.Station != chosenStation)
|
||||
continue;
|
||||
|
||||
// check access
|
||||
// the AreAccessTagsAllowed function is a little weird because it technically has support for certain tags to be locked out of opening something
|
||||
// which might have unintened side effects (see the comments in the function itself)
|
||||
@@ -74,13 +82,17 @@ public sealed class GreytideVirusRule : StationEventSystem<GreytideVirusRuleComp
|
||||
_lock.Unlock(lockUid, null, lockComp);
|
||||
}
|
||||
|
||||
var airlockQuery = AllEntityQuery<AirlockComponent, DoorComponent>();
|
||||
while (airlockQuery.MoveNext(out var airlockUid, out var airlockComp, out var doorComp))
|
||||
var airlockQuery = AllEntityQuery<AirlockComponent, DoorComponent, TransformComponent>();
|
||||
while (airlockQuery.MoveNext(out var airlockUid, out var airlockComp, out var doorComp, out var xform))
|
||||
{
|
||||
// don't space everything
|
||||
if (firelockQuery.HasComp(airlockUid))
|
||||
continue;
|
||||
|
||||
// make sure not to hit CentCom or other maps
|
||||
if (CompOrNull<StationMemberComponent>(xform.GridUid)?.Station != chosenStation)
|
||||
continue;
|
||||
|
||||
// use the access reader from the door electronics if they exist
|
||||
if (!_access.GetMainAccessReader(airlockUid, out var accessComp))
|
||||
continue;
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
using Content.Server.Storage.EntitySystems;
|
||||
using Content.Shared.EntityTable.EntitySelectors;
|
||||
|
||||
namespace Content.Server.Storage.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Spawns items from an entity table when used in hand.
|
||||
/// </summary>
|
||||
[RegisterComponent, Access(typeof(SpawnTableOnUseSystem))]
|
||||
public sealed partial class SpawnTableOnUseComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The entity table to select entities from.
|
||||
/// </summary>
|
||||
[DataField(required: true)]
|
||||
public EntityTableSelector Table = default!;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using Content.Server.Administration.Logs;
|
||||
using Content.Server.Storage.Components;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.EntityTable;
|
||||
using Content.Shared.Hands.EntitySystems;
|
||||
using Content.Shared.Interaction.Events;
|
||||
|
||||
namespace Content.Server.Storage.EntitySystems;
|
||||
|
||||
public sealed class SpawnTableOnUseSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly EntityTableSystem _entityTable = default!;
|
||||
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
|
||||
[Dependency] private readonly SharedHandsSystem _hands = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<SpawnTableOnUseComponent, UseInHandEvent>(OnUseInHand);
|
||||
}
|
||||
|
||||
private void OnUseInHand(Entity<SpawnTableOnUseComponent> ent, ref UseInHandEvent args)
|
||||
{
|
||||
if (args.Handled)
|
||||
return;
|
||||
|
||||
args.Handled = true;
|
||||
|
||||
var coords = Transform(ent).Coordinates;
|
||||
var spawns = _entityTable.GetSpawns(ent.Comp.Table);
|
||||
foreach (var id in spawns)
|
||||
{
|
||||
var spawned = Spawn(id, coords);
|
||||
_adminLogger.Add(LogType.EntitySpawn, LogImpact.Low, $"{ToPrettyString(args.User):user} used {ToPrettyString(ent):spawner} which spawned {ToPrettyString(spawned)}");
|
||||
_hands.TryPickupAnyHand(args.User, spawned);
|
||||
}
|
||||
|
||||
Del(ent);
|
||||
}
|
||||
}
|
||||
468
Content.Server/Telephone/TelephoneSystem.cs
Normal file
468
Content.Server/Telephone/TelephoneSystem.cs
Normal file
@@ -0,0 +1,468 @@
|
||||
using Content.Server.Access.Systems;
|
||||
using Content.Server.Administration.Logs;
|
||||
using Content.Server.Chat.Systems;
|
||||
using Content.Server.Interaction;
|
||||
using Content.Server.Power.EntitySystems;
|
||||
using Content.Server.Speech;
|
||||
using Content.Server.Speech.Components;
|
||||
using Content.Shared.Chat;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Mind.Components;
|
||||
using Content.Shared.Power;
|
||||
using Content.Shared.Speech;
|
||||
using Content.Shared.Telephone;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Replays;
|
||||
using System.Linq;
|
||||
using Content.Shared.Silicons.StationAi;
|
||||
using Content.Shared.Silicons.Borgs.Components;
|
||||
|
||||
namespace Content.Server.Telephone;
|
||||
|
||||
public sealed class TelephoneSystem : SharedTelephoneSystem
|
||||
{
|
||||
[Dependency] private readonly AppearanceSystem _appearanceSystem = default!;
|
||||
[Dependency] private readonly InteractionSystem _interaction = default!;
|
||||
[Dependency] private readonly IdCardSystem _idCardSystem = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
[Dependency] private readonly ChatSystem _chat = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototype = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
|
||||
[Dependency] private readonly IReplayRecordingManager _replay = default!;
|
||||
|
||||
// Has set used to prevent telephone feedback loops
|
||||
private HashSet<(EntityUid, string, Entity<TelephoneComponent>)> _recentChatMessages = new();
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<TelephoneComponent, ComponentShutdown>(OnComponentShutdown);
|
||||
SubscribeLocalEvent<TelephoneComponent, PowerChangedEvent>(OnPowerChanged);
|
||||
SubscribeLocalEvent<TelephoneComponent, ListenAttemptEvent>(OnAttemptListen);
|
||||
SubscribeLocalEvent<TelephoneComponent, ListenEvent>(OnListen);
|
||||
SubscribeLocalEvent<TelephoneComponent, TelephoneMessageReceivedEvent>(OnTelephoneMessageReceived);
|
||||
}
|
||||
|
||||
#region: Events
|
||||
|
||||
private void OnComponentShutdown(Entity<TelephoneComponent> entity, ref ComponentShutdown ev)
|
||||
{
|
||||
TerminateTelephoneCalls(entity);
|
||||
}
|
||||
|
||||
private void OnPowerChanged(Entity<TelephoneComponent> entity, ref PowerChangedEvent ev)
|
||||
{
|
||||
if (!ev.Powered)
|
||||
TerminateTelephoneCalls(entity);
|
||||
}
|
||||
|
||||
private void OnAttemptListen(Entity<TelephoneComponent> entity, ref ListenAttemptEvent args)
|
||||
{
|
||||
if (!IsTelephonePowered(entity) ||
|
||||
!IsTelephoneEngaged(entity) ||
|
||||
entity.Comp.Muted ||
|
||||
!_interaction.InRangeUnobstructed(args.Source, entity.Owner, 0))
|
||||
{
|
||||
args.Cancel();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnListen(Entity<TelephoneComponent> entity, ref ListenEvent args)
|
||||
{
|
||||
if (args.Source == entity.Owner)
|
||||
return;
|
||||
|
||||
// Ignore background chatter from non-player entities
|
||||
if (!HasComp<MindContainerComponent>(args.Source))
|
||||
return;
|
||||
|
||||
// Simple check to make sure that we haven't sent this message already this frame
|
||||
if (!_recentChatMessages.Add((args.Source, args.Message, entity)))
|
||||
return;
|
||||
|
||||
SendTelephoneMessage(args.Source, args.Message, entity);
|
||||
}
|
||||
|
||||
private void OnTelephoneMessageReceived(Entity<TelephoneComponent> entity, ref TelephoneMessageReceivedEvent args)
|
||||
{
|
||||
// Prevent message feedback loops
|
||||
if (entity == args.TelephoneSource)
|
||||
return;
|
||||
|
||||
if (!IsTelephonePowered(entity) ||
|
||||
!IsSourceConnectedToReceiver(args.TelephoneSource, entity))
|
||||
return;
|
||||
|
||||
var nameEv = new TransformSpeakerNameEvent(args.MessageSource, Name(args.MessageSource));
|
||||
RaiseLocalEvent(args.MessageSource, nameEv);
|
||||
|
||||
var name = Loc.GetString("speech-name-relay",
|
||||
("speaker", Name(entity)),
|
||||
("originalName", nameEv.VoiceName));
|
||||
|
||||
var volume = entity.Comp.SpeakerVolume == TelephoneVolume.Speak ? InGameICChatType.Speak : InGameICChatType.Whisper;
|
||||
_chat.TrySendInGameICMessage(entity, args.Message, volume, ChatTransmitRange.GhostRangeLimit, nameOverride: name, checkRadioPrefix: false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
|
||||
var query = EntityManager.EntityQueryEnumerator<TelephoneComponent>();
|
||||
while (query.MoveNext(out var uid, out var telephone))
|
||||
{
|
||||
var entity = new Entity<TelephoneComponent>(uid, telephone);
|
||||
|
||||
if (IsTelephoneEngaged(entity))
|
||||
{
|
||||
foreach (var receiver in telephone.LinkedTelephones)
|
||||
{
|
||||
if (!IsSourceInRangeOfReceiver(entity, receiver) &&
|
||||
!IsSourceInRangeOfReceiver(receiver, entity))
|
||||
{
|
||||
EndTelephoneCall(entity, receiver);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch (telephone.CurrentState)
|
||||
{
|
||||
// Try to play ring tone if ringing
|
||||
case TelephoneState.Ringing:
|
||||
if (_timing.CurTime > telephone.StateStartTime + TimeSpan.FromSeconds(telephone.RingingTimeout))
|
||||
EndTelephoneCalls(entity);
|
||||
|
||||
else if (telephone.RingTone != null &&
|
||||
_timing.CurTime > telephone.NextRingToneTime)
|
||||
{
|
||||
_audio.PlayPvs(telephone.RingTone, uid);
|
||||
telephone.NextRingToneTime = _timing.CurTime + TimeSpan.FromSeconds(telephone.RingInterval);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
// Try to hang up if their has been no recent in-call activity
|
||||
case TelephoneState.InCall:
|
||||
if (_timing.CurTime > telephone.StateStartTime + TimeSpan.FromSeconds(telephone.IdlingTimeout))
|
||||
EndTelephoneCalls(entity);
|
||||
|
||||
break;
|
||||
|
||||
// Try to terminate if the telephone has finished hanging up
|
||||
case TelephoneState.EndingCall:
|
||||
if (_timing.CurTime > telephone.StateStartTime + TimeSpan.FromSeconds(telephone.HangingUpTimeout))
|
||||
TerminateTelephoneCalls(entity);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_recentChatMessages.Clear();
|
||||
}
|
||||
|
||||
public void BroadcastCallToTelephones(Entity<TelephoneComponent> source, HashSet<Entity<TelephoneComponent>> receivers, EntityUid user, TelephoneCallOptions? options = null)
|
||||
{
|
||||
if (IsTelephoneEngaged(source))
|
||||
return;
|
||||
|
||||
foreach (var receiver in receivers)
|
||||
TryCallTelephone(source, receiver, user, options);
|
||||
|
||||
// If no connections could be made, hang up the telephone
|
||||
if (!IsTelephoneEngaged(source))
|
||||
EndTelephoneCalls(source);
|
||||
}
|
||||
|
||||
public void CallTelephone(Entity<TelephoneComponent> source, Entity<TelephoneComponent> receiver, EntityUid user, TelephoneCallOptions? options = null)
|
||||
{
|
||||
if (IsTelephoneEngaged(source))
|
||||
return;
|
||||
|
||||
if (!TryCallTelephone(source, receiver, user, options))
|
||||
EndTelephoneCalls(source);
|
||||
}
|
||||
|
||||
private bool TryCallTelephone(Entity<TelephoneComponent> source, Entity<TelephoneComponent> receiver, EntityUid user, TelephoneCallOptions? options = null)
|
||||
{
|
||||
if (!IsSourceAbleToReachReceiver(source, receiver))
|
||||
return false;
|
||||
|
||||
if (IsTelephoneEngaged(receiver) &&
|
||||
options?.ForceConnect != true &&
|
||||
options?.ForceJoin != true)
|
||||
return false;
|
||||
|
||||
var evCallAttempt = new TelephoneCallAttemptEvent(source, receiver, user);
|
||||
RaiseLocalEvent(source, ref evCallAttempt);
|
||||
|
||||
if (evCallAttempt.Cancelled)
|
||||
return false;
|
||||
|
||||
if (options?.ForceConnect == true)
|
||||
TerminateTelephoneCalls(receiver);
|
||||
|
||||
source.Comp.LinkedTelephones.Add(receiver);
|
||||
source.Comp.Muted = options?.MuteSource == true;
|
||||
|
||||
receiver.Comp.LastCallerId = GetNameAndJobOfCallingEntity(user); // This will be networked when the state changes
|
||||
receiver.Comp.LinkedTelephones.Add(source);
|
||||
receiver.Comp.Muted = options?.MuteReceiver == true;
|
||||
|
||||
// Try to open a line of communication immediately
|
||||
if (options?.ForceConnect == true ||
|
||||
(options?.ForceJoin == true && receiver.Comp.CurrentState == TelephoneState.InCall))
|
||||
{
|
||||
CommenceTelephoneCall(source, receiver);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Otherwise start ringing the receiver
|
||||
SetTelephoneState(source, TelephoneState.Calling);
|
||||
SetTelephoneState(receiver, TelephoneState.Ringing);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void AnswerTelephone(Entity<TelephoneComponent> receiver, EntityUid user)
|
||||
{
|
||||
if (receiver.Comp.CurrentState != TelephoneState.Ringing)
|
||||
return;
|
||||
|
||||
// If the telephone isn't linked, or is linked to more than one telephone,
|
||||
// you shouldn't need to answer the call. If you do need to answer it,
|
||||
// you'll need to be handled this a different way
|
||||
if (receiver.Comp.LinkedTelephones.Count != 1)
|
||||
return;
|
||||
|
||||
var source = receiver.Comp.LinkedTelephones.First();
|
||||
CommenceTelephoneCall(source, receiver);
|
||||
}
|
||||
|
||||
private void CommenceTelephoneCall(Entity<TelephoneComponent> source, Entity<TelephoneComponent> receiver)
|
||||
{
|
||||
SetTelephoneState(source, TelephoneState.InCall);
|
||||
SetTelephoneState(receiver, TelephoneState.InCall);
|
||||
|
||||
SetTelephoneMicrophoneState(source, true);
|
||||
SetTelephoneMicrophoneState(receiver, true);
|
||||
|
||||
var evSource = new TelephoneCallCommencedEvent(receiver);
|
||||
var evReceiver = new TelephoneCallCommencedEvent(source);
|
||||
|
||||
RaiseLocalEvent(source, ref evSource);
|
||||
RaiseLocalEvent(receiver, ref evReceiver);
|
||||
}
|
||||
|
||||
public void EndTelephoneCall(Entity<TelephoneComponent> source, Entity<TelephoneComponent> receiver)
|
||||
{
|
||||
source.Comp.LinkedTelephones.Remove(receiver);
|
||||
receiver.Comp.LinkedTelephones.Remove(source);
|
||||
|
||||
if (!IsTelephoneEngaged(source))
|
||||
EndTelephoneCalls(source);
|
||||
|
||||
if (!IsTelephoneEngaged(receiver))
|
||||
EndTelephoneCalls(receiver);
|
||||
}
|
||||
|
||||
public void EndTelephoneCalls(Entity<TelephoneComponent> entity)
|
||||
{
|
||||
HandleEndingTelephoneCalls(entity, TelephoneState.EndingCall);
|
||||
|
||||
var ev = new TelephoneCallEndedEvent();
|
||||
RaiseLocalEvent(entity, ref ev);
|
||||
}
|
||||
|
||||
public void TerminateTelephoneCalls(Entity<TelephoneComponent> entity)
|
||||
{
|
||||
HandleEndingTelephoneCalls(entity, TelephoneState.Idle);
|
||||
}
|
||||
|
||||
private void HandleEndingTelephoneCalls(Entity<TelephoneComponent> entity, TelephoneState newState)
|
||||
{
|
||||
if (entity.Comp.CurrentState == newState)
|
||||
return;
|
||||
|
||||
foreach (var linkedTelephone in entity.Comp.LinkedTelephones)
|
||||
{
|
||||
if (!linkedTelephone.Comp.LinkedTelephones.Remove(entity))
|
||||
continue;
|
||||
|
||||
if (!IsTelephoneEngaged(linkedTelephone))
|
||||
EndTelephoneCalls(linkedTelephone);
|
||||
}
|
||||
|
||||
entity.Comp.LinkedTelephones.Clear();
|
||||
entity.Comp.Muted = false;
|
||||
|
||||
SetTelephoneState(entity, newState);
|
||||
SetTelephoneMicrophoneState(entity, false);
|
||||
}
|
||||
|
||||
private void SendTelephoneMessage(EntityUid messageSource, string message, Entity<TelephoneComponent> source, bool escapeMarkup = true)
|
||||
{
|
||||
// This method assumes that you've already checked that this
|
||||
// telephone is able to transmit messages and that it can
|
||||
// send messages to any telephones linked to it
|
||||
|
||||
var ev = new TransformSpeakerNameEvent(messageSource, MetaData(messageSource).EntityName);
|
||||
RaiseLocalEvent(messageSource, ev);
|
||||
|
||||
var name = ev.VoiceName;
|
||||
name = FormattedMessage.EscapeText(name);
|
||||
|
||||
SpeechVerbPrototype speech;
|
||||
if (ev.SpeechVerb != null && _prototype.TryIndex(ev.SpeechVerb, out var evntProto))
|
||||
speech = evntProto;
|
||||
else
|
||||
speech = _chat.GetSpeechVerb(messageSource, message);
|
||||
|
||||
var content = escapeMarkup
|
||||
? FormattedMessage.EscapeText(message)
|
||||
: message;
|
||||
|
||||
var wrappedMessage = Loc.GetString(speech.Bold ? "chat-telephone-message-wrap-bold" : "chat-telephone-message-wrap",
|
||||
("color", Color.White),
|
||||
("fontType", speech.FontId),
|
||||
("fontSize", speech.FontSize),
|
||||
("verb", Loc.GetString(_random.Pick(speech.SpeechVerbStrings))),
|
||||
("name", name),
|
||||
("message", content));
|
||||
|
||||
var chat = new ChatMessage(
|
||||
ChatChannel.Local,
|
||||
message,
|
||||
wrappedMessage,
|
||||
NetEntity.Invalid,
|
||||
null);
|
||||
|
||||
var chatMsg = new MsgChatMessage { Message = chat };
|
||||
|
||||
var evSentMessage = new TelephoneMessageSentEvent(message, chatMsg, messageSource);
|
||||
RaiseLocalEvent(source, ref evSentMessage);
|
||||
source.Comp.StateStartTime = _timing.CurTime;
|
||||
|
||||
var evReceivedMessage = new TelephoneMessageReceivedEvent(message, chatMsg, messageSource, source);
|
||||
|
||||
foreach (var receiver in source.Comp.LinkedTelephones)
|
||||
{
|
||||
RaiseLocalEvent(receiver, ref evReceivedMessage);
|
||||
receiver.Comp.StateStartTime = _timing.CurTime;
|
||||
}
|
||||
|
||||
if (name != Name(messageSource))
|
||||
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Telephone message from {ToPrettyString(messageSource):user} as {name} on {source}: {message}");
|
||||
else
|
||||
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Telephone message from {ToPrettyString(messageSource):user} on {source}: {message}");
|
||||
|
||||
_replay.RecordServerMessage(chat);
|
||||
}
|
||||
|
||||
private void SetTelephoneState(Entity<TelephoneComponent> entity, TelephoneState newState)
|
||||
{
|
||||
var oldState = entity.Comp.CurrentState;
|
||||
|
||||
entity.Comp.CurrentState = newState;
|
||||
entity.Comp.StateStartTime = _timing.CurTime;
|
||||
Dirty(entity);
|
||||
|
||||
_appearanceSystem.SetData(entity, TelephoneVisuals.Key, entity.Comp.CurrentState);
|
||||
|
||||
var ev = new TelephoneStateChangeEvent(oldState, newState);
|
||||
RaiseLocalEvent(entity, ref ev);
|
||||
}
|
||||
|
||||
private void SetTelephoneMicrophoneState(Entity<TelephoneComponent> entity, bool microphoneOn)
|
||||
{
|
||||
if (microphoneOn && !HasComp<ActiveListenerComponent>(entity))
|
||||
{
|
||||
var activeListener = AddComp<ActiveListenerComponent>(entity);
|
||||
activeListener.Range = entity.Comp.ListeningRange;
|
||||
}
|
||||
|
||||
if (!microphoneOn && HasComp<ActiveListenerComponent>(entity))
|
||||
{
|
||||
RemComp<ActiveListenerComponent>(entity);
|
||||
}
|
||||
}
|
||||
|
||||
private (string?, string?) GetNameAndJobOfCallingEntity(EntityUid uid)
|
||||
{
|
||||
string? presumedName = null;
|
||||
string? presumedJob = null;
|
||||
|
||||
if (HasComp<StationAiHeldComponent>(uid) || HasComp<BorgChassisComponent>(uid))
|
||||
{
|
||||
presumedName = Name(uid);
|
||||
return (presumedName, presumedJob);
|
||||
}
|
||||
|
||||
if (_idCardSystem.TryFindIdCard(uid, out var idCard))
|
||||
{
|
||||
presumedName = string.IsNullOrWhiteSpace(idCard.Comp.FullName) ? null : idCard.Comp.FullName;
|
||||
presumedJob = idCard.Comp.LocalizedJobTitle;
|
||||
}
|
||||
|
||||
return (presumedName, presumedJob);
|
||||
}
|
||||
|
||||
public bool IsSourceAbleToReachReceiver(Entity<TelephoneComponent> source, Entity<TelephoneComponent> receiver)
|
||||
{
|
||||
if (source == receiver ||
|
||||
!IsTelephonePowered(source) ||
|
||||
!IsTelephonePowered(receiver) ||
|
||||
!IsSourceInRangeOfReceiver(source, receiver))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool IsSourceInRangeOfReceiver(Entity<TelephoneComponent> source, Entity<TelephoneComponent> receiver)
|
||||
{
|
||||
var sourceXform = Transform(source);
|
||||
var receiverXform = Transform(receiver);
|
||||
|
||||
switch (source.Comp.TransmissionRange)
|
||||
{
|
||||
case TelephoneRange.Grid:
|
||||
return sourceXform.GridUid != null &&
|
||||
receiverXform.GridUid == sourceXform.GridUid &&
|
||||
receiver.Comp.TransmissionRange != TelephoneRange.Long;
|
||||
|
||||
case TelephoneRange.Map:
|
||||
return sourceXform.MapID == receiverXform.MapID &&
|
||||
receiver.Comp.TransmissionRange != TelephoneRange.Long;
|
||||
|
||||
case TelephoneRange.Long:
|
||||
return sourceXform.MapID != receiverXform.MapID &&
|
||||
receiver.Comp.TransmissionRange == TelephoneRange.Long;
|
||||
|
||||
case TelephoneRange.Unlimited:
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool IsSourceConnectedToReceiver(Entity<TelephoneComponent> source, Entity<TelephoneComponent> receiver)
|
||||
{
|
||||
return source.Comp.LinkedTelephones.Contains(receiver);
|
||||
}
|
||||
|
||||
public bool IsTelephonePowered(Entity<TelephoneComponent> entity)
|
||||
{
|
||||
return this.IsPowered(entity, EntityManager) || !entity.Comp.RequiresPower;
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ using Content.Server.Chat;
|
||||
using Content.Server.Chat.Systems;
|
||||
using Content.Server.Emoting.Systems;
|
||||
using Content.Server.Speech.EntitySystems;
|
||||
using Content.Shared.Anomaly.Components;
|
||||
using Content.Shared.Bed.Sleep;
|
||||
using Content.Shared.Cloning;
|
||||
using Content.Shared.Damage;
|
||||
@@ -64,10 +65,19 @@ namespace Content.Server.Zombies
|
||||
SubscribeLocalEvent<ZombieComponent, GetCharactedDeadIcEvent>(OnGetCharacterDeadIC);
|
||||
|
||||
SubscribeLocalEvent<PendingZombieComponent, MapInitEvent>(OnPendingMapInit);
|
||||
SubscribeLocalEvent<PendingZombieComponent, BeforeRemoveAnomalyOnDeathEvent>(OnBeforeRemoveAnomalyOnDeath);
|
||||
|
||||
SubscribeLocalEvent<IncurableZombieComponent, MapInitEvent>(OnPendingMapInit);
|
||||
|
||||
SubscribeLocalEvent<ZombifyOnDeathComponent, MobStateChangedEvent>(OnDamageChanged);
|
||||
|
||||
}
|
||||
|
||||
private void OnBeforeRemoveAnomalyOnDeath(Entity<PendingZombieComponent> ent, ref BeforeRemoveAnomalyOnDeathEvent args)
|
||||
{
|
||||
// Pending zombies (e.g. infected non-zombies) do not remove their hosted anomaly on death.
|
||||
// Current zombies DO remove the anomaly on death.
|
||||
args.Cancelled = true;
|
||||
}
|
||||
|
||||
private void OnPendingMapInit(EntityUid uid, IncurableZombieComponent component, MapInitEvent args)
|
||||
|
||||
57
Content.Shared/Animals/UdderComponent.cs
Normal file
57
Content.Shared/Animals/UdderComponent.cs
Normal file
@@ -0,0 +1,57 @@
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.Chemistry.Reagent;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.Animals;
|
||||
|
||||
/// <summary>
|
||||
/// Gives the ability to produce a solution;
|
||||
/// produces endlessly if the owner does not have a HungerComponent.
|
||||
/// </summary>
|
||||
[RegisterComponent, AutoGenerateComponentState, AutoGenerateComponentPause, NetworkedComponent]
|
||||
public sealed partial class UdderComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The reagent to produce.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public ProtoId<ReagentPrototype> ReagentId = new();
|
||||
|
||||
/// <summary>
|
||||
/// The name of <see cref="Solution"/>.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public string SolutionName = "udder";
|
||||
|
||||
/// <summary>
|
||||
/// The solution to add reagent to.
|
||||
/// </summary>
|
||||
[DataField, ViewVariables(VVAccess.ReadOnly)]
|
||||
public Entity<SolutionComponent>? Solution = null;
|
||||
|
||||
/// <summary>
|
||||
/// The amount of reagent to be generated on update.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public FixedPoint2 QuantityPerUpdate = 25;
|
||||
|
||||
/// <summary>
|
||||
/// The amount of nutrient consumed on update.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public float HungerUsage = 10f;
|
||||
|
||||
/// <summary>
|
||||
/// How long to wait before producing.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public TimeSpan GrowthDelay = TimeSpan.FromMinutes(1);
|
||||
|
||||
/// <summary>
|
||||
/// When to next try to produce.
|
||||
/// </summary>
|
||||
[DataField, AutoPausedField, Access(typeof(UdderSystem))]
|
||||
public TimeSpan NextGrowth = TimeSpan.Zero;
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
using Content.Server.Animals.Components;
|
||||
using Content.Server.Popups;
|
||||
using Content.Shared.Chemistry.EntitySystems;
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.Chemistry.EntitySystems;
|
||||
using Content.Shared.DoAfter;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.IdentityManagement;
|
||||
using Content.Shared.Mobs.Systems;
|
||||
using Content.Shared.Nutrition.Components;
|
||||
@@ -12,18 +11,17 @@ using Content.Shared.Udder;
|
||||
using Content.Shared.Verbs;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Server.Animals.Systems;
|
||||
|
||||
namespace Content.Shared.Animals;
|
||||
/// <summary>
|
||||
/// Gives ability to produce milkable reagents, produces endless if the
|
||||
/// owner has no HungerComponent
|
||||
/// Gives the ability to produce milkable reagents;
|
||||
/// produces endlessly if the owner does not have a HungerComponent.
|
||||
/// </summary>
|
||||
internal sealed class UdderSystem : EntitySystem
|
||||
public sealed class UdderSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly HungerSystem _hunger = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly MobStateSystem _mobState = default!;
|
||||
[Dependency] private readonly PopupSystem _popupSystem = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
|
||||
[Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
|
||||
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
|
||||
|
||||
@@ -31,26 +29,37 @@ internal sealed class UdderSystem : EntitySystem
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<UdderComponent, MapInitEvent>(OnMapInit);
|
||||
SubscribeLocalEvent<UdderComponent, GetVerbsEvent<AlternativeVerb>>(AddMilkVerb);
|
||||
SubscribeLocalEvent<UdderComponent, MilkingDoAfterEvent>(OnDoAfter);
|
||||
SubscribeLocalEvent<UdderComponent, ExaminedEvent>(OnExamine);
|
||||
}
|
||||
|
||||
private void OnMapInit(EntityUid uid, UdderComponent component, MapInitEvent args)
|
||||
{
|
||||
component.NextGrowth = _timing.CurTime + component.GrowthDelay;
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
|
||||
var query = EntityQueryEnumerator<UdderComponent>();
|
||||
var now = _timing.CurTime;
|
||||
while (query.MoveNext(out var uid, out var udder))
|
||||
{
|
||||
if (now < udder.NextGrowth)
|
||||
if (_timing.CurTime < udder.NextGrowth)
|
||||
continue;
|
||||
|
||||
udder.NextGrowth = now + udder.GrowthDelay;
|
||||
udder.NextGrowth += udder.GrowthDelay;
|
||||
|
||||
if (_mobState.IsDead(uid))
|
||||
continue;
|
||||
|
||||
if (!_solutionContainerSystem.ResolveSolution(uid, udder.SolutionName, ref udder.Solution, out var solution))
|
||||
continue;
|
||||
|
||||
if (solution.AvailableVolume == 0)
|
||||
continue;
|
||||
|
||||
// Actually there is food digestion so no problem with instant reagent generation "OnFeed"
|
||||
if (EntityManager.TryGetComponent(uid, out HungerComponent? hunger))
|
||||
{
|
||||
@@ -61,9 +70,6 @@ internal sealed class UdderSystem : EntitySystem
|
||||
_hunger.ModifyHunger(uid, -udder.HungerUsage, hunger);
|
||||
}
|
||||
|
||||
if (!_solutionContainerSystem.ResolveSolution(uid, udder.SolutionName, ref udder.Solution))
|
||||
continue;
|
||||
|
||||
//TODO: toxins from bloodstream !?
|
||||
_solutionContainerSystem.TryAddReagent(udder.Solution.Value, udder.ReagentId, udder.QuantityPerUpdate, out _);
|
||||
}
|
||||
@@ -99,7 +105,7 @@ internal sealed class UdderSystem : EntitySystem
|
||||
var quantity = solution.Volume;
|
||||
if (quantity == 0)
|
||||
{
|
||||
_popupSystem.PopupEntity(Loc.GetString("udder-system-dry"), entity.Owner, args.Args.User);
|
||||
_popupSystem.PopupClient(Loc.GetString("udder-system-dry"), entity.Owner, args.Args.User);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -109,7 +115,7 @@ internal sealed class UdderSystem : EntitySystem
|
||||
var split = _solutionContainerSystem.SplitSolution(entity.Comp.Solution.Value, quantity);
|
||||
_solutionContainerSystem.TryAddSolution(targetSoln.Value, split);
|
||||
|
||||
_popupSystem.PopupEntity(Loc.GetString("udder-system-success", ("amount", quantity), ("target", Identity.Entity(args.Args.Used.Value, EntityManager))), entity.Owner,
|
||||
_popupSystem.PopupClient(Loc.GetString("udder-system-success", ("amount", quantity), ("target", Identity.Entity(args.Args.Used.Value, EntityManager))), entity.Owner,
|
||||
args.Args.User, PopupType.Medium);
|
||||
}
|
||||
|
||||
@@ -134,4 +140,50 @@ internal sealed class UdderSystem : EntitySystem
|
||||
};
|
||||
args.Verbs.Add(verb);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defines the text provided on examine.
|
||||
/// Changes depending on the amount of hunger the target has.
|
||||
/// </summary>
|
||||
private void OnExamine(Entity<UdderComponent> entity, ref ExaminedEvent args)
|
||||
{
|
||||
|
||||
var entityIdentity = Identity.Entity(args.Examined, EntityManager);
|
||||
|
||||
string message;
|
||||
|
||||
// Check if the target has hunger, otherwise return not hungry.
|
||||
if (!TryComp<HungerComponent>(entity, out var hunger))
|
||||
{
|
||||
message = Loc.GetString("udder-system-examine-none", ("entity", entityIdentity));
|
||||
args.PushMarkup(message);
|
||||
return;
|
||||
}
|
||||
|
||||
// Choose the correct examine string based on HungerThreshold.
|
||||
switch (_hunger.GetHungerThreshold(hunger))
|
||||
{
|
||||
case >= HungerThreshold.Overfed:
|
||||
message = Loc.GetString("udder-system-examine-overfed", ("entity", entityIdentity));
|
||||
break;
|
||||
|
||||
case HungerThreshold.Okay:
|
||||
message = Loc.GetString("udder-system-examine-okay", ("entity", entityIdentity));
|
||||
break;
|
||||
|
||||
case HungerThreshold.Peckish:
|
||||
message = Loc.GetString("udder-system-examine-hungry", ("entity", entityIdentity));
|
||||
break;
|
||||
|
||||
// There's a final hunger threshold called "dead" but animals don't actually die so we'll re-use this.
|
||||
case <= HungerThreshold.Starving:
|
||||
message = Loc.GetString("udder-system-examine-starved", ("entity", entityIdentity));
|
||||
break;
|
||||
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
args.PushMarkup(message);
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,22 @@
|
||||
using Content.Server.Animals.Systems;
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.Chemistry.Reagent;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
|
||||
|
||||
namespace Content.Server.Animals.Components;
|
||||
namespace Content.Shared.Animals;
|
||||
|
||||
/// <summary>
|
||||
/// Lets an entity produce wool fibers. Uses hunger if present.
|
||||
/// Gives the ability to produce wool fibers;
|
||||
/// produces endlessly if the owner does not have a HungerComponent.
|
||||
/// </summary>
|
||||
|
||||
[RegisterComponent, Access(typeof(WoolySystem))]
|
||||
[RegisterComponent, AutoGenerateComponentState, AutoGenerateComponentPause, NetworkedComponent]
|
||||
public sealed partial class WoolyComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The reagent to grow.
|
||||
/// </summary>
|
||||
[DataField, ViewVariables(VVAccess.ReadOnly)]
|
||||
[DataField, AutoNetworkedField]
|
||||
public ProtoId<ReagentPrototype> ReagentId = "Fiber";
|
||||
|
||||
/// <summary>
|
||||
@@ -29,30 +28,30 @@ public sealed partial class WoolyComponent : Component
|
||||
/// <summary>
|
||||
/// The solution to add reagent to.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
[DataField, ViewVariables(VVAccess.ReadOnly)]
|
||||
public Entity<SolutionComponent>? Solution;
|
||||
|
||||
/// <summary>
|
||||
/// The amount of reagent to be generated on update.
|
||||
/// </summary>
|
||||
[DataField, ViewVariables(VVAccess.ReadOnly)]
|
||||
[DataField, AutoNetworkedField]
|
||||
public FixedPoint2 Quantity = 25;
|
||||
|
||||
/// <summary>
|
||||
/// The amount of nutrient consumed on update.
|
||||
/// </summary>
|
||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField, AutoNetworkedField]
|
||||
public float HungerUsage = 10f;
|
||||
|
||||
/// <summary>
|
||||
/// How long to wait before growing wool.
|
||||
/// </summary>
|
||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField, AutoNetworkedField]
|
||||
public TimeSpan GrowthDelay = TimeSpan.FromMinutes(1);
|
||||
|
||||
/// <summary>
|
||||
/// When to next try growing wool.
|
||||
/// </summary>
|
||||
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite)]
|
||||
public TimeSpan NextGrowth = TimeSpan.FromSeconds(0);
|
||||
[DataField, AutoPausedField, Access(typeof(WoolySystem))]
|
||||
public TimeSpan NextGrowth = TimeSpan.Zero;
|
||||
}
|
||||
@@ -1,16 +1,15 @@
|
||||
using Content.Server.Animals.Components;
|
||||
using Content.Server.Nutrition;
|
||||
using Content.Shared.Chemistry.EntitySystems;
|
||||
using Content.Shared.Mobs.Systems;
|
||||
using Content.Shared.Nutrition;
|
||||
using Content.Shared.Nutrition.Components;
|
||||
using Content.Shared.Nutrition.EntitySystems;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Server.Animals.Systems;
|
||||
namespace Content.Shared.Animals;
|
||||
|
||||
/// <summary>
|
||||
/// Gives ability to produce fiber reagents, produces endless if the
|
||||
/// owner has no HungerComponent
|
||||
/// Gives ability to produce fiber reagents;
|
||||
/// produces endlessly if the owner has no HungerComponent.
|
||||
/// </summary>
|
||||
public sealed class WoolySystem : EntitySystem
|
||||
{
|
||||
@@ -24,6 +23,12 @@ public sealed class WoolySystem : EntitySystem
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<WoolyComponent, BeforeFullyEatenEvent>(OnBeforeFullyEaten);
|
||||
SubscribeLocalEvent<WoolyComponent, MapInitEvent>(OnMapInit);
|
||||
}
|
||||
|
||||
private void OnMapInit(EntityUid uid, WoolyComponent component, MapInitEvent args)
|
||||
{
|
||||
component.NextGrowth = _timing.CurTime + component.GrowthDelay;
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
@@ -31,17 +36,22 @@ public sealed class WoolySystem : EntitySystem
|
||||
base.Update(frameTime);
|
||||
|
||||
var query = EntityQueryEnumerator<WoolyComponent>();
|
||||
var now = _timing.CurTime;
|
||||
while (query.MoveNext(out var uid, out var wooly))
|
||||
{
|
||||
if (now < wooly.NextGrowth)
|
||||
if (_timing.CurTime < wooly.NextGrowth)
|
||||
continue;
|
||||
|
||||
wooly.NextGrowth = now + wooly.GrowthDelay;
|
||||
wooly.NextGrowth += wooly.GrowthDelay;
|
||||
|
||||
if (_mobState.IsDead(uid))
|
||||
continue;
|
||||
|
||||
if (!_solutionContainer.ResolveSolution(uid, wooly.SolutionName, ref wooly.Solution, out var solution))
|
||||
continue;
|
||||
|
||||
if (solution.AvailableVolume == 0)
|
||||
continue;
|
||||
|
||||
// Actually there is food digestion so no problem with instant reagent generation "OnFeed"
|
||||
if (EntityManager.TryGetComponent(uid, out HungerComponent? hunger))
|
||||
{
|
||||
@@ -52,9 +62,6 @@ public sealed class WoolySystem : EntitySystem
|
||||
_hunger.ModifyHunger(uid, -wooly.HungerUsage, hunger);
|
||||
}
|
||||
|
||||
if (!_solutionContainer.ResolveSolution(uid, wooly.SolutionName, ref wooly.Solution))
|
||||
continue;
|
||||
|
||||
_solutionContainer.TryAddReagent(wooly.Solution.Value, wooly.ReagentId, wooly.Quantity, out _);
|
||||
}
|
||||
}
|
||||
@@ -70,3 +70,11 @@ public sealed partial class InnerBodyAnomalyComponent : Component
|
||||
[DataField]
|
||||
public string LayerMap = "inner_anomaly_layer";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event broadcast when an anomaly is being removed because the host is dying.
|
||||
/// Raised directed at the host entity with the anomaly.
|
||||
/// Cancel this if you want to prevent the host from losing their anomaly on death.
|
||||
/// </summary>
|
||||
[ByRefEvent]
|
||||
public record struct BeforeRemoveAnomalyOnDeathEvent(bool Cancelled = false);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user