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 :(
|
# 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
|
# 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.
|
/Resources/ConfigPresets/WizardsDen/ @nikthechampiongr
|
||||||
# You need to add your name to these entries, not make a new one, if you care about them.
|
/Content.*/Administration/ @DrSmugleaf @nikthechampiongr
|
||||||
/Content.*/Toolshed/ @moonheart08
|
/Resources/ServerInfo/ @nikthechampiongr
|
||||||
**/Toolshed/** @moonheart08
|
/Resources/ServerInfo/Guidebook/ServerRules/ @nikthechampiongr
|
||||||
*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/Prototypes/Maps/** @Emisse
|
/Resources/Prototypes/Maps/** @Emisse
|
||||||
|
|
||||||
/Resources/Prototypes/Body/ @DrSmugleaf # suffering
|
/Resources/Prototypes/Body/ @DrSmugleaf # suffering
|
||||||
/Resources/Prototypes/Entities/Mobs/Player/ @DrSmugleaf
|
/Resources/Prototypes/Entities/Mobs/Player/ @DrSmugleaf
|
||||||
/Resources/Prototypes/Entities/Mobs/Species/ @DrSmugleaf
|
/Resources/Prototypes/Entities/Mobs/Species/ @DrSmugleaf
|
||||||
/Resources/Prototypes/Guidebook/rules.yml @Chief-Engineer
|
/Resources/Prototypes/Guidebook/rules.yml @nikthechampiongr
|
||||||
/Content.*/Body/ @DrSmugleaf
|
/Content.*/Body/ @DrSmugleaf
|
||||||
/Content.YAMLLinter @DrSmugleaf
|
/Content.YAMLLinter @DrSmugleaf
|
||||||
/Content.Shared/Damage/ @DrSmugleaf
|
/Content.Shared/Damage/ @DrSmugleaf
|
||||||
|
|
||||||
/Content.*/Anomaly/ @EmoGarbage404 @TheShuEd
|
/Content.*/Anomaly/ @TheShuEd
|
||||||
/Content.*/Lathe/ @EmoGarbage404
|
/Resources/Prototypes/Entities/Structures/Specific/anomalies.yml @TheShuEd
|
||||||
/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.*/Forensics/ @ficcialfaint
|
/Content.*/Forensics/ @ficcialfaint
|
||||||
|
|
||||||
# SKREEEE
|
# SKREEEE
|
||||||
/Content.*.Database/ @PJB3005 @DrSmugleaf
|
/Content.*.Database/ @PJB3005 @DrSmugleaf
|
||||||
/Content.Shared.Database/Log*.cs @PJB3005 @DrSmugleaf @Chief-Engineer
|
/Content.Shared.Database/Log*.cs @PJB3005 @DrSmugleaf @nikthechampiongr
|
||||||
/Pow3r/ @PJB3005
|
/Pow3r/ @PJB3005
|
||||||
/Content.Server/Power/Pow3r/ @PJB3005
|
/Content.Server/Power/Pow3r/ @PJB3005
|
||||||
|
|
||||||
@@ -52,7 +33,7 @@
|
|||||||
/Content.*/Atmos/ @Partmedia
|
/Content.*/Atmos/ @Partmedia
|
||||||
/Content.*/Botany/ @Partmedia
|
/Content.*/Botany/ @Partmedia
|
||||||
|
|
||||||
#Jezi
|
# Jezi
|
||||||
/Content.*/Medical @Jezithyr
|
/Content.*/Medical @Jezithyr
|
||||||
/Content.*/Body @Jezithyr
|
/Content.*/Body @Jezithyr
|
||||||
|
|
||||||
|
|||||||
4
.github/labeler.yml
vendored
4
.github/labeler.yml
vendored
@@ -16,6 +16,10 @@
|
|||||||
- changed-files:
|
- changed-files:
|
||||||
- any-glob-to-any-file: '**/*.swsl'
|
- any-glob-to-any-file: '**/*.swsl'
|
||||||
|
|
||||||
|
"Changes: Audio":
|
||||||
|
- changed-files:
|
||||||
|
- any-glob-to-any-file: '**/*.ogg'
|
||||||
|
|
||||||
"Changes: No C#":
|
"Changes: No C#":
|
||||||
- changed-files:
|
- 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.
|
# 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",
|
[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)
|
public AtmosAlarmEntryContainer(NetEntity uid, EntityCoordinates? coordinates)
|
||||||
{
|
{
|
||||||
RobustXamlLoader.Load(this);
|
RobustXamlLoader.Load(this);
|
||||||
@@ -162,12 +149,11 @@ public sealed partial class AtmosAlarmEntryContainer : BoxContainer
|
|||||||
foreach ((var gas, (var mol, var percent, var alert)) in keyValuePairs)
|
foreach ((var gas, (var mol, var percent, var alert)) in keyValuePairs)
|
||||||
{
|
{
|
||||||
FixedPoint2 gasPercent = percent * 100f;
|
FixedPoint2 gasPercent = percent * 100f;
|
||||||
|
var gasAbbreviation = Atmospherics.GasAbbreviations.GetValueOrDefault(gas, Loc.GetString("gas-unknown-abbreviation"));
|
||||||
var gasShorthand = _gasShorthands.GetValueOrDefault(gas, "X");
|
|
||||||
|
|
||||||
var gasLabel = new Label()
|
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,
|
FontOverride = normalFont,
|
||||||
FontColorOverride = GetAlarmStateColor(alert),
|
FontColorOverride = GetAlarmStateColor(alert),
|
||||||
HorizontalAlignment = HAlignment.Center,
|
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;
|
||||||
|
using Content.Shared.Atmos.Components;
|
||||||
using Content.Shared.Atmos.Piping.Binary.Components;
|
using Content.Shared.Atmos.Piping.Binary.Components;
|
||||||
|
using Content.Shared.IdentityManagement;
|
||||||
using Content.Shared.Localizations;
|
using Content.Shared.Localizations;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using Robust.Client.GameObjects;
|
|
||||||
using Robust.Client.UserInterface;
|
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>
|
[ViewVariables]
|
||||||
/// Initializes a <see cref="GasPressurePumpWindow"/> and updates it when new server messages are received.
|
private const float MaxPressure = Atmospherics.MaxOutputPressure;
|
||||||
/// </summary>
|
|
||||||
[UsedImplicitly]
|
[ViewVariables]
|
||||||
public sealed class GasPressurePumpBoundUserInterface : BoundUserInterface
|
private GasPressurePumpWindow? _window;
|
||||||
|
|
||||||
|
public GasPressurePumpBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
|
||||||
{
|
{
|
||||||
[ViewVariables]
|
}
|
||||||
private const float MaxPressure = Atmospherics.MaxOutputPressure;
|
|
||||||
|
|
||||||
[ViewVariables]
|
protected override void Open()
|
||||||
private GasPressurePumpWindow? _window;
|
{
|
||||||
|
base.Open();
|
||||||
|
|
||||||
public GasPressurePumpBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
|
_window = this.CreateWindow<GasPressurePumpWindow>();
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void Open()
|
_window.ToggleStatusButtonPressed += OnToggleStatusButtonPressed;
|
||||||
{
|
_window.PumpOutputPressureChanged += OnPumpOutputPressurePressed;
|
||||||
base.Open();
|
Update();
|
||||||
|
}
|
||||||
|
|
||||||
_window = this.CreateWindow<GasPressurePumpWindow>();
|
public void Update()
|
||||||
|
{
|
||||||
|
if (_window == null)
|
||||||
|
return;
|
||||||
|
|
||||||
_window.ToggleStatusButtonPressed += OnToggleStatusButtonPressed;
|
_window.Title = Identity.Name(Owner, EntMan);
|
||||||
_window.PumpOutputPressureChanged += OnPumpOutputPressurePressed;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnToggleStatusButtonPressed()
|
if (!EntMan.TryGetComponent(Owner, out GasPressurePumpComponent? pump))
|
||||||
{
|
return;
|
||||||
if (_window is null) return;
|
|
||||||
SendMessage(new GasPressurePumpToggleStatusMessage(_window.PumpStatus));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnPumpOutputPressurePressed(string value)
|
_window.SetPumpStatus(pump.Enabled);
|
||||||
{
|
_window.MaxPressure = pump.MaxTargetPressure;
|
||||||
var pressure = UserInputParser.TryFloat(value, out var parsed) ? parsed : 0f;
|
_window.SetOutputPressure(pump.TargetPressure);
|
||||||
if (pressure > MaxPressure) pressure = MaxPressure;
|
}
|
||||||
|
|
||||||
SendMessage(new GasPressurePumpChangeOutputPressureMessage(pressure));
|
private void OnToggleStatusButtonPressed()
|
||||||
}
|
{
|
||||||
|
if (_window is null) return;
|
||||||
|
SendPredictedMessage(new GasPressurePumpToggleStatusMessage(_window.PumpStatus));
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
private void OnPumpOutputPressurePressed(float value)
|
||||||
/// Update the UI state based on server-sent info
|
{
|
||||||
/// </summary>
|
SendPredictedMessage(new GasPressurePumpChangeOutputPressureMessage(value));
|
||||||
/// <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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,18 @@
|
|||||||
<DefaultWindow xmlns="https://spacestation14.io"
|
<controls:FancyWindow xmlns="https://spacestation14.io"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
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="Vertical" Margin="5 5 5 5" SeparationOverride="10">
|
||||||
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
|
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
|
||||||
<Label Text="{Loc comp-gas-pump-ui-pump-status}"/>
|
<Label Text="{Loc comp-gas-pump-ui-pump-status}" Margin="0 0 5 0"/>
|
||||||
<Control MinSize="5 0" />
|
|
||||||
<Button Name="ToggleStatusButton"/>
|
<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>
|
||||||
|
|
||||||
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
|
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
|
||||||
<Label Text="{Loc comp-gas-pump-ui-pump-output-pressure}"/>
|
<Label Text="{Loc comp-gas-pump-ui-pump-output-pressure}"/>
|
||||||
<Control MinSize="5 0" />
|
<FloatSpinBox HorizontalExpand="True" Name="PumpPressureOutputInput" MinSize="70 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"/>
|
|
||||||
</BoxContainer>
|
</BoxContainer>
|
||||||
</BoxContainer>
|
</BoxContainer>
|
||||||
</DefaultWindow>
|
</controls:FancyWindow>
|
||||||
|
|||||||
@@ -1,14 +1,8 @@
|
|||||||
using System;
|
using Content.Client.UserInterface.Controls;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Globalization;
|
|
||||||
using Content.Client.Atmos.EntitySystems;
|
|
||||||
using Content.Shared.Atmos;
|
using Content.Shared.Atmos;
|
||||||
using Content.Shared.Atmos.Prototypes;
|
|
||||||
using Robust.Client.AutoGenerated;
|
using Robust.Client.AutoGenerated;
|
||||||
using Robust.Client.UserInterface.Controls;
|
|
||||||
using Robust.Client.UserInterface.CustomControls;
|
using Robust.Client.UserInterface.CustomControls;
|
||||||
using Robust.Client.UserInterface.XAML;
|
using Robust.Client.UserInterface.XAML;
|
||||||
using Robust.Shared.Localization;
|
|
||||||
|
|
||||||
namespace Content.Client.Atmos.UI
|
namespace Content.Client.Atmos.UI
|
||||||
{
|
{
|
||||||
@@ -16,12 +10,25 @@ namespace Content.Client.Atmos.UI
|
|||||||
/// Client-side UI used to control a gas pressure pump.
|
/// Client-side UI used to control a gas pressure pump.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[GenerateTypedNameReferences]
|
[GenerateTypedNameReferences]
|
||||||
public sealed partial class GasPressurePumpWindow : DefaultWindow
|
public sealed partial class GasPressurePumpWindow : FancyWindow
|
||||||
{
|
{
|
||||||
public bool PumpStatus = true;
|
public bool PumpStatus = true;
|
||||||
|
|
||||||
public event Action? ToggleStatusButtonPressed;
|
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()
|
public GasPressurePumpWindow()
|
||||||
{
|
{
|
||||||
@@ -30,23 +37,25 @@ namespace Content.Client.Atmos.UI
|
|||||||
ToggleStatusButton.OnPressed += _ => SetPumpStatus(!PumpStatus);
|
ToggleStatusButton.OnPressed += _ => SetPumpStatus(!PumpStatus);
|
||||||
ToggleStatusButton.OnPressed += _ => ToggleStatusButtonPressed?.Invoke();
|
ToggleStatusButton.OnPressed += _ => ToggleStatusButtonPressed?.Invoke();
|
||||||
|
|
||||||
PumpPressureOutputInput.OnTextChanged += _ => SetOutputPressureButton.Disabled = false;
|
PumpPressureOutputInput.OnValueChanged += _ => SetOutputPressureButton.Disabled = false;
|
||||||
|
|
||||||
SetOutputPressureButton.OnPressed += _ =>
|
SetOutputPressureButton.OnPressed += _ =>
|
||||||
{
|
{
|
||||||
PumpOutputPressureChanged?.Invoke(PumpPressureOutputInput.Text ??= "");
|
PumpPressureOutputInput.Value = Math.Clamp(PumpPressureOutputInput.Value, 0f, _maxPressure);
|
||||||
|
PumpOutputPressureChanged?.Invoke(PumpPressureOutputInput.Value);
|
||||||
SetOutputPressureButton.Disabled = true;
|
SetOutputPressureButton.Disabled = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
SetMaxPressureButton.OnPressed += _ =>
|
SetMaxPressureButton.OnPressed += _ =>
|
||||||
{
|
{
|
||||||
PumpPressureOutputInput.Text = Atmospherics.MaxOutputPressure.ToString(CultureInfo.CurrentCulture);
|
PumpPressureOutputInput.Value = _maxPressure;
|
||||||
SetOutputPressureButton.Disabled = false;
|
SetOutputPressureButton.Disabled = false;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetOutputPressure(float pressure)
|
public void SetOutputPressure(float pressure)
|
||||||
{
|
{
|
||||||
PumpPressureOutputInput.Text = pressure.ToString(CultureInfo.CurrentCulture);
|
PumpPressureOutputInput.Value = pressure;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetPumpStatus(bool enabled)
|
public void SetPumpStatus(bool enabled)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using System.Numerics;
|
|||||||
using Content.Client.Chat.Managers;
|
using Content.Client.Chat.Managers;
|
||||||
using Content.Shared.CCVar;
|
using Content.Shared.CCVar;
|
||||||
using Content.Shared.Chat;
|
using Content.Shared.Chat;
|
||||||
|
using Content.Shared.Speech;
|
||||||
using Robust.Client.Graphics;
|
using Robust.Client.Graphics;
|
||||||
using Robust.Client.UserInterface;
|
using Robust.Client.UserInterface;
|
||||||
using Robust.Client.UserInterface.Controls;
|
using Robust.Client.UserInterface.Controls;
|
||||||
@@ -141,7 +142,12 @@ namespace Content.Client.Chat.UI
|
|||||||
Modulate = Color.White;
|
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 worldPos = _transformSystem.GetWorldPosition(xform) + offset;
|
||||||
|
|
||||||
var lowerCenter = _eyeManager.WorldToScreen(worldPos) / UIScale;
|
var lowerCenter = _eyeManager.WorldToScreen(worldPos) / UIScale;
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ using Robust.Shared.Utility;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using Content.Shared.FixedPoint;
|
using Content.Shared.FixedPoint;
|
||||||
|
using Robust.Client.Graphics;
|
||||||
using static Robust.Client.UserInterface.Controls.BoxContainer;
|
using static Robust.Client.UserInterface.Controls.BoxContainer;
|
||||||
|
|
||||||
namespace Content.Client.Chemistry.UI
|
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)
|
private ReagentButton MakeReagentButton(string text, ChemMasterReagentAmount amount, ReagentId id, bool isBuffer, string styleClass)
|
||||||
{
|
{
|
||||||
var button = new ReagentButton(text, amount, id, isBuffer, styleClass);
|
var reagentTransferButton = new ReagentButton(text, amount, id, isBuffer, styleClass);
|
||||||
button.OnPressed += args
|
reagentTransferButton.OnPressed += args
|
||||||
=> OnReagentButtonPressed?.Invoke(args, button);
|
=> OnReagentButtonPressed?.Invoke(args, reagentTransferButton);
|
||||||
return button;
|
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>
|
/// <summary>
|
||||||
@@ -102,24 +133,35 @@ namespace Content.Client.Chemistry.UI
|
|||||||
/// <param name="state">State data sent by the server.</param>
|
/// <param name="state">State data sent by the server.</param>
|
||||||
public void UpdateState(BoundUserInterfaceState state)
|
public void UpdateState(BoundUserInterfaceState state)
|
||||||
{
|
{
|
||||||
var castState = (ChemMasterBoundUserInterfaceState) state;
|
var castState = (ChemMasterBoundUserInterfaceState)state;
|
||||||
|
|
||||||
if (castState.UpdateLabel)
|
if (castState.UpdateLabel)
|
||||||
LabelLine = GenerateLabel(castState);
|
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";
|
BufferCurrentVolume.Text = $" {castState.BufferCurrentVolume?.Int() ?? 0}u";
|
||||||
|
|
||||||
InputEjectButton.Disabled = castState.InputContainerInfo is null;
|
InputEjectButton.Disabled = castState.InputContainerInfo is null;
|
||||||
OutputEjectButton.Disabled = output is null;
|
OutputEjectButton.Disabled = castState.OutputContainerInfo is null;
|
||||||
CreateBottleButton.Disabled = output?.Reagents == null;
|
CreateBottleButton.Disabled = castState.OutputContainerInfo?.Reagents == null;
|
||||||
CreatePillButton.Disabled = output?.Entities == 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 remainingCapacity = output is null ? 0 : (output.MaxVolume - output.CurrentVolume).Int();
|
||||||
var holdsReagents = output?.Reagents != null;
|
var holdsReagents = output?.Reagents != null;
|
||||||
var pillNumberMax = holdsReagents ? 0 : remainingCapacity;
|
var pillNumberMax = holdsReagents ? 0 : remainingCapacity;
|
||||||
var bottleAmountMax = holdsReagents ? remainingCapacity : 0;
|
var bottleAmountMax = holdsReagents ? remainingCapacity : 0;
|
||||||
|
var bufferVolume = castState.BufferCurrentVolume?.Int() ?? 0;
|
||||||
|
|
||||||
|
PillDosage.Value = (int)Math.Min(bufferVolume, castState.PillDosageLimit);
|
||||||
|
|
||||||
PillTypeButtons[castState.SelectedPillType].Pressed = true;
|
PillTypeButtons[castState.SelectedPillType].Pressed = true;
|
||||||
PillNumber.IsValid = x => x >= 0 && x <= pillNumberMax;
|
PillNumber.IsValid = x => x >= 0 && x <= pillNumberMax;
|
||||||
@@ -130,8 +172,19 @@ namespace Content.Client.Chemistry.UI
|
|||||||
PillNumber.Value = pillNumberMax;
|
PillNumber.Value = pillNumberMax;
|
||||||
if (BottleDosage.Value > bottleAmountMax)
|
if (BottleDosage.Value > bottleAmountMax)
|
||||||
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>
|
/// <summary>
|
||||||
/// Generate a product label based on reagents in the buffer.
|
/// Generate a product label based on reagents in the buffer.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -178,43 +231,20 @@ namespace Content.Client.Chemistry.UI
|
|||||||
var bufferVol = new Label
|
var bufferVol = new Label
|
||||||
{
|
{
|
||||||
Text = $"{state.BufferCurrentVolume}u",
|
Text = $"{state.BufferCurrentVolume}u",
|
||||||
StyleClasses = {StyleNano.StyleClassLabelSecondaryColor}
|
StyleClasses = { StyleNano.StyleClassLabelSecondaryColor }
|
||||||
};
|
};
|
||||||
bufferHBox.AddChild(bufferVol);
|
bufferHBox.AddChild(bufferVol);
|
||||||
|
|
||||||
|
// initialises rowCount to allow for striped rows
|
||||||
|
|
||||||
|
var rowCount = 0;
|
||||||
foreach (var (reagent, quantity) in state.BufferReagents)
|
foreach (var (reagent, quantity) in state.BufferReagents)
|
||||||
{
|
{
|
||||||
// Try to get the prototype for the given reagent. This gives us its name.
|
var reagentId = reagent;
|
||||||
_prototypeManager.TryIndex(reagent.Prototype, out ReagentPrototype? proto);
|
_prototypeManager.TryIndex(reagentId.Prototype, out ReagentPrototype? proto);
|
||||||
var name = proto?.LocalizedName ?? Loc.GetString("chem-master-window-unknown-reagent-text");
|
var name = proto?.LocalizedName ?? Loc.GetString("chem-master-window-unknown-reagent-text");
|
||||||
|
var reagentColor = proto?.SubstanceColor ?? default(Color);
|
||||||
if (proto != null)
|
BufferInfo.Children.Add(BuildReagentRow(reagentColor, rowCount++, name, reagentId, quantity, true, true));
|
||||||
{
|
|
||||||
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),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,104 +258,111 @@ namespace Content.Client.Chemistry.UI
|
|||||||
{
|
{
|
||||||
Text = Loc.GetString("chem-master-window-no-container-loaded-text")
|
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)
|
Orientation = LayoutOrientation.Horizontal,
|
||||||
control.Children.Add(new BoxContainer
|
Children =
|
||||||
{
|
{
|
||||||
Orientation = LayoutOrientation.Horizontal,
|
new Label { Text = $"{info.DisplayName}: " },
|
||||||
Children =
|
new Label
|
||||||
{
|
{
|
||||||
new Label {Text = $"{info.DisplayName}: "},
|
Text = $"{info.CurrentVolume}/{info.MaxVolume}",
|
||||||
new Label
|
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 =>
|
control.Children.Add(BuildReagentRow(default(Color), rowCount++, id, default(ReagentId), quantity, false, addReagentButtons));
|
||||||
{
|
|
||||||
_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);
|
|
||||||
}
|
}
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/// <summary>
|
||||||
public String LabelLine
|
/// 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;
|
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;
|
var dummy = _prototypeManager.Index<SpeciesPrototype>(humanoid.Species).DollPrototype;
|
||||||
dummyEnt = EntityManager.SpawnEntity(dummy, MapCoordinates.Nullspace);
|
dummyEnt = EntityManager.SpawnEntity(dummy, MapCoordinates.Nullspace);
|
||||||
@@ -469,7 +483,8 @@ public sealed class LobbyUIController : UIController, IOnStateEntered<LobbyState
|
|||||||
|
|
||||||
if (humanoid != null && jobClothes)
|
if (humanoid != null && jobClothes)
|
||||||
{
|
{
|
||||||
job ??= GetPreferredJob(humanoid);
|
DebugTools.Assert(job != null);
|
||||||
|
|
||||||
GiveDummyJobClothes(dummyEnt, humanoid, job);
|
GiveDummyJobClothes(dummyEnt, humanoid, job);
|
||||||
|
|
||||||
if (_prototypeManager.HasIndex<RoleLoadoutPrototype>(LoadoutSystem.GetJobPrototype(job.ID)))
|
if (_prototypeManager.HasIndex<RoleLoadoutPrototype>(LoadoutSystem.GetJobPrototype(job.ID)))
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
SeparationOverride="0"
|
SeparationOverride="0"
|
||||||
Name="InternalHBox">
|
Name="InternalHBox">
|
||||||
<SpriteView Scale="2 2"
|
<SpriteView Scale="2 2"
|
||||||
|
Margin="0 4 4 4"
|
||||||
OverrideDirection="South"
|
OverrideDirection="South"
|
||||||
Name="View"/>
|
Name="View"/>
|
||||||
<Label Name="DescriptionLabel"
|
<Label Name="DescriptionLabel"
|
||||||
|
|||||||
@@ -140,7 +140,7 @@
|
|||||||
</BoxContainer>
|
</BoxContainer>
|
||||||
<!-- Right side -->
|
<!-- Right side -->
|
||||||
<BoxContainer Orientation="Vertical" VerticalExpand="True" VerticalAlignment="Center">
|
<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">
|
<BoxContainer Orientation="Horizontal" HorizontalAlignment="Center" Margin="0 5">
|
||||||
<Button Name="SpriteRotateLeft" Text="◀" StyleClasses="OpenRight" />
|
<Button Name="SpriteRotateLeft" Text="◀" StyleClasses="OpenRight" />
|
||||||
<cc:VSeparator Margin="2 0 3 0" />
|
<cc:VSeparator Margin="2 0 3 0" />
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ using Robust.Client.UserInterface.Controls;
|
|||||||
using Robust.Client.UserInterface.XAML;
|
using Robust.Client.UserInterface.XAML;
|
||||||
using Robust.Shared.Utility;
|
using Robust.Shared.Utility;
|
||||||
using Robust.Client.UserInterface.RichText;
|
using Robust.Client.UserInterface.RichText;
|
||||||
|
using Content.Client.UserInterface.RichText;
|
||||||
using Robust.Shared.Input;
|
using Robust.Shared.Input;
|
||||||
|
|
||||||
namespace Content.Client.Paper.UI
|
namespace Content.Client.Paper.UI
|
||||||
@@ -43,7 +44,8 @@ namespace Content.Client.Paper.UI
|
|||||||
typeof(BulletTag),
|
typeof(BulletTag),
|
||||||
typeof(ColorTag),
|
typeof(ColorTag),
|
||||||
typeof(HeadingTag),
|
typeof(HeadingTag),
|
||||||
typeof(ItalicTag)
|
typeof(ItalicTag),
|
||||||
|
typeof(MonoTag)
|
||||||
};
|
};
|
||||||
|
|
||||||
public event Action<string>? OnSaved;
|
public event Action<string>? OnSaved;
|
||||||
|
|||||||
@@ -385,26 +385,6 @@ public partial class NavMapControl : MapGridControl
|
|||||||
if (PostWallDrawingAction != null)
|
if (PostWallDrawingAction != null)
|
||||||
PostWallDrawingAction.Invoke(handle);
|
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 curTime = Timing.RealTime;
|
||||||
var blinkFrequency = 1f / 1f;
|
var blinkFrequency = 1f / 1f;
|
||||||
var lit = curTime.TotalSeconds % blinkFrequency > blinkFrequency / 2f;
|
var lit = curTime.TotalSeconds % blinkFrequency > blinkFrequency / 2f;
|
||||||
@@ -443,11 +423,31 @@ public partial class NavMapControl : MapGridControl
|
|||||||
position = ScalePosition(new Vector2(position.X, -position.Y));
|
position = ScalePosition(new Vector2(position.X, -position.Y));
|
||||||
|
|
||||||
var scalingCoefficient = MinmapScaleModifier * float.Sqrt(MinimapScale);
|
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);
|
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)
|
protected override void FrameUpdate(FrameEventArgs args)
|
||||||
@@ -689,6 +689,9 @@ public partial class NavMapControl : MapGridControl
|
|||||||
Vector2i foundTermius;
|
Vector2i foundTermius;
|
||||||
Vector2i foundOrigin;
|
Vector2i foundOrigin;
|
||||||
|
|
||||||
|
if (origin == terminus)
|
||||||
|
return;
|
||||||
|
|
||||||
// Does our new line end at the beginning of an existing line?
|
// Does our new line end at the beginning of an existing line?
|
||||||
if (lookup.Remove(terminus, out foundTermius))
|
if (lookup.Remove(terminus, out foundTermius))
|
||||||
{
|
{
|
||||||
@@ -739,13 +742,15 @@ public struct NavMapBlip
|
|||||||
public Color Color;
|
public Color Color;
|
||||||
public bool Blinks;
|
public bool Blinks;
|
||||||
public bool Selectable;
|
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;
|
Coordinates = coordinates;
|
||||||
Texture = texture;
|
Texture = texture;
|
||||||
Color = color;
|
Color = color;
|
||||||
Blinks = blinks;
|
Blinks = blinks;
|
||||||
Selectable = selectable;
|
Selectable = selectable;
|
||||||
|
Scale = scale;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -157,10 +157,5 @@ namespace Content.Client.Sandbox
|
|||||||
{
|
{
|
||||||
_consoleHost.ExecuteCommand("physics shapes");
|
_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)
|
foreach (var ((_, amount), proto) in currency)
|
||||||
{
|
{
|
||||||
balanceStr += Loc.GetString("store-ui-balance-display", ("amount", amount),
|
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());
|
BalanceInfo.SetMarkup(balanceStr.TrimEnd());
|
||||||
@@ -63,7 +63,10 @@ public sealed partial class StoreMenu : DefaultWindow
|
|||||||
foreach (var type in currency)
|
foreach (var type in currency)
|
||||||
{
|
{
|
||||||
if (type.Value.CanWithdraw && type.Value.Cash != null && type.Key.Item2 > 0)
|
if (type.Value.CanWithdraw && type.Value.Cash != null && type.Key.Item2 > 0)
|
||||||
|
{
|
||||||
disabled = false;
|
disabled = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
WithdrawButton.Disabled = disabled;
|
WithdrawButton.Disabled = disabled;
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ public sealed partial class StoreWithdrawWindow : DefaultWindow
|
|||||||
{
|
{
|
||||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||||
|
|
||||||
private Dictionary<FixedPoint2, CurrencyPrototype> _validCurrencies = new();
|
private Dictionary<CurrencyPrototype, FixedPoint2> _validCurrencies = new();
|
||||||
private HashSet<CurrencyWithdrawButton> _buttons = new();
|
private HashSet<CurrencyWithdrawButton> _buttons = new();
|
||||||
public event Action<BaseButton.ButtonEventArgs, string, int>? OnWithdrawAttempt;
|
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))
|
if (!_prototypeManager.TryIndex(currency.Key, out var proto))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
_validCurrencies.Add(currency.Value, proto);
|
_validCurrencies.Add(proto, currency.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
//this shouldn't ever happen but w/e
|
//this shouldn't ever happen but w/e
|
||||||
@@ -47,14 +47,17 @@ public sealed partial class StoreWithdrawWindow : DefaultWindow
|
|||||||
_buttons.Clear();
|
_buttons.Clear();
|
||||||
foreach (var currency in _validCurrencies)
|
foreach (var currency in _validCurrencies)
|
||||||
{
|
{
|
||||||
|
if (!currency.Key.CanWithdraw)
|
||||||
|
continue;
|
||||||
|
|
||||||
var button = new CurrencyWithdrawButton()
|
var button = new CurrencyWithdrawButton()
|
||||||
{
|
{
|
||||||
Id = currency.Value.ID,
|
Id = currency.Key.ID,
|
||||||
Amount = currency.Key,
|
Amount = currency.Value,
|
||||||
MinHeight = 20,
|
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 =>
|
button.OnPressed += args =>
|
||||||
{
|
{
|
||||||
OnWithdrawAttempt?.Invoke(args, button.Id, WithdrawSlider.Value);
|
OnWithdrawAttempt?.Invoke(args, button.Id, WithdrawSlider.Value);
|
||||||
@@ -65,7 +68,7 @@ public sealed partial class StoreWithdrawWindow : DefaultWindow
|
|||||||
ButtonContainer.AddChild(button);
|
ButtonContainer.AddChild(button);
|
||||||
}
|
}
|
||||||
|
|
||||||
var maxWithdrawAmount = _validCurrencies.Keys.Max().Int();
|
var maxWithdrawAmount = _validCurrencies.Values.Max().Int();
|
||||||
|
|
||||||
// setup withdraw slider
|
// setup withdraw slider
|
||||||
WithdrawSlider.MinValue = 1;
|
WithdrawSlider.MinValue = 1;
|
||||||
|
|||||||
@@ -110,6 +110,7 @@ namespace Content.Client.Stylesheets
|
|||||||
|
|
||||||
public static readonly Color ButtonColorGoodDefault = Color.FromHex("#3E6C45");
|
public static readonly Color ButtonColorGoodDefault = Color.FromHex("#3E6C45");
|
||||||
public static readonly Color ButtonColorGoodHovered = Color.FromHex("#31843E");
|
public static readonly Color ButtonColorGoodHovered = Color.FromHex("#31843E");
|
||||||
|
public static readonly Color ButtonColorGoodDisabled = Color.FromHex("#164420");
|
||||||
|
|
||||||
//NavMap
|
//NavMap
|
||||||
public static readonly Color PointRed = Color.FromHex("#B02E26");
|
public static readonly Color PointRed = Color.FromHex("#B02E26");
|
||||||
@@ -1499,6 +1500,20 @@ namespace Content.Client.Stylesheets
|
|||||||
|
|
||||||
Element<Button>().Class("ButtonColorGreen").Pseudo(ContainerButton.StylePseudoClassHover)
|
Element<Button>().Class("ButtonColorGreen").Pseudo(ContainerButton.StylePseudoClassHover)
|
||||||
.Prop(Control.StylePropertyModulateSelf, ButtonColorGoodHovered),
|
.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 ---
|
// 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.OnClose += () => { OnClose?.Invoke(); };
|
||||||
_window.OnOpen += () => { OnOpen?.Invoke(); };
|
_window.OnOpen += () => { OnOpen?.Invoke(); };
|
||||||
_window.Contents.AddChild(_chatPanel);
|
_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()
|
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.Gameplay;
|
||||||
using Content.Client.Markers;
|
using Content.Client.Markers;
|
||||||
using Content.Client.Sandbox;
|
using Content.Client.Sandbox;
|
||||||
@@ -7,9 +8,7 @@ using Content.Client.UserInterface.Controls;
|
|||||||
using Content.Client.UserInterface.Systems.DecalPlacer;
|
using Content.Client.UserInterface.Systems.DecalPlacer;
|
||||||
using Content.Client.UserInterface.Systems.Sandbox.Windows;
|
using Content.Client.UserInterface.Systems.Sandbox.Windows;
|
||||||
using Content.Shared.Input;
|
using Content.Shared.Input;
|
||||||
using Content.Shared.Silicons.StationAi;
|
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using Robust.Client.Console;
|
|
||||||
using Robust.Client.Debugging;
|
using Robust.Client.Debugging;
|
||||||
using Robust.Client.Graphics;
|
using Robust.Client.Graphics;
|
||||||
using Robust.Client.Input;
|
using Robust.Client.Input;
|
||||||
@@ -109,9 +108,13 @@ public sealed class SandboxUIController : UIController, IOnStateChanged<Gameplay
|
|||||||
|
|
||||||
private void EnsureWindow()
|
private void EnsureWindow()
|
||||||
{
|
{
|
||||||
if(_window is { Disposed: false })
|
if (_window is { Disposed: false })
|
||||||
return;
|
return;
|
||||||
_window = UIManager.CreateWindow<SandboxWindow>();
|
_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.OnOpen += () => { SandboxButton!.Pressed = true; };
|
||||||
_window.OnClose += () => { SandboxButton!.Pressed = false; };
|
_window.OnClose += () => { SandboxButton!.Pressed = false; };
|
||||||
_window.ToggleLightButton.Pressed = !_light.Enabled;
|
_window.ToggleLightButton.Pressed = !_light.Enabled;
|
||||||
@@ -149,7 +152,6 @@ public sealed class SandboxUIController : UIController, IOnStateChanged<Gameplay
|
|||||||
_window.ToggleSubfloorButton.OnPressed += _ => _sandbox.ToggleSubFloor();
|
_window.ToggleSubfloorButton.OnPressed += _ => _sandbox.ToggleSubFloor();
|
||||||
_window.ShowMarkersButton.OnPressed += _ => _sandbox.ShowMarkers();
|
_window.ShowMarkersButton.OnPressed += _ => _sandbox.ShowMarkers();
|
||||||
_window.ShowBbButton.OnPressed += _ => _sandbox.ShowBb();
|
_window.ShowBbButton.OnPressed += _ => _sandbox.ShowBb();
|
||||||
_window.MachineLinkingButton.OnPressed += _ => _sandbox.MachineLinking();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CheckSandboxVisibility()
|
private void CheckSandboxVisibility()
|
||||||
@@ -164,7 +166,7 @@ public sealed class SandboxUIController : UIController, IOnStateChanged<Gameplay
|
|||||||
{
|
{
|
||||||
if (_window != null)
|
if (_window != null)
|
||||||
{
|
{
|
||||||
_window.Dispose();
|
_window.Close();
|
||||||
_window = null;
|
_window = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,7 +211,7 @@ public sealed class SandboxUIController : UIController, IOnStateChanged<Gameplay
|
|||||||
if (_sandbox.SandboxAllowed && _window.IsOpen != true)
|
if (_sandbox.SandboxAllowed && _window.IsOpen != true)
|
||||||
{
|
{
|
||||||
UIManager.ClickSound();
|
UIManager.ClickSound();
|
||||||
_window.OpenCentered();
|
_window.Open();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,20 +4,24 @@
|
|||||||
Title="{Loc sandbox-window-title}"
|
Title="{Loc sandbox-window-title}"
|
||||||
Resizable="False">
|
Resizable="False">
|
||||||
<BoxContainer Orientation="Vertical" SeparationOverride="4">
|
<BoxContainer Orientation="Vertical" SeparationOverride="4">
|
||||||
<Button Name="AiOverlayButton" Access="Public" Text="{Loc sandbox-window-ai-overlay-button}" ToggleMode="True"/>
|
<Label Text="{Loc sandbox-window-map-editing-label}"/>
|
||||||
<Button Name="RespawnButton" Access="Public" Text="{Loc sandbox-window-respawn-button}"/>
|
|
||||||
<Button Name="SpawnEntitiesButton" Access="Public" Text="{Loc sandbox-window-spawn-entities-button}"/>
|
|
||||||
<Button Name="SpawnTilesButton" Access="Public" Text="{Loc sandbox-window-spawn-tiles-button}"/>
|
<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="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="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="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="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="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="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="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>
|
</BoxContainer>
|
||||||
</windows:SandboxWindow>
|
</windows:SandboxWindow>
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ public sealed partial class GunSystem : SharedGunSystem
|
|||||||
base.Initialize();
|
base.Initialize();
|
||||||
UpdatesOutsidePrediction = true;
|
UpdatesOutsidePrediction = true;
|
||||||
SubscribeLocalEvent<AmmoCounterComponent, ItemStatusCollectMessage>(OnAmmoCounterCollect);
|
SubscribeLocalEvent<AmmoCounterComponent, ItemStatusCollectMessage>(OnAmmoCounterCollect);
|
||||||
|
SubscribeLocalEvent<AmmoCounterComponent, UpdateClientAmmoEvent>(OnUpdateClientAmmo);
|
||||||
SubscribeAllEvent<MuzzleFlashEvent>(OnMuzzleFlash);
|
SubscribeAllEvent<MuzzleFlashEvent>(OnMuzzleFlash);
|
||||||
|
|
||||||
// Plays animated effects on the client.
|
// Plays animated effects on the client.
|
||||||
@@ -86,6 +87,11 @@ public sealed partial class GunSystem : SharedGunSystem
|
|||||||
InitializeSpentAmmo();
|
InitializeSpentAmmo();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnUpdateClientAmmo(EntityUid uid, AmmoCounterComponent ammoComp, ref UpdateClientAmmoEvent args)
|
||||||
|
{
|
||||||
|
UpdateAmmoCount(uid, ammoComp);
|
||||||
|
}
|
||||||
|
|
||||||
private void OnMuzzleFlash(MuzzleFlashEvent args)
|
private void OnMuzzleFlash(MuzzleFlashEvent args)
|
||||||
{
|
{
|
||||||
var gunUid = GetEntity(args.Uid);
|
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",
|
"Train",
|
||||||
"Oasis",
|
"Oasis",
|
||||||
"Cog",
|
"Cog",
|
||||||
|
"Gate",
|
||||||
"Amber"
|
"Amber"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -98,24 +98,4 @@ public sealed class ResearchTest
|
|||||||
|
|
||||||
await pair.CleanReturnAsync();
|
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);
|
var recharger = EnsureComp<BatterySelfRechargerComponent>(args.Target);
|
||||||
recharger.AutoRecharge = true;
|
recharger.AutoRecharge = true;
|
||||||
recharger.AutoRechargeRate = battery.MaxCharge; // Instant refill.
|
recharger.AutoRechargeRate = battery.MaxCharge; // Instant refill.
|
||||||
|
recharger.AutoRechargePause = false; // No delay.
|
||||||
},
|
},
|
||||||
Impact = LogImpact.Medium,
|
Impact = LogImpact.Medium,
|
||||||
Message = Loc.GetString("admin-trick-infinite-battery-object-description"),
|
Message = Loc.GetString("admin-trick-infinite-battery-object-description"),
|
||||||
@@ -603,6 +604,7 @@ public sealed partial class AdminVerbSystem
|
|||||||
|
|
||||||
recharger.AutoRecharge = true;
|
recharger.AutoRecharge = true;
|
||||||
recharger.AutoRechargeRate = battery.MaxCharge; // Instant refill.
|
recharger.AutoRechargeRate = battery.MaxCharge; // Instant refill.
|
||||||
|
recharger.AutoRechargePause = false; // No delay.
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Impact = LogImpact.Extreme,
|
Impact = LogImpact.Extreme,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using Content.Server.Animals.Systems;
|
||||||
using Content.Shared.Storage;
|
using Content.Shared.Storage;
|
||||||
using Robust.Shared.Audio;
|
using Robust.Shared.Audio;
|
||||||
using Robust.Shared.Prototypes;
|
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.
|
/// It also grants an action to players who are controlling these entities, allowing them to do it manually.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
||||||
[RegisterComponent]
|
[RegisterComponent, Access(typeof(EggLayerSystem)), AutoGenerateComponentPause]
|
||||||
public sealed partial class EggLayerComponent : Component
|
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]
|
[DataField]
|
||||||
public EntProtoId EggLayAction = "ActionAnimalLayEgg";
|
public EntProtoId EggLayAction = "ActionAnimalLayEgg";
|
||||||
|
|
||||||
/// <summary>
|
[DataField]
|
||||||
/// The amount of nutrient consumed on update.
|
public SoundSpecifier EggLaySound = new SoundPathSpecifier("/Audio/Effects/pop.ogg");
|
||||||
/// </summary>
|
|
||||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
|
||||||
public float HungerUsage = 60f;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Minimum cooldown used for the automatic egg laying.
|
/// Minimum cooldown used for the automatic egg laying.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
[DataField]
|
||||||
public float EggLayCooldownMin = 60f;
|
public float EggLayCooldownMin = 60f;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Maximum cooldown used for the automatic egg laying.
|
/// Maximum cooldown used for the automatic egg laying.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
[DataField]
|
||||||
public float EggLayCooldownMax = 120f;
|
public float EggLayCooldownMax = 120f;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Set during component init.
|
/// The amount of nutrient consumed on update.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ViewVariables(VVAccess.ReadWrite)]
|
|
||||||
public float CurrentEggLayCooldown;
|
|
||||||
|
|
||||||
[DataField(required: true), ViewVariables(VVAccess.ReadWrite)]
|
|
||||||
public List<EntitySpawnEntry> EggSpawn = default!;
|
|
||||||
|
|
||||||
[DataField]
|
[DataField]
|
||||||
public SoundSpecifier EggLaySound = new SoundPathSpecifier("/Audio/Effects/pop.ogg");
|
public float HungerUsage = 60f;
|
||||||
|
|
||||||
[DataField]
|
|
||||||
public float AccumulatedFrametime;
|
|
||||||
|
|
||||||
[DataField] public EntityUid? Action;
|
[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.Nutrition.EntitySystems;
|
||||||
using Content.Shared.Storage;
|
using Content.Shared.Storage;
|
||||||
using Robust.Server.Audio;
|
using Robust.Server.Audio;
|
||||||
using Robust.Server.GameObjects;
|
|
||||||
using Robust.Shared.Player;
|
using Robust.Shared.Player;
|
||||||
using Robust.Shared.Random;
|
using Robust.Shared.Random;
|
||||||
|
using Robust.Shared.Timing;
|
||||||
|
|
||||||
namespace Content.Server.Animals.Systems;
|
namespace Content.Server.Animals.Systems;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gives ability to produce eggs, produces endless if the
|
/// Gives the ability to lay eggs/other things;
|
||||||
/// owner has no HungerComponent
|
/// produces endlessly if the owner does not have a HungerComponent.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class EggLayerSystem : EntitySystem
|
public sealed class EggLayerSystem : EntitySystem
|
||||||
{
|
{
|
||||||
@@ -23,6 +23,7 @@ public sealed class EggLayerSystem : EntitySystem
|
|||||||
[Dependency] private readonly ActionsSystem _actions = default!;
|
[Dependency] private readonly ActionsSystem _actions = default!;
|
||||||
[Dependency] private readonly AudioSystem _audio = default!;
|
[Dependency] private readonly AudioSystem _audio = default!;
|
||||||
[Dependency] private readonly HungerSystem _hunger = default!;
|
[Dependency] private readonly HungerSystem _hunger = default!;
|
||||||
|
[Dependency] private readonly IGameTiming _timing = default!;
|
||||||
[Dependency] private readonly PopupSystem _popup = default!;
|
[Dependency] private readonly PopupSystem _popup = default!;
|
||||||
[Dependency] private readonly MobStateSystem _mobState = default!;
|
[Dependency] private readonly MobStateSystem _mobState = default!;
|
||||||
|
|
||||||
@@ -37,7 +38,6 @@ public sealed class EggLayerSystem : EntitySystem
|
|||||||
public override void Update(float frameTime)
|
public override void Update(float frameTime)
|
||||||
{
|
{
|
||||||
base.Update(frameTime);
|
base.Update(frameTime);
|
||||||
|
|
||||||
var query = EntityQueryEnumerator<EggLayerComponent>();
|
var query = EntityQueryEnumerator<EggLayerComponent>();
|
||||||
while (query.MoveNext(out var uid, out var eggLayer))
|
while (query.MoveNext(out var uid, out var eggLayer))
|
||||||
{
|
{
|
||||||
@@ -45,13 +45,17 @@ public sealed class EggLayerSystem : EntitySystem
|
|||||||
if (HasComp<ActorComponent>(uid))
|
if (HasComp<ActorComponent>(uid))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
eggLayer.AccumulatedFrametime += frameTime;
|
if (_timing.CurTime < eggLayer.NextGrowth)
|
||||||
|
|
||||||
if (eggLayer.AccumulatedFrametime < eggLayer.CurrentEggLayCooldown)
|
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
eggLayer.AccumulatedFrametime -= eggLayer.CurrentEggLayCooldown;
|
// Randomize next growth time for more organic egglaying.
|
||||||
eggLayer.CurrentEggLayCooldown = _random.NextFloat(eggLayer.EggLayCooldownMin, eggLayer.EggLayCooldownMax);
|
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);
|
TryLayEgg(uid, eggLayer);
|
||||||
}
|
}
|
||||||
@@ -60,11 +64,12 @@ public sealed class EggLayerSystem : EntitySystem
|
|||||||
private void OnMapInit(EntityUid uid, EggLayerComponent component, MapInitEvent args)
|
private void OnMapInit(EntityUid uid, EggLayerComponent component, MapInitEvent args)
|
||||||
{
|
{
|
||||||
_actions.AddAction(uid, ref component.Action, component.EggLayAction);
|
_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)
|
private void OnEggLayAction(EntityUid uid, EggLayerComponent egglayer, EggLayInstantActionEvent args)
|
||||||
{
|
{
|
||||||
|
// Cooldown is handeled by ActionAnimalLayEgg in types.yml.
|
||||||
args.Handled = TryLayEgg(uid, egglayer);
|
args.Handled = TryLayEgg(uid, egglayer);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,7 +81,7 @@ public sealed class EggLayerSystem : EntitySystem
|
|||||||
if (_mobState.IsDead(uid))
|
if (_mobState.IsDead(uid))
|
||||||
return false;
|
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 (TryComp<HungerComponent>(uid, out var hunger))
|
||||||
{
|
{
|
||||||
if (hunger.CurrentHunger < egglayer.HungerUsage)
|
if (hunger.CurrentHunger < egglayer.HungerUsage)
|
||||||
|
|||||||
@@ -186,6 +186,11 @@ public sealed class InnerBodyAnomalySystem : SharedInnerBodyAnomalySystem
|
|||||||
if (args.NewMobState != MobState.Dead)
|
if (args.NewMobState != MobState.Dead)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
var ev = new BeforeRemoveAnomalyOnDeathEvent();
|
||||||
|
RaiseLocalEvent(args.Target, ref ev);
|
||||||
|
if (ev.Cancelled)
|
||||||
|
return;
|
||||||
|
|
||||||
_anomaly.ChangeAnomalyHealth(ent, -2); //Shutdown it
|
_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.Server.Power.EntitySystems;
|
||||||
using Content.Shared.Atmos;
|
using Content.Shared.Atmos;
|
||||||
using Content.Shared.Atmos.Monitor;
|
using Content.Shared.Atmos.Monitor;
|
||||||
|
using Content.Shared.Atmos.Piping.Components;
|
||||||
using Content.Shared.DeviceNetwork;
|
using Content.Shared.DeviceNetwork;
|
||||||
using Content.Shared.Power;
|
using Content.Shared.Power;
|
||||||
using Content.Shared.Tag;
|
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.EntitySystems;
|
||||||
using Content.Server.Atmos.Piping.Binary.Components;
|
|
||||||
using Content.Server.Atmos.Piping.Components;
|
using Content.Server.Atmos.Piping.Components;
|
||||||
using Content.Server.NodeContainer.EntitySystems;
|
using Content.Server.NodeContainer.EntitySystems;
|
||||||
using Content.Server.NodeContainer.Nodes;
|
using Content.Server.NodeContainer.Nodes;
|
||||||
using Content.Server.Power.Components;
|
using Content.Server.Power.Components;
|
||||||
using Content.Shared.Atmos;
|
using Content.Shared.Atmos;
|
||||||
using Content.Shared.Atmos.Piping;
|
using Content.Shared.Atmos.Components;
|
||||||
using Content.Shared.Atmos.Piping.Binary.Components;
|
using Content.Shared.Atmos.EntitySystems;
|
||||||
using Content.Shared.Audio;
|
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 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]
|
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
|
||||||
public sealed class GasPressurePumpSystem : EntitySystem
|
[Dependency] private readonly SharedAmbientSoundSystem _ambientSoundSystem = default!;
|
||||||
|
[Dependency] private readonly NodeContainerSystem _nodeContainer = default!;
|
||||||
|
|
||||||
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
[Dependency] private readonly UserInterfaceSystem _userInterfaceSystem = default!;
|
base.Initialize();
|
||||||
[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!;
|
|
||||||
|
|
||||||
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();
|
_ambientSoundSystem.SetAmbience(uid, false);
|
||||||
|
return;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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.
|
// We calculate the necessary moles to transfer using our good ol' friend PV=nRT.
|
||||||
return;
|
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,
|
var removed = inlet.Air.Remove(transferMoles);
|
||||||
("statusColor", "lightblue"), // TODO: change with pressure?
|
_atmosphereSystem.Merge(outlet.Air, removed);
|
||||||
("pressure", pump.TargetPressure)
|
_ambientSoundSystem.SetAmbience(uid, removed.TotalMoles > 0f);
|
||||||
))
|
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using Content.Server.NodeContainer.EntitySystems;
|
|||||||
using Content.Server.NodeContainer.Nodes;
|
using Content.Server.NodeContainer.Nodes;
|
||||||
using Content.Shared.Atmos;
|
using Content.Shared.Atmos;
|
||||||
using Content.Shared.Atmos.Piping;
|
using Content.Shared.Atmos.Piping;
|
||||||
|
using Content.Shared.Atmos.Piping.Components;
|
||||||
using Content.Shared.Audio;
|
using Content.Shared.Audio;
|
||||||
using Content.Shared.Examine;
|
using Content.Shared.Examine;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ using Content.Server.NodeContainer.EntitySystems;
|
|||||||
using Content.Server.NodeContainer.Nodes;
|
using Content.Server.NodeContainer.Nodes;
|
||||||
using Content.Server.Power.Components;
|
using Content.Server.Power.Components;
|
||||||
using Content.Shared.Atmos.Piping.Binary.Components;
|
using Content.Shared.Atmos.Piping.Binary.Components;
|
||||||
|
using Content.Shared.Atmos.Piping.Components;
|
||||||
using Content.Shared.Atmos.Visuals;
|
using Content.Shared.Atmos.Visuals;
|
||||||
using Content.Shared.Audio;
|
using Content.Shared.Audio;
|
||||||
using Content.Shared.Database;
|
using Content.Shared.Database;
|
||||||
|
|||||||
@@ -67,15 +67,3 @@ public readonly struct AtmosDeviceUpdateEvent(float dt, Entity<GridAtmosphereCom
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public readonly Entity<MapAtmosphereComponent?>? Map = map;
|
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 Content.Server.Atmos.Piping.EntitySystems;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
|
|
||||||
namespace Content.Server.Atmos.Piping.Components
|
namespace Content.Server.Atmos.Piping.Components;
|
||||||
{
|
|
||||||
[RegisterComponent]
|
|
||||||
public sealed partial class AtmosPipeColorComponent : Component
|
|
||||||
{
|
|
||||||
[DataField("color")]
|
|
||||||
public Color Color { get; set; } = Color.White;
|
|
||||||
|
|
||||||
[ViewVariables(VVAccess.ReadWrite), UsedImplicitly]
|
[RegisterComponent]
|
||||||
public Color ColorVV
|
public sealed partial class AtmosPipeColorComponent : Component
|
||||||
{
|
{
|
||||||
get => Color;
|
[DataField]
|
||||||
set => IoCManager.Resolve<IEntityManager>().System<AtmosPipeColorSystem>().SetColor(Owner, this, value);
|
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.Components;
|
||||||
using Content.Server.Atmos.EntitySystems;
|
using Content.Server.Atmos.EntitySystems;
|
||||||
using Content.Server.Atmos.Piping.Components;
|
using Content.Server.Atmos.Piping.Components;
|
||||||
|
using Content.Shared.Atmos.Piping.Components;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using Robust.Shared.Timing;
|
using Robust.Shared.Timing;
|
||||||
using Robust.Shared.Utility;
|
using Robust.Shared.Utility;
|
||||||
|
|||||||
@@ -40,6 +40,9 @@ namespace Content.Server.Atmos.Piping.EntitySystems
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
_appearance.SetData(uid, PipeColorVisuals.Color, color, appearance);
|
_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.EntitySystems;
|
||||||
using Content.Server.Atmos.Piping.Components;
|
using Content.Server.Atmos.Piping.Components;
|
||||||
using Content.Server.NodeContainer;
|
using Content.Server.NodeContainer;
|
||||||
|
using Content.Server.NodeContainer.EntitySystems;
|
||||||
using Content.Server.NodeContainer.Nodes;
|
using Content.Server.NodeContainer.Nodes;
|
||||||
using Content.Server.Popups;
|
using Content.Server.Popups;
|
||||||
using Content.Shared.Atmos;
|
using Content.Shared.Atmos;
|
||||||
@@ -8,7 +9,6 @@ using Content.Shared.Construction.Components;
|
|||||||
using Content.Shared.Destructible;
|
using Content.Shared.Destructible;
|
||||||
using Content.Shared.Popups;
|
using Content.Shared.Popups;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using Robust.Shared.Player;
|
|
||||||
|
|
||||||
namespace Content.Server.Atmos.Piping.EntitySystems
|
namespace Content.Server.Atmos.Piping.EntitySystems
|
||||||
{
|
{
|
||||||
@@ -16,11 +16,12 @@ namespace Content.Server.Atmos.Piping.EntitySystems
|
|||||||
public sealed class AtmosUnsafeUnanchorSystem : EntitySystem
|
public sealed class AtmosUnsafeUnanchorSystem : EntitySystem
|
||||||
{
|
{
|
||||||
[Dependency] private readonly AtmosphereSystem _atmosphere = default!;
|
[Dependency] private readonly AtmosphereSystem _atmosphere = default!;
|
||||||
|
[Dependency] private readonly NodeGroupSystem _group = default!;
|
||||||
[Dependency] private readonly PopupSystem _popup = default!;
|
[Dependency] private readonly PopupSystem _popup = default!;
|
||||||
|
|
||||||
public override void Initialize()
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
SubscribeLocalEvent<AtmosUnsafeUnanchorComponent, BeforeUnanchoredEvent>(OnBeforeUnanchored);
|
SubscribeLocalEvent<AtmosUnsafeUnanchorComponent, UserUnanchoredEvent>(OnUserUnanchored);
|
||||||
SubscribeLocalEvent<AtmosUnsafeUnanchorComponent, UnanchorAttemptEvent>(OnUnanchorAttempt);
|
SubscribeLocalEvent<AtmosUnsafeUnanchorComponent, UnanchorAttemptEvent>(OnUnanchorAttempt);
|
||||||
SubscribeLocalEvent<AtmosUnsafeUnanchorComponent, BreakageEventArgs>(OnBreak);
|
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)
|
if (component.Enabled)
|
||||||
|
{
|
||||||
|
_group.ForceUpdate();
|
||||||
LeakGas(uid);
|
LeakGas(uid);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnBreak(EntityUid uid, AtmosUnsafeUnanchorComponent component, BreakageEventArgs args)
|
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
|
// 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.
|
// from leaking. So we make up for this by queueing deletion here.
|
||||||
QueueDel(uid);
|
QueueDel(uid);
|
||||||
@@ -64,32 +72,17 @@ namespace Content.Server.Atmos.Piping.EntitySystems
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Leak gas from the uid's NodeContainer into the tile atmosphere.
|
/// 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>
|
/// </summary>
|
||||||
public void LeakGas(EntityUid uid)
|
public void LeakGas(EntityUid uid, bool removeFromPipe = true)
|
||||||
{
|
{
|
||||||
if (!EntityManager.TryGetComponent(uid, out NodeContainerComponent? nodes))
|
if (!EntityManager.TryGetComponent(uid, out NodeContainerComponent? nodes))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (_atmosphere.GetContainingMixture(uid, true, true) is not {} environment)
|
if (_atmosphere.GetContainingMixture(uid, true, true) is not { } environment)
|
||||||
environment = GasMixture.SpaceGas;
|
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();
|
var buffer = new GasMixture();
|
||||||
|
|
||||||
foreach (var node in nodes.Nodes.Values)
|
foreach (var node in nodes.Nodes.Values)
|
||||||
@@ -97,7 +90,13 @@ namespace Content.Server.Atmos.Piping.EntitySystems
|
|||||||
if (node is not PipeNode pipe)
|
if (node is not PipeNode pipe)
|
||||||
continue;
|
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);
|
_atmosphere.Merge(environment, buffer);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using Content.Server.NodeContainer.EntitySystems;
|
|||||||
using Content.Server.NodeContainer.Nodes;
|
using Content.Server.NodeContainer.Nodes;
|
||||||
using Content.Shared.Atmos;
|
using Content.Shared.Atmos;
|
||||||
using Content.Shared.Atmos.Piping;
|
using Content.Shared.Atmos.Piping;
|
||||||
|
using Content.Shared.Atmos.Piping.Components;
|
||||||
using Content.Shared.Atmos.Piping.Trinary.Components;
|
using Content.Shared.Atmos.Piping.Trinary.Components;
|
||||||
using Content.Shared.Audio;
|
using Content.Shared.Audio;
|
||||||
using Content.Shared.Database;
|
using Content.Shared.Database;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using Content.Server.NodeContainer.EntitySystems;
|
|||||||
using Content.Server.NodeContainer.Nodes;
|
using Content.Server.NodeContainer.Nodes;
|
||||||
using Content.Shared.Atmos;
|
using Content.Shared.Atmos;
|
||||||
using Content.Shared.Atmos.Piping;
|
using Content.Shared.Atmos.Piping;
|
||||||
|
using Content.Shared.Atmos.Piping.Components;
|
||||||
using Content.Shared.Atmos.Piping.Trinary.Components;
|
using Content.Shared.Atmos.Piping.Trinary.Components;
|
||||||
using Content.Shared.Audio;
|
using Content.Shared.Audio;
|
||||||
using Content.Shared.Database;
|
using Content.Shared.Database;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using Content.Server.NodeContainer;
|
|||||||
using Content.Server.NodeContainer.EntitySystems;
|
using Content.Server.NodeContainer.EntitySystems;
|
||||||
using Content.Server.NodeContainer.Nodes;
|
using Content.Server.NodeContainer.Nodes;
|
||||||
using Content.Shared.Atmos.Piping;
|
using Content.Shared.Atmos.Piping;
|
||||||
|
using Content.Shared.Atmos.Piping.Components;
|
||||||
using Content.Shared.Audio;
|
using Content.Shared.Audio;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
|
|
||||||
|
|||||||
@@ -44,11 +44,6 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
portableNode.ConnectionsEnabled = args.Anchored;
|
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)
|
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.Server.NodeContainer.Nodes;
|
||||||
using Content.Shared.Atmos;
|
using Content.Shared.Atmos;
|
||||||
using Content.Shared.Atmos.Monitor;
|
using Content.Shared.Atmos.Monitor;
|
||||||
|
using Content.Shared.Atmos.Piping.Components;
|
||||||
using Content.Shared.Atmos.Piping.Unary;
|
using Content.Shared.Atmos.Piping.Unary;
|
||||||
using Content.Shared.Atmos.Piping.Unary.Components;
|
using Content.Shared.Atmos.Piping.Unary.Components;
|
||||||
using Content.Shared.Atmos.Visuals;
|
using Content.Shared.Atmos.Visuals;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ using Content.Server.Power.Components;
|
|||||||
using Content.Shared.Atmos;
|
using Content.Shared.Atmos;
|
||||||
using Content.Shared.Atmos.Piping.Unary.Visuals;
|
using Content.Shared.Atmos.Piping.Unary.Visuals;
|
||||||
using Content.Shared.Atmos.Monitor;
|
using Content.Shared.Atmos.Monitor;
|
||||||
|
using Content.Shared.Atmos.Piping.Components;
|
||||||
using Content.Shared.Atmos.Piping.Unary.Components;
|
using Content.Shared.Atmos.Piping.Unary.Components;
|
||||||
using Content.Shared.Audio;
|
using Content.Shared.Audio;
|
||||||
using Content.Shared.DeviceNetwork;
|
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.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
using Content.Server.Administration.Managers;
|
||||||
using Content.Server.Chat.Managers;
|
using Content.Server.Chat.Managers;
|
||||||
using Content.Server.Database;
|
using Content.Server.Database;
|
||||||
using Content.Server.GameTicking;
|
using Content.Server.GameTicking;
|
||||||
@@ -56,6 +57,7 @@ namespace Content.Server.Connection
|
|||||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||||
[Dependency] private readonly ILogManager _logManager = default!;
|
[Dependency] private readonly ILogManager _logManager = default!;
|
||||||
[Dependency] private readonly IChatManager _chatManager = default!;
|
[Dependency] private readonly IChatManager _chatManager = default!;
|
||||||
|
[Dependency] private readonly IAdminManager _adminManager = default!;
|
||||||
|
|
||||||
private ISawmill _sawmill = default!;
|
private ISawmill _sawmill = default!;
|
||||||
private readonly Dictionary<NetUserId, TimeSpan> _temporaryBypasses = [];
|
private readonly Dictionary<NetUserId, TimeSpan> _temporaryBypasses = [];
|
||||||
@@ -270,7 +272,14 @@ namespace Content.Server.Connection
|
|||||||
ticker.PlayerGameStatuses.TryGetValue(userId, out var status) &&
|
ticker.PlayerGameStatuses.TryGetValue(userId, out var status) &&
|
||||||
status == PlayerGameStatus.JoinedGame;
|
status == PlayerGameStatus.JoinedGame;
|
||||||
var adminBypass = _cfg.GetCVar(CCVars.AdminBypassMaxPlayers) && adminData != null;
|
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);
|
return (ConnectionDenyReason.Full, Loc.GetString("soft-player-cap-full"), null);
|
||||||
}
|
}
|
||||||
@@ -287,7 +296,7 @@ namespace Content.Server.Connection
|
|||||||
|
|
||||||
foreach (var whitelist in _whitelists)
|
foreach (var whitelist in _whitelists)
|
||||||
{
|
{
|
||||||
if (!IsValid(whitelist, _plyMgr.PlayerCount))
|
if (!IsValid(whitelist, softPlayerCount))
|
||||||
{
|
{
|
||||||
// Not valid for current player count.
|
// Not valid for current player count.
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -1,53 +1,58 @@
|
|||||||
using Content.Shared.DeviceLinking;
|
using Content.Shared.DeviceLinking;
|
||||||
using Robust.Shared.Audio;
|
using Robust.Shared.Audio;
|
||||||
using Robust.Shared.Prototypes;
|
using Robust.Shared.Prototypes;
|
||||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
|
||||||
|
|
||||||
namespace Content.Server.DeviceLinking.Components;
|
namespace Content.Server.DeviceLinking.Components;
|
||||||
|
|
||||||
[RegisterComponent]
|
[RegisterComponent]
|
||||||
public sealed partial class SignalTimerComponent : Component
|
public sealed partial class SignalTimerComponent : Component
|
||||||
{
|
{
|
||||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
[DataField]
|
||||||
public double Delay = 5;
|
public double Delay = 5;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// This shows the Label: text box in the UI.
|
/// This shows the Label: text box in the UI.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
[DataField]
|
||||||
public bool CanEditLabel = true;
|
public bool CanEditLabel = true;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The label, used for TextScreen visuals currently.
|
/// The label, used for TextScreen visuals currently.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
[DataField]
|
||||||
public string Label = string.Empty;
|
public string Label = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Default max width of a label (how many letters can this render?)
|
/// Default max width of a label (how many letters can this render?)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
[DataField]
|
||||||
public int MaxLength = 5;
|
public int MaxLength = 5;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The port that gets signaled when the timer triggers.
|
/// The port that gets signaled when the timer triggers.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
[DataField]
|
||||||
public ProtoId<SourcePortPrototype> TriggerPort = "Timer";
|
public ProtoId<SourcePortPrototype> TriggerPort = "Timer";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The port that gets signaled when the timer starts.
|
/// The port that gets signaled when the timer starts.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
[DataField]
|
||||||
public ProtoId<SourcePortPrototype> StartPort = "Start";
|
public ProtoId<SourcePortPrototype> StartPort = "Start";
|
||||||
|
|
||||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
[DataField]
|
||||||
public ProtoId<SinkPortPrototype> Trigger = "Trigger";
|
public ProtoId<SinkPortPrototype> Trigger = "Trigger";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// If not null, this timer will play this sound when done.
|
/// If not null, this timer will play this sound when done.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
[DataField]
|
||||||
public SoundSpecifier? DoneSound;
|
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))
|
if (!IsMessageValid(uid, args))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
component.Delay = args.Delay.TotalSeconds;
|
component.Delay = Math.Min(args.Delay.TotalSeconds, component.MaxDuration);
|
||||||
_appearanceSystem.SetData(uid, TextScreenVisuals.TargetTime, component.Delay);
|
_appearanceSystem.SetData(uid, TextScreenVisuals.TargetTime, component.Delay);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -135,12 +135,13 @@ namespace Content.Server.Disposal.Unit.EntitySystems
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
_xformSystem.AttachToGridOrMap(entity, xform);
|
_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();
|
var directionAngle = direction.ToAngle();
|
||||||
direction += _xformSystem.GetWorldRotation(parentXform);
|
directionAngle += _xformSystem.GetWorldRotation(gridXform);
|
||||||
_throwing.TryThrow(entity, direction.ToWorldVec() * 3f, 10f);
|
_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]
|
[DataField]
|
||||||
public TimeSpan WarNukieArriveDelay = TimeSpan.FromMinutes(15);
|
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>
|
/// <summary>
|
||||||
/// Minimal operatives count for war declaration
|
/// Minimal operatives count for war declaration
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
using Content.Server.GameTicking.Rules.Components;
|
using Content.Server.GameTicking.Rules.Components;
|
||||||
using Content.Server.GridPreloader;
|
using Content.Server.GridPreloader;
|
||||||
|
using Content.Server.StationEvents.Events;
|
||||||
using Content.Shared.GameTicking.Components;
|
using Content.Shared.GameTicking.Components;
|
||||||
using Robust.Server.GameObjects;
|
using Robust.Server.GameObjects;
|
||||||
using Robust.Server.Maps;
|
using Robust.Server.Maps;
|
||||||
using Robust.Shared.Map;
|
|
||||||
using Robust.Shared.Prototypes;
|
using Robust.Shared.Prototypes;
|
||||||
|
|
||||||
namespace Content.Server.GameTicking.Rules;
|
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 IPrototypeManager _prototypeManager = default!;
|
||||||
[Dependency] private readonly MapSystem _map = default!;
|
[Dependency] private readonly MapSystem _map = default!;
|
||||||
@@ -75,5 +75,7 @@ public sealed class LoadMapRuleSystem : GameRuleSystem<LoadMapRuleComponent>
|
|||||||
|
|
||||||
var ev = new RuleLoadedGridsEvent(mapId, grids);
|
var ev = new RuleLoadedGridsEvent(mapId, grids);
|
||||||
RaiseLocalEvent(uid, ref ev);
|
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
|
// Nukies must wait some time after declaration of war to get on the station
|
||||||
var warTime = Timing.CurTime.Subtract(nukeops.WarDeclaredTime.Value);
|
var warTime = Timing.CurTime.Subtract(nukeops.WarDeclaredTime.Value);
|
||||||
if (warTime < nukeops.WarNukieArriveDelay)
|
if (warTime < nukeops.WarEvacShuttleDisabled)
|
||||||
{
|
{
|
||||||
ev.Cancelled = true;
|
ev.Cancelled = true;
|
||||||
ev.Reason = Loc.GetString("war-ops-shuttle-call-unavailable");
|
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) ||
|
if (!gameTicker.PlayerGameStatuses.TryGetValue(player.UserId, out var playerStatus) ||
|
||||||
playerStatus is not PlayerGameStatus.JoinedGame)
|
playerStatus is not PlayerGameStatus.JoinedGame)
|
||||||
{
|
{
|
||||||
shell.WriteLine("ghost-command-error-lobby");
|
shell.WriteLine(Loc.GetString("ghost-command-error-lobby"));
|
||||||
return;
|
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))
|
if (TryComp<PullableComponent>(ent, out var pull) && _pullingSystem.IsPulled(ent, pull))
|
||||||
_pullingSystem.TryStopPull(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 xform = Transform(ent);
|
||||||
var targetCoords = SelectRandomTileInRange(xform, implant.TeleportRadius);
|
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()
|
private void DoGroupUpdates()
|
||||||
{
|
{
|
||||||
// "Why is there a separate queue for group remakes and node refloods when they both cause eachother"
|
// "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.Humanoid;
|
||||||
using Content.Server.Inventory;
|
using Content.Server.Inventory;
|
||||||
using Content.Server.Mind.Commands;
|
using Content.Server.Mind.Commands;
|
||||||
using Content.Server.Nutrition;
|
|
||||||
using Content.Server.Polymorph.Components;
|
using Content.Server.Polymorph.Components;
|
||||||
using Content.Shared.Actions;
|
using Content.Shared.Actions;
|
||||||
using Content.Shared.Buckle;
|
using Content.Shared.Buckle;
|
||||||
@@ -13,6 +12,7 @@ using Content.Shared.IdentityManagement;
|
|||||||
using Content.Shared.Mind;
|
using Content.Shared.Mind;
|
||||||
using Content.Shared.Mobs.Components;
|
using Content.Shared.Mobs.Components;
|
||||||
using Content.Shared.Mobs.Systems;
|
using Content.Shared.Mobs.Systems;
|
||||||
|
using Content.Shared.Nutrition;
|
||||||
using Content.Shared.Polymorph;
|
using Content.Shared.Polymorph;
|
||||||
using Content.Shared.Popups;
|
using Content.Shared.Popups;
|
||||||
using Robust.Server.Audio;
|
using Robust.Server.Audio;
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
namespace Content.Server.Power.Components
|
namespace Content.Server.Power.Components
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -6,8 +8,29 @@ namespace Content.Server.Power.Components
|
|||||||
[RegisterComponent]
|
[RegisterComponent]
|
||||||
public sealed partial class BatterySelfRechargerComponent : Component
|
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.Server.Power.Components;
|
||||||
using Content.Shared.Examine;
|
using Content.Shared.Examine;
|
||||||
using Content.Shared.Rejuvenate;
|
using Content.Shared.Rejuvenate;
|
||||||
|
using Content.Shared.Timing;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using Robust.Shared.Utility;
|
using Robust.Shared.Utility;
|
||||||
|
using Robust.Shared.Timing;
|
||||||
|
|
||||||
namespace Content.Server.Power.EntitySystems
|
namespace Content.Server.Power.EntitySystems
|
||||||
{
|
{
|
||||||
[UsedImplicitly]
|
[UsedImplicitly]
|
||||||
public sealed class BatterySystem : EntitySystem
|
public sealed class BatterySystem : EntitySystem
|
||||||
{
|
{
|
||||||
|
[Dependency] protected readonly IGameTiming Timing = default!;
|
||||||
|
|
||||||
public override void Initialize()
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
base.Initialize();
|
base.Initialize();
|
||||||
@@ -84,6 +88,14 @@ namespace Content.Server.Power.EntitySystems
|
|||||||
while (query.MoveNext(out var uid, out var comp, out var batt))
|
while (query.MoveNext(out var uid, out var comp, out var batt))
|
||||||
{
|
{
|
||||||
if (!comp.AutoRecharge) continue;
|
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);
|
SetCharge(uid, batt.CurrentCharge + comp.AutoRechargeRate * frameTime, batt);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -100,6 +112,8 @@ namespace Content.Server.Power.EntitySystems
|
|||||||
{
|
{
|
||||||
args.Affected = true;
|
args.Affected = true;
|
||||||
UseCharge(uid, args.EnergyConsumption, component);
|
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)
|
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 newValue = Math.Clamp(0, battery.CurrentCharge - value, battery.MaxCharge);
|
||||||
var delta = newValue - battery.CurrentCharge;
|
var delta = newValue - battery.CurrentCharge;
|
||||||
battery.CurrentCharge = newValue;
|
battery.CurrentCharge = newValue;
|
||||||
|
|
||||||
|
// Apply a cooldown to the entity's self recharge if needed.
|
||||||
|
TrySetChargeCooldown(uid);
|
||||||
|
|
||||||
var ev = new ChargeChangedEvent(battery.CurrentCharge, battery.MaxCharge);
|
var ev = new ChargeChangedEvent(battery.CurrentCharge, battery.MaxCharge);
|
||||||
RaiseLocalEvent(uid, ref ev);
|
RaiseLocalEvent(uid, ref ev);
|
||||||
return delta;
|
return delta;
|
||||||
@@ -139,11 +157,47 @@ namespace Content.Server.Power.EntitySystems
|
|||||||
battery.CurrentCharge = MathHelper.Clamp(value, 0, battery.MaxCharge);
|
battery.CurrentCharge = MathHelper.Clamp(value, 0, battery.MaxCharge);
|
||||||
if (MathHelper.CloseTo(battery.CurrentCharge, old) &&
|
if (MathHelper.CloseTo(battery.CurrentCharge, old) &&
|
||||||
!(old != battery.CurrentCharge && battery.CurrentCharge == battery.MaxCharge))
|
!(old != battery.CurrentCharge && battery.CurrentCharge == battery.MaxCharge))
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var ev = new ChargeChangedEvent(battery.CurrentCharge, battery.MaxCharge);
|
var ev = new ChargeChangedEvent(battery.CurrentCharge, battery.MaxCharge);
|
||||||
RaiseLocalEvent(uid, ref ev);
|
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>
|
/// <summary>
|
||||||
/// If sufficient charge is available on the battery, use it. Otherwise, don't.
|
/// 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 msg = Loc.GetString("laws-notify");
|
||||||
var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", msg));
|
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))
|
if (!TryComp<SiliconLawProviderComponent>(uid, out var lawcomp))
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ namespace Content.Server.Silicons.StationAi;
|
|||||||
public sealed partial class AiVisionWireAction : ComponentWireAction<StationAiVisionComponent>
|
public sealed partial class AiVisionWireAction : ComponentWireAction<StationAiVisionComponent>
|
||||||
{
|
{
|
||||||
public override string Name { get; set; } = "wire-name-ai-vision-light";
|
public override string Name { get; set; } = "wire-name-ai-vision-light";
|
||||||
public override Color Color { get; set; } = Color.DeepSkyBlue;
|
public override Color Color { get; set; } = Color.White;
|
||||||
public override object StatusKey => AirlockWireStatus.AiControlIndicator;
|
public override object StatusKey => AirlockWireStatus.AiVisionIndicator;
|
||||||
|
|
||||||
public override StatusLightState? GetLightState(Wire wire, StationAiVisionComponent component)
|
public override StatusLightState? GetLightState(Wire wire, StationAiVisionComponent component)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Content.Server.Chat.Managers;
|
using Content.Server.Chat.Managers;
|
||||||
|
using Content.Server.Chat.Systems;
|
||||||
using Content.Shared.Chat;
|
using Content.Shared.Chat;
|
||||||
using Content.Shared.Mind;
|
using Content.Shared.Mind;
|
||||||
using Content.Shared.Roles;
|
using Content.Shared.Roles;
|
||||||
@@ -8,6 +9,7 @@ using Content.Shared.StationAi;
|
|||||||
using Robust.Shared.Audio;
|
using Robust.Shared.Audio;
|
||||||
using Robust.Shared.Map.Components;
|
using Robust.Shared.Map.Components;
|
||||||
using Robust.Shared.Player;
|
using Robust.Shared.Player;
|
||||||
|
using static Content.Server.Chat.Systems.ChatSystem;
|
||||||
|
|
||||||
namespace Content.Server.Silicons.StationAi;
|
namespace Content.Server.Silicons.StationAi;
|
||||||
|
|
||||||
@@ -15,11 +17,50 @@ public sealed class StationAiSystem : SharedStationAiSystem
|
|||||||
{
|
{
|
||||||
[Dependency] private readonly IChatManager _chats = default!;
|
[Dependency] private readonly IChatManager _chats = default!;
|
||||||
[Dependency] private readonly EntityLookupSystem _lookup = default!;
|
[Dependency] private readonly EntityLookupSystem _lookup = default!;
|
||||||
|
[Dependency] private readonly SharedTransformSystem _xforms = default!;
|
||||||
[Dependency] private readonly SharedMindSystem _mind = default!;
|
[Dependency] private readonly SharedMindSystem _mind = default!;
|
||||||
[Dependency] private readonly SharedRoleSystem _roles = default!;
|
[Dependency] private readonly SharedRoleSystem _roles = default!;
|
||||||
|
|
||||||
private readonly HashSet<Entity<StationAiCoreComponent>> _ais = new();
|
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)
|
public override bool SetVisionEnabled(Entity<StationAiVisionComponent> entity, bool enabled, bool announce = false)
|
||||||
{
|
{
|
||||||
if (!base.SetVisionEnabled(entity, enabled, announce))
|
if (!base.SetVisionEnabled(entity, enabled, announce))
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ public sealed class ContainmentFieldGeneratorSystem : EntitySystem
|
|||||||
|
|
||||||
SubscribeLocalEvent<ContainmentFieldGeneratorComponent, StartCollideEvent>(HandleGeneratorCollide);
|
SubscribeLocalEvent<ContainmentFieldGeneratorComponent, StartCollideEvent>(HandleGeneratorCollide);
|
||||||
SubscribeLocalEvent<ContainmentFieldGeneratorComponent, ExaminedEvent>(OnExamine);
|
SubscribeLocalEvent<ContainmentFieldGeneratorComponent, ExaminedEvent>(OnExamine);
|
||||||
SubscribeLocalEvent<ContainmentFieldGeneratorComponent, InteractHandEvent>(OnInteract);
|
SubscribeLocalEvent<ContainmentFieldGeneratorComponent, ActivateInWorldEvent>(OnActivate);
|
||||||
SubscribeLocalEvent<ContainmentFieldGeneratorComponent, AnchorStateChangedEvent>(OnAnchorChanged);
|
SubscribeLocalEvent<ContainmentFieldGeneratorComponent, AnchorStateChangedEvent>(OnAnchorChanged);
|
||||||
SubscribeLocalEvent<ContainmentFieldGeneratorComponent, ReAnchorEvent>(OnReanchorEvent);
|
SubscribeLocalEvent<ContainmentFieldGeneratorComponent, ReAnchorEvent>(OnReanchorEvent);
|
||||||
SubscribeLocalEvent<ContainmentFieldGeneratorComponent, UnanchorAttemptEvent>(OnUnanchorAttempt);
|
SubscribeLocalEvent<ContainmentFieldGeneratorComponent, UnanchorAttemptEvent>(OnUnanchorAttempt);
|
||||||
@@ -90,7 +90,7 @@ public sealed class ContainmentFieldGeneratorSystem : EntitySystem
|
|||||||
args.PushMarkup(Loc.GetString("comp-containment-off"));
|
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)
|
if (args.Handled)
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ namespace Content.Server.Singularity.EntitySystems
|
|||||||
|
|
||||||
SubscribeLocalEvent<EmitterComponent, PowerConsumerReceivedChanged>(ReceivedChanged);
|
SubscribeLocalEvent<EmitterComponent, PowerConsumerReceivedChanged>(ReceivedChanged);
|
||||||
SubscribeLocalEvent<EmitterComponent, PowerChangedEvent>(OnApcChanged);
|
SubscribeLocalEvent<EmitterComponent, PowerChangedEvent>(OnApcChanged);
|
||||||
SubscribeLocalEvent<EmitterComponent, InteractHandEvent>(OnInteractHand);
|
SubscribeLocalEvent<EmitterComponent, ActivateInWorldEvent>(OnActivate);
|
||||||
SubscribeLocalEvent<EmitterComponent, GetVerbsEvent<Verb>>(OnGetVerb);
|
SubscribeLocalEvent<EmitterComponent, GetVerbsEvent<Verb>>(OnGetVerb);
|
||||||
SubscribeLocalEvent<EmitterComponent, ExaminedEvent>(OnExamined);
|
SubscribeLocalEvent<EmitterComponent, ExaminedEvent>(OnExamined);
|
||||||
SubscribeLocalEvent<EmitterComponent, AnchorStateChangedEvent>(OnAnchorStateChanged);
|
SubscribeLocalEvent<EmitterComponent, AnchorStateChangedEvent>(OnAnchorStateChanged);
|
||||||
@@ -60,7 +60,7 @@ namespace Content.Server.Singularity.EntitySystems
|
|||||||
SwitchOff(uid, component);
|
SwitchOff(uid, component);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnInteractHand(EntityUid uid, EmitterComponent component, InteractHandEvent args)
|
private void OnActivate(EntityUid uid, EmitterComponent component, ActivateInWorldEvent args)
|
||||||
{
|
{
|
||||||
if (args.Handled)
|
if (args.Handled)
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ public sealed class RadiationCollectorSystem : EntitySystem
|
|||||||
public override void Initialize()
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
base.Initialize();
|
base.Initialize();
|
||||||
SubscribeLocalEvent<RadiationCollectorComponent, InteractHandEvent>(OnInteractHand);
|
SubscribeLocalEvent<RadiationCollectorComponent, ActivateInWorldEvent>(OnActivate);
|
||||||
SubscribeLocalEvent<RadiationCollectorComponent, OnIrradiatedEvent>(OnRadiation);
|
SubscribeLocalEvent<RadiationCollectorComponent, OnIrradiatedEvent>(OnRadiation);
|
||||||
SubscribeLocalEvent<RadiationCollectorComponent, ExaminedEvent>(OnExamined);
|
SubscribeLocalEvent<RadiationCollectorComponent, ExaminedEvent>(OnExamined);
|
||||||
SubscribeLocalEvent<RadiationCollectorComponent, GasAnalyzerScanEvent>(OnAnalyzed);
|
SubscribeLocalEvent<RadiationCollectorComponent, GasAnalyzerScanEvent>(OnAnalyzed);
|
||||||
@@ -65,7 +65,7 @@ public sealed class RadiationCollectorSystem : EntitySystem
|
|||||||
UpdateTankAppearance(uid, component, gasTank);
|
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))
|
if (TryComp(uid, out UseDelayComponent? useDelay) && !_useDelay.TryResetDelay((uid, useDelay), true))
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using Content.Shared.Doors.Components;
|
|||||||
using Content.Shared.Doors.Systems;
|
using Content.Shared.Doors.Systems;
|
||||||
using Content.Shared.Lock;
|
using Content.Shared.Lock;
|
||||||
using Content.Shared.GameTicking.Components;
|
using Content.Shared.GameTicking.Components;
|
||||||
|
using Content.Shared.Station.Components;
|
||||||
using Robust.Shared.Prototypes;
|
using Robust.Shared.Prototypes;
|
||||||
using Robust.Shared.Random;
|
using Robust.Shared.Random;
|
||||||
|
|
||||||
@@ -43,6 +44,9 @@ public sealed class GreytideVirusRule : StationEventSystem<GreytideVirusRuleComp
|
|||||||
if (virusComp.Severity == null)
|
if (virusComp.Severity == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
if (!TryGetRandomStation(out var chosenStation))
|
||||||
|
return;
|
||||||
|
|
||||||
// pick random access groups
|
// pick random access groups
|
||||||
var chosen = _random.GetItems(virusComp.AccessGroups, virusComp.Severity.Value, allowDuplicates: false);
|
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 firelockQuery = GetEntityQuery<FirelockComponent>();
|
||||||
var accessQuery = GetEntityQuery<AccessReaderComponent>();
|
var accessQuery = GetEntityQuery<AccessReaderComponent>();
|
||||||
|
|
||||||
var lockQuery = AllEntityQuery<LockComponent>();
|
var lockQuery = AllEntityQuery<LockComponent, TransformComponent>();
|
||||||
while (lockQuery.MoveNext(out var lockUid, out var lockComp))
|
while (lockQuery.MoveNext(out var lockUid, out var lockComp, out var xform))
|
||||||
{
|
{
|
||||||
if (!accessQuery.TryComp(lockUid, out var accessComp))
|
if (!accessQuery.TryComp(lockUid, out var accessComp))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
// make sure not to hit CentCom or other maps
|
||||||
|
if (CompOrNull<StationMemberComponent>(xform.GridUid)?.Station != chosenStation)
|
||||||
|
continue;
|
||||||
|
|
||||||
// check access
|
// check access
|
||||||
// the AreAccessTagsAllowed function is a little weird because it technically has support for certain tags to be locked out of opening something
|
// 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)
|
// 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);
|
_lock.Unlock(lockUid, null, lockComp);
|
||||||
}
|
}
|
||||||
|
|
||||||
var airlockQuery = AllEntityQuery<AirlockComponent, DoorComponent>();
|
var airlockQuery = AllEntityQuery<AirlockComponent, DoorComponent, TransformComponent>();
|
||||||
while (airlockQuery.MoveNext(out var airlockUid, out var airlockComp, out var doorComp))
|
while (airlockQuery.MoveNext(out var airlockUid, out var airlockComp, out var doorComp, out var xform))
|
||||||
{
|
{
|
||||||
// don't space everything
|
// don't space everything
|
||||||
if (firelockQuery.HasComp(airlockUid))
|
if (firelockQuery.HasComp(airlockUid))
|
||||||
continue;
|
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
|
// use the access reader from the door electronics if they exist
|
||||||
if (!_access.GetMainAccessReader(airlockUid, out var accessComp))
|
if (!_access.GetMainAccessReader(airlockUid, out var accessComp))
|
||||||
continue;
|
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.Chat.Systems;
|
||||||
using Content.Server.Emoting.Systems;
|
using Content.Server.Emoting.Systems;
|
||||||
using Content.Server.Speech.EntitySystems;
|
using Content.Server.Speech.EntitySystems;
|
||||||
|
using Content.Shared.Anomaly.Components;
|
||||||
using Content.Shared.Bed.Sleep;
|
using Content.Shared.Bed.Sleep;
|
||||||
using Content.Shared.Cloning;
|
using Content.Shared.Cloning;
|
||||||
using Content.Shared.Damage;
|
using Content.Shared.Damage;
|
||||||
@@ -64,10 +65,19 @@ namespace Content.Server.Zombies
|
|||||||
SubscribeLocalEvent<ZombieComponent, GetCharactedDeadIcEvent>(OnGetCharacterDeadIC);
|
SubscribeLocalEvent<ZombieComponent, GetCharactedDeadIcEvent>(OnGetCharacterDeadIC);
|
||||||
|
|
||||||
SubscribeLocalEvent<PendingZombieComponent, MapInitEvent>(OnPendingMapInit);
|
SubscribeLocalEvent<PendingZombieComponent, MapInitEvent>(OnPendingMapInit);
|
||||||
|
SubscribeLocalEvent<PendingZombieComponent, BeforeRemoveAnomalyOnDeathEvent>(OnBeforeRemoveAnomalyOnDeath);
|
||||||
|
|
||||||
SubscribeLocalEvent<IncurableZombieComponent, MapInitEvent>(OnPendingMapInit);
|
SubscribeLocalEvent<IncurableZombieComponent, MapInitEvent>(OnPendingMapInit);
|
||||||
|
|
||||||
SubscribeLocalEvent<ZombifyOnDeathComponent, MobStateChangedEvent>(OnDamageChanged);
|
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)
|
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.Components;
|
||||||
|
using Content.Shared.Chemistry.EntitySystems;
|
||||||
using Content.Shared.DoAfter;
|
using Content.Shared.DoAfter;
|
||||||
|
using Content.Shared.Examine;
|
||||||
using Content.Shared.IdentityManagement;
|
using Content.Shared.IdentityManagement;
|
||||||
using Content.Shared.Mobs.Systems;
|
using Content.Shared.Mobs.Systems;
|
||||||
using Content.Shared.Nutrition.Components;
|
using Content.Shared.Nutrition.Components;
|
||||||
@@ -12,18 +11,17 @@ using Content.Shared.Udder;
|
|||||||
using Content.Shared.Verbs;
|
using Content.Shared.Verbs;
|
||||||
using Robust.Shared.Timing;
|
using Robust.Shared.Timing;
|
||||||
|
|
||||||
namespace Content.Server.Animals.Systems;
|
namespace Content.Shared.Animals;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gives ability to produce milkable reagents, produces endless if the
|
/// Gives the ability to produce milkable reagents;
|
||||||
/// owner has no HungerComponent
|
/// produces endlessly if the owner does not have a HungerComponent.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class UdderSystem : EntitySystem
|
public sealed class UdderSystem : EntitySystem
|
||||||
{
|
{
|
||||||
[Dependency] private readonly HungerSystem _hunger = default!;
|
[Dependency] private readonly HungerSystem _hunger = default!;
|
||||||
[Dependency] private readonly IGameTiming _timing = default!;
|
[Dependency] private readonly IGameTiming _timing = default!;
|
||||||
[Dependency] private readonly MobStateSystem _mobState = 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 SharedDoAfterSystem _doAfterSystem = default!;
|
||||||
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
|
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
|
||||||
|
|
||||||
@@ -31,26 +29,37 @@ internal sealed class UdderSystem : EntitySystem
|
|||||||
{
|
{
|
||||||
base.Initialize();
|
base.Initialize();
|
||||||
|
|
||||||
|
SubscribeLocalEvent<UdderComponent, MapInitEvent>(OnMapInit);
|
||||||
SubscribeLocalEvent<UdderComponent, GetVerbsEvent<AlternativeVerb>>(AddMilkVerb);
|
SubscribeLocalEvent<UdderComponent, GetVerbsEvent<AlternativeVerb>>(AddMilkVerb);
|
||||||
SubscribeLocalEvent<UdderComponent, MilkingDoAfterEvent>(OnDoAfter);
|
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)
|
public override void Update(float frameTime)
|
||||||
{
|
{
|
||||||
base.Update(frameTime);
|
base.Update(frameTime);
|
||||||
|
|
||||||
var query = EntityQueryEnumerator<UdderComponent>();
|
var query = EntityQueryEnumerator<UdderComponent>();
|
||||||
var now = _timing.CurTime;
|
|
||||||
while (query.MoveNext(out var uid, out var udder))
|
while (query.MoveNext(out var uid, out var udder))
|
||||||
{
|
{
|
||||||
if (now < udder.NextGrowth)
|
if (_timing.CurTime < udder.NextGrowth)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
udder.NextGrowth = now + udder.GrowthDelay;
|
udder.NextGrowth += udder.GrowthDelay;
|
||||||
|
|
||||||
if (_mobState.IsDead(uid))
|
if (_mobState.IsDead(uid))
|
||||||
continue;
|
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"
|
// Actually there is food digestion so no problem with instant reagent generation "OnFeed"
|
||||||
if (EntityManager.TryGetComponent(uid, out HungerComponent? hunger))
|
if (EntityManager.TryGetComponent(uid, out HungerComponent? hunger))
|
||||||
{
|
{
|
||||||
@@ -61,9 +70,6 @@ internal sealed class UdderSystem : EntitySystem
|
|||||||
_hunger.ModifyHunger(uid, -udder.HungerUsage, hunger);
|
_hunger.ModifyHunger(uid, -udder.HungerUsage, hunger);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_solutionContainerSystem.ResolveSolution(uid, udder.SolutionName, ref udder.Solution))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
//TODO: toxins from bloodstream !?
|
//TODO: toxins from bloodstream !?
|
||||||
_solutionContainerSystem.TryAddReagent(udder.Solution.Value, udder.ReagentId, udder.QuantityPerUpdate, out _);
|
_solutionContainerSystem.TryAddReagent(udder.Solution.Value, udder.ReagentId, udder.QuantityPerUpdate, out _);
|
||||||
}
|
}
|
||||||
@@ -99,7 +105,7 @@ internal sealed class UdderSystem : EntitySystem
|
|||||||
var quantity = solution.Volume;
|
var quantity = solution.Volume;
|
||||||
if (quantity == 0)
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,7 +115,7 @@ internal sealed class UdderSystem : EntitySystem
|
|||||||
var split = _solutionContainerSystem.SplitSolution(entity.Comp.Solution.Value, quantity);
|
var split = _solutionContainerSystem.SplitSolution(entity.Comp.Solution.Value, quantity);
|
||||||
_solutionContainerSystem.TryAddSolution(targetSoln.Value, split);
|
_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);
|
args.Args.User, PopupType.Medium);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,4 +140,50 @@ internal sealed class UdderSystem : EntitySystem
|
|||||||
};
|
};
|
||||||
args.Verbs.Add(verb);
|
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.Components;
|
||||||
using Content.Shared.Chemistry.Reagent;
|
using Content.Shared.Chemistry.Reagent;
|
||||||
using Content.Shared.FixedPoint;
|
using Content.Shared.FixedPoint;
|
||||||
|
using Robust.Shared.GameStates;
|
||||||
using Robust.Shared.Prototypes;
|
using Robust.Shared.Prototypes;
|
||||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
|
|
||||||
|
|
||||||
namespace Content.Server.Animals.Components;
|
namespace Content.Shared.Animals;
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
|
[RegisterComponent, AutoGenerateComponentState, AutoGenerateComponentPause, NetworkedComponent]
|
||||||
[RegisterComponent, Access(typeof(WoolySystem))]
|
|
||||||
public sealed partial class WoolyComponent : Component
|
public sealed partial class WoolyComponent : Component
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The reagent to grow.
|
/// The reagent to grow.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[DataField, ViewVariables(VVAccess.ReadOnly)]
|
[DataField, AutoNetworkedField]
|
||||||
public ProtoId<ReagentPrototype> ReagentId = "Fiber";
|
public ProtoId<ReagentPrototype> ReagentId = "Fiber";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -29,30 +28,30 @@ public sealed partial class WoolyComponent : Component
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The solution to add reagent to.
|
/// The solution to add reagent to.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[DataField]
|
[DataField, ViewVariables(VVAccess.ReadOnly)]
|
||||||
public Entity<SolutionComponent>? Solution;
|
public Entity<SolutionComponent>? Solution;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The amount of reagent to be generated on update.
|
/// The amount of reagent to be generated on update.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[DataField, ViewVariables(VVAccess.ReadOnly)]
|
[DataField, AutoNetworkedField]
|
||||||
public FixedPoint2 Quantity = 25;
|
public FixedPoint2 Quantity = 25;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The amount of nutrient consumed on update.
|
/// The amount of nutrient consumed on update.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
[DataField, AutoNetworkedField]
|
||||||
public float HungerUsage = 10f;
|
public float HungerUsage = 10f;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// How long to wait before growing wool.
|
/// How long to wait before growing wool.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
[DataField, AutoNetworkedField]
|
||||||
public TimeSpan GrowthDelay = TimeSpan.FromMinutes(1);
|
public TimeSpan GrowthDelay = TimeSpan.FromMinutes(1);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// When to next try growing wool.
|
/// When to next try growing wool.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite)]
|
[DataField, AutoPausedField, Access(typeof(WoolySystem))]
|
||||||
public TimeSpan NextGrowth = TimeSpan.FromSeconds(0);
|
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.Chemistry.EntitySystems;
|
||||||
using Content.Shared.Mobs.Systems;
|
using Content.Shared.Mobs.Systems;
|
||||||
|
using Content.Shared.Nutrition;
|
||||||
using Content.Shared.Nutrition.Components;
|
using Content.Shared.Nutrition.Components;
|
||||||
using Content.Shared.Nutrition.EntitySystems;
|
using Content.Shared.Nutrition.EntitySystems;
|
||||||
using Robust.Shared.Timing;
|
using Robust.Shared.Timing;
|
||||||
|
|
||||||
namespace Content.Server.Animals.Systems;
|
namespace Content.Shared.Animals;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gives ability to produce fiber reagents, produces endless if the
|
/// Gives ability to produce fiber reagents;
|
||||||
/// owner has no HungerComponent
|
/// produces endlessly if the owner has no HungerComponent.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class WoolySystem : EntitySystem
|
public sealed class WoolySystem : EntitySystem
|
||||||
{
|
{
|
||||||
@@ -24,6 +23,12 @@ public sealed class WoolySystem : EntitySystem
|
|||||||
base.Initialize();
|
base.Initialize();
|
||||||
|
|
||||||
SubscribeLocalEvent<WoolyComponent, BeforeFullyEatenEvent>(OnBeforeFullyEaten);
|
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)
|
public override void Update(float frameTime)
|
||||||
@@ -31,17 +36,22 @@ public sealed class WoolySystem : EntitySystem
|
|||||||
base.Update(frameTime);
|
base.Update(frameTime);
|
||||||
|
|
||||||
var query = EntityQueryEnumerator<WoolyComponent>();
|
var query = EntityQueryEnumerator<WoolyComponent>();
|
||||||
var now = _timing.CurTime;
|
|
||||||
while (query.MoveNext(out var uid, out var wooly))
|
while (query.MoveNext(out var uid, out var wooly))
|
||||||
{
|
{
|
||||||
if (now < wooly.NextGrowth)
|
if (_timing.CurTime < wooly.NextGrowth)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
wooly.NextGrowth = now + wooly.GrowthDelay;
|
wooly.NextGrowth += wooly.GrowthDelay;
|
||||||
|
|
||||||
if (_mobState.IsDead(uid))
|
if (_mobState.IsDead(uid))
|
||||||
continue;
|
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"
|
// Actually there is food digestion so no problem with instant reagent generation "OnFeed"
|
||||||
if (EntityManager.TryGetComponent(uid, out HungerComponent? hunger))
|
if (EntityManager.TryGetComponent(uid, out HungerComponent? hunger))
|
||||||
{
|
{
|
||||||
@@ -52,9 +62,6 @@ public sealed class WoolySystem : EntitySystem
|
|||||||
_hunger.ModifyHunger(uid, -wooly.HungerUsage, hunger);
|
_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 _);
|
_solutionContainer.TryAddReagent(wooly.Solution.Value, wooly.ReagentId, wooly.Quantity, out _);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -70,3 +70,11 @@ public sealed partial class InnerBodyAnomalyComponent : Component
|
|||||||
[DataField]
|
[DataField]
|
||||||
public string LayerMap = "inner_anomaly_layer";
|
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