Compare commits

..

17 Commits

Author SHA1 Message Date
comasqw
fff4287ad5 ревёрт "demiplane modifiers", всё же нужно делать биом 2024-12-12 01:07:35 +04:00
comasqw
d1e4501b6f Merge branch 'master' into MagicWands 2024-12-12 00:59:47 +04:00
comasqw
fc4f51f0dd demiplane modifiers 2024-12-12 00:58:55 +04:00
comasqw
06cb93fb1d recipes for workbench 2024-12-11 16:34:09 +04:00
comasqw
14dbb5d3ed added all modular parts 2024-12-11 13:27:33 +04:00
comasqw
337d248884 added magic crystals for all spells 2024-12-11 00:15:19 +04:00
comasqw
596c329c96 new staff 2024-12-10 17:37:43 +04:00
comasqw
bae34b8318 bup 2024-12-10 01:58:47 +04:00
comasqw
e7e4e61972 magic artifacts 2024-12-10 01:57:41 +04:00
comasqw
fc90485838 Merge branch 'master' into MagicWands 2024-12-09 21:35:22 +04:00
comasqw
ac5579c7fd added base old magic scroll 2024-12-09 21:34:28 +04:00
comasqw
cd618efc31 update crystals modalrPart structure 2024-12-09 20:59:59 +04:00
comasqw
6b41aa71ca change ManacostModify 2024-12-05 01:23:47 +04:00
comasqw
a4395f2066 Merge branch 'master' into MagicWands 2024-12-05 00:46:27 +04:00
comasqw
685a9616ab много чего 2024-12-05 00:41:23 +04:00
comasqw
86168288a3 Merge branch 'master' into MagicWands 2024-12-02 12:34:39 +04:00
comasqw
81dd06ac2c init 2024-12-02 12:31:07 +04:00
1948 changed files with 219022 additions and 671468 deletions

37
.github/CODEOWNERS vendored
View File

@@ -2,30 +2,49 @@
# 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
/Resources/ConfigPresets/WizardsDen/ @nikthechampiongr @crazybrain23 # Moony's Gargantuan List Of Things She Cares About, or MGLOTSCA for short.
/Content.*/Administration/ @DrSmugleaf @nikthechampiongr @crazybrain23 # You need to add your name to these entries, not make a new one, if you care about them.
/Resources/ServerInfo/ @nikthechampiongr @crazybrain23 /Content.*/Toolshed/ @moonheart08
/Resources/ServerInfo/Guidebook/ServerRules/ @nikthechampiongr @crazybrain23 **/Toolshed/** @moonheart08
*Command.cs @moonheart08
/Content.*/Administration/ @moonheart08 @DrSmugleaf @Chief-Engineer
/Content.*/Station/ @moonheart08
/Content.*/Maps/ @moonheart08
/Content.*/GameTicking/ @moonheart08 @EmoGarbage404
/Resources/ServerInfo/ @moonheart08 @Chief-Engineer
/Resources/ServerInfo/Guidebook/ @moonheart08 @EmoGarbage404
/Resources/ServerInfo/Guidebook/ServerRules/ @Chief-Engineer
/Resources/engineCommandPerms.yml @moonheart08 @Chief-Engineer
/Resources/clientCommandPerms.yml @moonheart08 @Chief-Engineer
/Resources/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 @nikthechampiongr @crazybrain23 /Resources/Prototypes/Guidebook/rules.yml @Chief-Engineer
/Content.*/Body/ @DrSmugleaf /Content.*/Body/ @DrSmugleaf
/Content.YAMLLinter @DrSmugleaf /Content.YAMLLinter @DrSmugleaf
/Content.Shared/Damage/ @DrSmugleaf /Content.Shared/Damage/ @DrSmugleaf
/Content.*/Anomaly/ @TheShuEd /Content.*/Anomaly/ @EmoGarbage404 @TheShuEd
/Resources/Prototypes/Entities/Structures/Specific/anomalies.yml @TheShuEd /Content.*/Lathe/ @EmoGarbage404
/Content.*/Materials/ @EmoGarbage404
/Content.*/Mech/ @EmoGarbage404
/Content.*/Research/ @EmoGarbage404
/Content.*/Stack/ @EmoGarbage404
/Content.*/Xenoarchaeology/ @EmoGarbage404
/Content.*/Zombies/ @EmoGarbage404
/Resources/Prototypes/Entities/Structures/Specific/anomalies.yml @EmoGarbage404 @TheShuEd
/Resources/Prototypes/Research/ @EmoGarbage404
/Content.*/Forensics/ @ficcialfaint /Content.*/Forensics/ @ficcialfaint
# SKREEEE # SKREEEE
/Content.*.Database/ @PJB3005 @DrSmugleaf /Content.*.Database/ @PJB3005 @DrSmugleaf
/Content.Shared.Database/Log*.cs @PJB3005 @DrSmugleaf @nikthechampiongr @crazybrain23 /Content.Shared.Database/Log*.cs @PJB3005 @DrSmugleaf @Chief-Engineer
/Pow3r/ @PJB3005 /Pow3r/ @PJB3005
/Content.Server/Power/Pow3r/ @PJB3005 /Content.Server/Power/Pow3r/ @PJB3005
@@ -33,7 +52,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
View File

@@ -16,10 +16,6 @@
- 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.

View File

@@ -2,11 +2,11 @@
on: on:
push: push:
branches: [ master, staging, stable ] branches: [ master, staging, trying ]
merge_group: merge_group:
pull_request: pull_request:
types: [ opened, reopened, synchronize, ready_for_review ] types: [ opened, reopened, synchronize, ready_for_review ]
branches: [ master, staging, stable ] branches: [ master ]
jobs: jobs:
build: build:

View File

@@ -2,11 +2,11 @@ name: Build & Test Debug
on: on:
push: push:
branches: [ master, staging, stable ] branches: [ master, staging, trying ]
merge_group: merge_group:
pull_request: pull_request:
types: [ opened, reopened, synchronize, ready_for_review ] types: [ opened, reopened, synchronize, ready_for_review ]
branches: [ master, staging, stable ] branches: [ master ]
jobs: jobs:
build: build:

View File

@@ -14,7 +14,7 @@ jobs:
{ {
"0": "XS", "0": "XS",
"10": "S", "10": "S",
"100": "M", "30": "M",
"1000": "L", "100": "L",
"5000": "XL" "1000": "XL"
} }

View File

@@ -2,7 +2,7 @@
on: on:
push: push:
branches: [ master, staging, stable ] branches: [ master, staging, trying ]
paths: paths:
- '**.cs' - '**.cs'
- '**.csproj' - '**.csproj'
@@ -16,7 +16,7 @@ on:
merge_group: merge_group:
pull_request: pull_request:
types: [ opened, reopened, synchronize, ready_for_review ] types: [ opened, reopened, synchronize, ready_for_review ]
branches: [ master, staging, stable ] branches: [ master ]
paths: paths:
- '**.cs' - '**.cs'
- '**.csproj' - '**.csproj'

View File

@@ -1,7 +1,7 @@
name: RGA schema validator name: RGA schema validator
on: on:
push: push:
branches: [ master, staging, stable ] branches: [ master, staging, trying ]
merge_group: merge_group:
pull_request: pull_request:
types: [ opened, reopened, synchronize, ready_for_review ] types: [ opened, reopened, synchronize, ready_for_review ]

View File

@@ -2,7 +2,7 @@ name: RSI Validator
on: on:
push: push:
branches: [ master, staging, stable ] branches: [ staging, trying ]
merge_group: merge_group:
pull_request: pull_request:
paths: paths:

View File

@@ -1,7 +1,7 @@
name: Map file schema validator name: Map file schema validator
on: on:
push: push:
branches: [ master, staging, stable ] branches: [ master, staging, trying ]
merge_group: merge_group:
pull_request: pull_request:
types: [ opened, reopened, synchronize, ready_for_review ] types: [ opened, reopened, synchronize, ready_for_review ]

View File

@@ -2,7 +2,7 @@ name: YAML Linter
on: on:
push: push:
branches: [ master, staging, stable ] branches: [ master, staging, trying ]
merge_group: merge_group:
pull_request: pull_request:
types: [ opened, reopened, synchronize, ready_for_review ] types: [ opened, reopened, synchronize, ready_for_review ]

View File

@@ -31,6 +31,19 @@ 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);
@@ -149,11 +162,12 @@ 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", gasAbbreviation), ("value", gasPercent)), Text = Loc.GetString("atmos-alerts-window-other-gases-value", ("shorthand", gasShorthand), ("value", gasPercent)),
FontOverride = normalFont, FontOverride = normalFont,
FontColorOverride = GetAlarmStateColor(alert), FontColorOverride = GetAlarmStateColor(alert),
HorizontalAlignment = HAlignment.Center, HorizontalAlignment = HAlignment.Center,

View File

@@ -1,40 +0,0 @@
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();
}
}

View File

@@ -1,295 +0,0 @@
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;
}
}

View File

@@ -1,69 +0,0 @@
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;
}
}
}

View File

@@ -1,99 +0,0 @@
<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>

View File

@@ -1,455 +0,0 @@
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);
}
}
}

View File

@@ -1,74 +0,0 @@
<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>

View File

@@ -1,166 +0,0 @@
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;
}
}

View File

@@ -1,23 +0,0 @@
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();
}
}
}

View File

@@ -1,63 +1,65 @@
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
{ {
[ViewVariables] /// <summary>
private const float MaxPressure = Atmospherics.MaxOutputPressure; /// Initializes a <see cref="GasPressurePumpWindow"/> and updates it when new server messages are received.
/// </summary>
[ViewVariables] [UsedImplicitly]
private GasPressurePumpWindow? _window; public sealed class GasPressurePumpBoundUserInterface : BoundUserInterface
public GasPressurePumpBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{ {
} [ViewVariables]
private const float MaxPressure = Atmospherics.MaxOutputPressure;
protected override void Open() [ViewVariables]
{ private GasPressurePumpWindow? _window;
base.Open();
_window = this.CreateWindow<GasPressurePumpWindow>(); public GasPressurePumpBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
}
_window.ToggleStatusButtonPressed += OnToggleStatusButtonPressed; protected override void Open()
_window.PumpOutputPressureChanged += OnPumpOutputPressurePressed; {
Update(); base.Open();
}
public void Update() _window = this.CreateWindow<GasPressurePumpWindow>();
{
if (_window == null)
return;
_window.Title = Identity.Name(Owner, EntMan); _window.ToggleStatusButtonPressed += OnToggleStatusButtonPressed;
_window.PumpOutputPressureChanged += OnPumpOutputPressurePressed;
}
if (!EntMan.TryGetComponent(Owner, out GasPressurePumpComponent? pump)) private void OnToggleStatusButtonPressed()
return; {
if (_window is null) return;
SendMessage(new GasPressurePumpToggleStatusMessage(_window.PumpStatus));
}
_window.SetPumpStatus(pump.Enabled); private void OnPumpOutputPressurePressed(string value)
_window.MaxPressure = pump.MaxTargetPressure; {
_window.SetOutputPressure(pump.TargetPressure); var pressure = UserInputParser.TryFloat(value, out var parsed) ? parsed : 0f;
} if (pressure > MaxPressure) pressure = MaxPressure;
private void OnToggleStatusButtonPressed() SendMessage(new GasPressurePumpChangeOutputPressureMessage(pressure));
{ }
if (_window is null) return;
SendPredictedMessage(new GasPressurePumpToggleStatusMessage(_window.PumpStatus));
}
private void OnPumpOutputPressurePressed(float value) /// <summary>
{ /// Update the UI state based on server-sent info
SendPredictedMessage(new GasPressurePumpChangeOutputPressureMessage(value)); /// </summary>
/// <param name="state"></param>
protected override void UpdateState(BoundUserInterfaceState state)
{
base.UpdateState(state);
if (_window == null || state is not GasPressurePumpBoundUserInterfaceState cast)
return;
_window.Title = (cast.PumpLabel);
_window.SetPumpStatus(cast.Enabled);
_window.SetOutputPressure(cast.OutputPressure);
}
} }
} }

View File

@@ -1,18 +1,22 @@
<controls:FancyWindow xmlns="https://spacestation14.io" <DefaultWindow xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls" MinSize="200 120" Title="Pressure Pump">
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}" Margin="0 0 5 0"/> <Label Text="{Loc comp-gas-pump-ui-pump-status}"/>
<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}"/>
<FloatSpinBox HorizontalExpand="True" Name="PumpPressureOutputInput" MinSize="70 0" /> <Control MinSize="5 0" />
<LineEdit Name="PumpPressureOutputInput" MinSize="70 0" />
<Control MinSize="5 0" />
<Button Name="SetMaxPressureButton" Text="{Loc comp-gas-pump-ui-pump-set-max}" />
<Control MinSize="5 0" />
<Control HorizontalExpand="True" />
<Button Name="SetOutputPressureButton" Text="{Loc comp-gas-pump-ui-pump-set-rate}" HorizontalAlignment="Right" Disabled="True"/>
</BoxContainer> </BoxContainer>
</BoxContainer> </BoxContainer>
</controls:FancyWindow> </DefaultWindow>

View File

@@ -1,8 +1,14 @@
using Content.Client.UserInterface.Controls; using System;
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
{ {
@@ -10,25 +16,12 @@ 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 : FancyWindow public sealed partial class GasPressurePumpWindow : DefaultWindow
{ {
public bool PumpStatus = true; public bool PumpStatus = true;
public event Action? ToggleStatusButtonPressed; public event Action? ToggleStatusButtonPressed;
public event Action<float>? PumpOutputPressureChanged; public event Action<string>? 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()
{ {
@@ -37,25 +30,23 @@ namespace Content.Client.Atmos.UI
ToggleStatusButton.OnPressed += _ => SetPumpStatus(!PumpStatus); ToggleStatusButton.OnPressed += _ => SetPumpStatus(!PumpStatus);
ToggleStatusButton.OnPressed += _ => ToggleStatusButtonPressed?.Invoke(); ToggleStatusButton.OnPressed += _ => ToggleStatusButtonPressed?.Invoke();
PumpPressureOutputInput.OnValueChanged += _ => SetOutputPressureButton.Disabled = false; PumpPressureOutputInput.OnTextChanged += _ => SetOutputPressureButton.Disabled = false;
SetOutputPressureButton.OnPressed += _ => SetOutputPressureButton.OnPressed += _ =>
{ {
PumpPressureOutputInput.Value = Math.Clamp(PumpPressureOutputInput.Value, 0f, _maxPressure); PumpOutputPressureChanged?.Invoke(PumpPressureOutputInput.Text ??= "");
PumpOutputPressureChanged?.Invoke(PumpPressureOutputInput.Value);
SetOutputPressureButton.Disabled = true; SetOutputPressureButton.Disabled = true;
}; };
SetMaxPressureButton.OnPressed += _ => SetMaxPressureButton.OnPressed += _ =>
{ {
PumpPressureOutputInput.Value = _maxPressure; PumpPressureOutputInput.Text = Atmospherics.MaxOutputPressure.ToString(CultureInfo.CurrentCulture);
SetOutputPressureButton.Disabled = false; SetOutputPressureButton.Disabled = false;
}; };
} }
public void SetOutputPressure(float pressure) public void SetOutputPressure(float pressure)
{ {
PumpPressureOutputInput.Value = pressure; PumpPressureOutputInput.Text = pressure.ToString(CultureInfo.CurrentCulture);
} }
public void SetPumpStatus(bool enabled) public void SetPumpStatus(bool enabled)

View File

@@ -2,7 +2,6 @@ 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;
@@ -142,12 +141,7 @@ namespace Content.Client.Chat.UI
Modulate = Color.White; Modulate = Color.White;
} }
var baseOffset = 0f; var offset = (-_eyeManager.CurrentEye.Rotation).ToWorldVec() * -EntityVerticalOffset;
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;

View File

@@ -12,7 +12,6 @@ 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
@@ -91,40 +90,10 @@ 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 reagentTransferButton = new ReagentButton(text, amount, id, isBuffer, styleClass); var button = new ReagentButton(text, amount, id, isBuffer, styleClass);
reagentTransferButton.OnPressed += args button.OnPressed += args
=> OnReagentButtonPressed?.Invoke(args, reagentTransferButton); => OnReagentButtonPressed?.Invoke(args, button);
return reagentTransferButton; return button;
}
/// <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>
@@ -133,36 +102,25 @@ 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);
// Ensure the Panel Info is updated, including UI elements for Buffer Volume, Output Container and so on
UpdatePanelInfo(castState); UpdatePanelInfo(castState);
BufferCurrentVolume.Text = $" {castState.BufferCurrentVolume?.Int() ?? 0}u";
InputEjectButton.Disabled = castState.InputContainerInfo is null;
OutputEjectButton.Disabled = castState.OutputContainerInfo is null;
CreateBottleButton.Disabled = castState.OutputContainerInfo?.Reagents == null;
CreatePillButton.Disabled = castState.OutputContainerInfo?.Entities == null;
UpdateDosageFields(castState);
}
//assign default values for pill and bottle fields.
private void UpdateDosageFields(ChemMasterBoundUserInterfaceState castState)
{
var output = castState.OutputContainerInfo; var output = castState.OutputContainerInfo;
BufferCurrentVolume.Text = $" {castState.BufferCurrentVolume?.Int() ?? 0}u";
InputEjectButton.Disabled = castState.InputContainerInfo is null;
OutputEjectButton.Disabled = output is null;
CreateBottleButton.Disabled = output?.Reagents == null;
CreatePillButton.Disabled = output?.Entities == null;
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;
PillDosage.IsValid = x => x > 0 && x <= castState.PillDosageLimit; PillDosage.IsValid = x => x > 0 && x <= castState.PillDosageLimit;
@@ -172,19 +130,8 @@ 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>
@@ -231,23 +178,46 @@ 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)
{ {
var reagentId = reagent; // Try to get the prototype for the given reagent. This gives us its name.
_prototypeManager.TryIndex(reagentId.Prototype, out ReagentPrototype? proto); _prototypeManager.TryIndex(reagent.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);
BufferInfo.Children.Add(BuildReagentRow(reagentColor, rowCount++, name, reagentId, quantity, true, true)); if (proto != null)
{
BufferInfo.Children.Add(new BoxContainer
{
Orientation = LayoutOrientation.Horizontal,
Children =
{
new Label {Text = $"{name}: "},
new Label
{
Text = $"{quantity}u",
StyleClasses = {StyleNano.StyleClassLabelSecondaryColor}
},
// Padding
new Control {HorizontalExpand = true},
MakeReagentButton("1", ChemMasterReagentAmount.U1, reagent, true, StyleBase.ButtonOpenRight),
MakeReagentButton("5", ChemMasterReagentAmount.U5, reagent, true, StyleBase.ButtonOpenBoth),
MakeReagentButton("10", ChemMasterReagentAmount.U10, reagent, true, StyleBase.ButtonOpenBoth),
MakeReagentButton("25", ChemMasterReagentAmount.U25, reagent, true, StyleBase.ButtonOpenBoth),
MakeReagentButton("50", ChemMasterReagentAmount.U50, reagent, true, StyleBase.ButtonOpenBoth),
MakeReagentButton("100", ChemMasterReagentAmount.U100, reagent, true, StyleBase.ButtonOpenBoth),
MakeReagentButton(Loc.GetString("chem-master-window-buffer-all-amount"), ChemMasterReagentAmount.All, reagent, true, StyleBase.ButtonOpenLeft),
}
});
}
} }
} }
private void BuildContainerUI(Control control, ContainerInfo? info, bool addReagentButtons) private void BuildContainerUI(Control control, ContainerInfo? info, bool addReagentButtons)
{ {
control.Children.Clear(); control.Children.Clear();
@@ -258,111 +228,104 @@ 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
{ {
Orientation = LayoutOrientation.Horizontal, // Name of the container and its fill status (Ex: 44/100u)
Children = control.Children.Add(new BoxContainer
{ {
new Label { Text = $"{info.DisplayName}: " }, Orientation = LayoutOrientation.Horizontal,
new Label Children =
{ {
Text = $"{info.CurrentVolume}/{info.MaxVolume}", new Label {Text = $"{info.DisplayName}: "},
StyleClasses = { StyleNano.StyleClassLabelSecondaryColor } new Label
}
}
});
// 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)))
{
control.Children.Add(BuildReagentRow(default(Color), rowCount++, id, default(ReagentId), quantity, false, addReagentButtons));
}
}
// Handle reagents if they are not null
if (info.Reagents != null)
{
foreach (var reagent in info.Reagents)
{
_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));
}
}
}
/// <summary>
/// Take reagent/entity data and present rows, labels, and buttons appropriately. todo sprites?
/// </summary>
private Control BuildReagentRow(Color reagentColor, int rowCount, string name, ReagentId reagent, FixedPoint2 quantity, bool isBuffer, bool addReagentButtons)
{
//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))
{
reagentColor = currentRowColor;
}
//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
{
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 Text = $"{info.CurrentVolume}/{info.MaxVolume}",
}, StyleClasses = {StyleNano.StyleClassLabelSecondaryColor}
Margin = new Thickness(0, 1) }
} }
} });
};
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)
{
contents = info.Reagents.Select(x =>
{
_prototypeManager.TryIndex(x.Reagent.Prototype, out ReagentPrototype? proto);
var name = proto?.LocalizedName
?? Loc.GetString("chem-master-window-unknown-reagent-text");
return (name, Id: x.Reagent, x.Quantity);
})
.OrderBy(r => r.Item1);
}
else
{
return;
}
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);
}
// 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 public String LabelLine
{ {
get => LabelLineEdit.Text; get
set => LabelLineEdit.Text = value; {
return LabelLineEdit.Text;
}
set
{
LabelLineEdit.Text = value;
}
} }
} }

View File

@@ -1,62 +1,17 @@
<controls:FancyWindow xmlns="https://spacestation14.io" <controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls" xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
Title="{Loc 'comms-console-menu-title'}" Title="{Loc 'comms-console-menu-title'}"
MinSize="400 300"> MinSize="400 225">
<BoxContainer Orientation="Vertical" HorizontalExpand="True" VerticalExpand="True" Margin="5">
<TextEdit Name="MessageInput" HorizontalExpand="True" VerticalExpand="True" Margin="0 0 0 5" MinHeight="100" />
<Button Name="AnnounceButton" Text="{Loc 'comms-console-menu-announcement-button'}" ToolTip="{Loc 'comms-console-menu-announcement-button-tooltip'}" StyleClasses="OpenLeft" Access="Public" />
<Button Name="BroadcastButton" Text="{Loc 'comms-console-menu-broadcast-button'}" ToolTip="{Loc 'comms-console-menu-broadcast-button-tooltip'}" StyleClasses="OpenLeft" Access="Public" />
<!-- Main Container --> <OptionButton Name="AlertLevelButton" ToolTip="{Loc 'comms-console-menu-alert-level-button-tooltip'}" StyleClasses="OpenRight" Access="Public" />
<BoxContainer Orientation="Vertical"
HorizontalExpand="False"
VerticalExpand="True"
Margin="6 6 6 5">
<TextEdit Name="MessageInput" <Control MinSize="10 10" />
VerticalExpand="True"
HorizontalExpand="True"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch"
MinHeight="100"/>
<!-- ButtonsPart --> <RichTextLabel Name="CountdownLabel" VerticalExpand="True" />
<BoxContainer Orientation="Vertical" <Button Name="EmergencyShuttleButton" Text="Placeholder Text" ToolTip="{Loc 'comms-console-menu-emergency-shuttle-button-tooltip'}" Access="Public" />
VerticalAlignment="Bottom"
SeparationOverride="4">
<!-- AnnouncePart -->
<BoxContainer Orientation="Vertical"
Margin="0 2">
<Button Name="AnnounceButton"
Access="Public"
Text="{Loc 'comms-console-menu-announcement-button'}"
ToolTip="{Loc 'comms-console-menu-announcement-button-tooltip'}"
StyleClasses="OpenLeft"
Margin="0 0 1 0"
Disabled="True"/>
<Button Name="BroadcastButton"
Access="Public"
Text="{Loc 'comms-console-menu-broadcast-button'}"
ToolTip="{Loc 'comms-console-menu-broadcast-button-tooltip'}"
StyleClasses="OpenBoth"/>
<OptionButton Name="AlertLevelButton"
Access="Public"
ToolTip="{Loc 'comms-console-menu-alert-level-button-tooltip'}"
StyleClasses="OpenRight"/>
</BoxContainer>
<!-- EmergencyPart -->
<BoxContainer Orientation="Vertical"
SeparationOverride="6">
<RichTextLabel Name="CountdownLabel"/>
<Button Name="EmergencyShuttleButton"
Access="Public"
Text="Placeholder Text"
ToolTip="{Loc 'comms-console-menu-emergency-shuttle-button-tooltip'}"/>
</BoxContainer>
</BoxContainer>
</BoxContainer> </BoxContainer>
</controls:FancyWindow> </controls:FancyWindow>

View File

@@ -12,7 +12,6 @@ using Content.Client.IoC;
using Content.Client.Launcher; using Content.Client.Launcher;
using Content.Client.Lobby; using Content.Client.Lobby;
using Content.Client.MainMenu; using Content.Client.MainMenu;
using Content.Client.Overlays;
using Content.Client.Parallax.Managers; using Content.Client.Parallax.Managers;
using Content.Client.Players.PlayTimeTracking; using Content.Client.Players.PlayTimeTracking;
using Content.Client.Radiation.Overlays; using Content.Client.Radiation.Overlays;
@@ -160,7 +159,6 @@ namespace Content.Client.Entry
_parallaxManager.LoadDefaultParallax(); _parallaxManager.LoadDefaultParallax();
_overlayManager.AddOverlay(new CP14BasePostProcessOverlay()); // CP14-PostProcess
_overlayManager.AddOverlay(new SingularityOverlay()); _overlayManager.AddOverlay(new SingularityOverlay());
_overlayManager.AddOverlay(new RadiationPulseOverlay()); _overlayManager.AddOverlay(new RadiationPulseOverlay());
_chatManager.Initialize(); _chatManager.Initialize();

View File

@@ -1,8 +0,0 @@
using Content.Shared.Explosion.EntitySystems;
namespace Content.Client.Explosion;
public sealed class ScatteringGrenadeSystem : SharedScatteringGrenadeSystem
{
}

View File

@@ -1,101 +0,0 @@
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());
}
}

View File

@@ -1,172 +0,0 @@
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;
}
}

View File

@@ -1,118 +0,0 @@
<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"/>
<BoxContainer Name="CallerIdContainer" Orientation="Vertical" ReservesSpace="False">
<RichTextLabel Name="CallerIdText" HorizontalAlignment="Center" Margin="0 0 0 0"/>
<Label Text="{Loc 'holopad-window-relay-label'}" Margin="10 5 10 0" ReservesSpace="False"/>
<RichTextLabel Name="HolopadIdText" HorizontalAlignment="Center" Margin="0 0 0 10"/>
</BoxContainer>
</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">
<BoxContainer Orientation="Vertical">
<!-- Contact filter -->
<LineEdit Name="SearchLineEdit" HorizontalExpand="True" Margin="4, 4, 4, 0"
PlaceHolder="{Loc holopad-window-filter-line-placeholder}" />
<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>
</BoxContainer>
</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>

View File

@@ -1,344 +0,0 @@
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);
var holoapdId = _telephoneSystem.GetFormattedDeviceIdForEntity(telephone.LastCallerId.Item3, Color.LightGray, "Default", 11);
CallerIdText.SetMessage(FormattedMessage.FromMarkupOrThrow(callerId));
HolopadIdText.SetMessage(FormattedMessage.FromMarkupOrThrow(holoapdId));
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 contactButton)
continue;
var passesFilter = string.IsNullOrEmpty(SearchLineEdit.Text) ||
contactButton.Text?.Contains(SearchLineEdit.Text, StringComparison.CurrentCultureIgnoreCase) == true;
contactButton.Visible = passesFilter;
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;
CallerIdContainer.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);
ReservesSpace = false;
}
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);
}
}

View File

@@ -455,21 +455,7 @@ public sealed class LobbyUIController : UIController, IOnStateEntered<LobbyState
{ {
EntityUid dummyEnt; EntityUid dummyEnt;
EntProtoId? previewEntity = null; if (humanoid is not 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);
@@ -483,8 +469,7 @@ public sealed class LobbyUIController : UIController, IOnStateEntered<LobbyState
if (humanoid != null && jobClothes) if (humanoid != null && jobClothes)
{ {
DebugTools.Assert(job != null); job ??= GetPreferredJob(humanoid);
GiveDummyJobClothes(dummyEnt, humanoid, job); GiveDummyJobClothes(dummyEnt, humanoid, job);
if (_prototypeManager.HasIndex<RoleLoadoutPrototype>(LoadoutSystem.GetJobPrototype(job.ID))) if (_prototypeManager.HasIndex<RoleLoadoutPrototype>(LoadoutSystem.GetJobPrototype(job.ID)))

View File

@@ -6,7 +6,6 @@
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"

View File

@@ -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" Margin="4" SizeFlagsStretchRatio="1" /> <SpriteView Name="SpriteView" Scale="8 8" 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" />

View File

@@ -1,46 +0,0 @@
using Content.Shared.Movement.Components;
using Content.Shared.Movement.Systems;
using Robust.Client.GameObjects;
using Robust.Shared.Timing;
namespace Content.Client.Movement.Systems;
/// <summary>
/// Controls the switching of motion and standing still animation
/// </summary>
public sealed class ClientSpriteMovementSystem : SharedSpriteMovementSystem
{
[Dependency] private readonly IGameTiming _timing = default!;
private EntityQuery<SpriteComponent> _spriteQuery;
public override void Initialize()
{
base.Initialize();
_spriteQuery = GetEntityQuery<SpriteComponent>();
SubscribeLocalEvent<SpriteMovementComponent, AfterAutoHandleStateEvent>(OnAfterAutoHandleState);
}
private void OnAfterAutoHandleState(Entity<SpriteMovementComponent> ent, ref AfterAutoHandleStateEvent args)
{
if (!_spriteQuery.TryGetComponent(ent, out var sprite))
return;
if (ent.Comp.IsMoving)
{
foreach (var (layer, state) in ent.Comp.MovementLayers)
{
sprite.LayerSetData(layer, state);
}
}
else
{
foreach (var (layer, state) in ent.Comp.NoMovementLayers)
{
sprite.LayerSetData(layer, state);
}
}
}
}

View File

@@ -0,0 +1,51 @@
using Content.Shared.Movement.Components;
using Content.Shared.Movement.Events;
using Content.Shared.Movement.Systems;
using Robust.Client.GameObjects;
using Robust.Shared.Timing;
namespace Content.Client.Movement.Systems;
/// <summary>
/// Handles setting sprite states based on whether an entity has movement input.
/// </summary>
public sealed class SpriteMovementSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _timing = default!;
private EntityQuery<SpriteComponent> _spriteQuery;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SpriteMovementComponent, MoveInputEvent>(OnSpriteMoveInput);
_spriteQuery = GetEntityQuery<SpriteComponent>();
}
private void OnSpriteMoveInput(EntityUid uid, SpriteMovementComponent component, ref MoveInputEvent args)
{
if (!_timing.IsFirstTimePredicted)
return;
var oldMoving = (SharedMoverController.GetNormalizedMovement(args.OldMovement) & MoveButtons.AnyDirection) != MoveButtons.None;
var moving = (SharedMoverController.GetNormalizedMovement(args.Entity.Comp.HeldMoveButtons) & MoveButtons.AnyDirection) != MoveButtons.None;
if (oldMoving == moving || !_spriteQuery.TryGetComponent(uid, out var sprite))
return;
if (moving)
{
foreach (var (layer, state) in component.MovementLayers)
{
sprite.LayerSetData(layer, state);
}
}
else
{
foreach (var (layer, state) in component.NoMovementLayers)
{
sprite.LayerSetData(layer, state);
}
}
}
}

View File

@@ -9,7 +9,6 @@ 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
@@ -44,8 +43,7 @@ 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;

View File

@@ -385,6 +385,26 @@ 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;
@@ -423,31 +443,11 @@ 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.Scale * blip.Texture.Width, scalingCoefficient * blip.Scale * blip.Texture.Height); var positionOffset = new Vector2(scalingCoefficient * blip.Texture.Width, scalingCoefficient * 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,9 +689,6 @@ 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))
{ {
@@ -742,15 +739,13 @@ 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, float scale = 1f) public NavMapBlip(EntityCoordinates coordinates, Texture texture, Color color, bool blinks, bool selectable = true)
{ {
Coordinates = coordinates; Coordinates = coordinates;
Texture = texture; Texture = texture;
Color = color; Color = color;
Blinks = blinks; Blinks = blinks;
Selectable = selectable; Selectable = selectable;
Scale = scale;
} }
} }

View File

@@ -6,7 +6,6 @@ using Robust.Shared.Utility;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using Vector4 = Robust.Shared.Maths.Vector4;
namespace Content.Client.Power; namespace Content.Client.Power;
@@ -105,26 +104,6 @@ public sealed partial class PowerMonitoringWindow
// Update power value // Update power value
// Don't use SI prefixes, just give the number in W, so that it is readily apparent which consumer is using a lot of power. // Don't use SI prefixes, just give the number in W, so that it is readily apparent which consumer is using a lot of power.
button.PowerValue.Text = Loc.GetString("power-monitoring-window-button-value", ("value", Math.Round(entry.PowerValue).ToString("N0"))); button.PowerValue.Text = Loc.GetString("power-monitoring-window-button-value", ("value", Math.Round(entry.PowerValue).ToString("N0")));
// Update battery level if applicable
if (entry.BatteryLevel != null)
{
button.BatteryLevel.Value = entry.BatteryLevel.Value;
button.BatteryLevel.Visible = true;
button.BatteryPercentage.Text = entry.BatteryLevel.Value.ToString("P0");
button.BatteryPercentage.Visible = true;
// Set progress bar color based on percentage
var color = Color.FromHsv(new Vector4(entry.BatteryLevel.Value * 0.33f, 1, 1, 1));
button.BatteryLevel.ForegroundStyleBoxOverride = new StyleBoxFlat { BackgroundColor = color };
}
else
{
button.BatteryLevel.Visible = false;
button.BatteryPercentage.Visible = false;
}
} }
private void UpdateEntrySourcesOrLoads(BoxContainer masterContainer, BoxContainer currentContainer, PowerMonitoringConsoleEntry[]? entries, SpriteSpecifier.Texture icon) private void UpdateEntrySourcesOrLoads(BoxContainer masterContainer, BoxContainer currentContainer, PowerMonitoringConsoleEntry[]? entries, SpriteSpecifier.Texture icon)
@@ -464,11 +443,6 @@ public sealed class PowerMonitoringButton : Button
public BoxContainer MainContainer; public BoxContainer MainContainer;
public TextureRect TextureRect; public TextureRect TextureRect;
public Label NameLocalized; public Label NameLocalized;
public ProgressBar BatteryLevel;
public PanelContainer BackgroundPanel;
public Label BatteryPercentage;
public Label PowerValue; public Label PowerValue;
public PowerMonitoringButton() public PowerMonitoringButton()
@@ -504,49 +478,6 @@ public sealed class PowerMonitoringButton : Button
MainContainer.AddChild(NameLocalized); MainContainer.AddChild(NameLocalized);
BatteryLevel = new ProgressBar()
{
SetWidth = 47f,
SetHeight = 20f,
Margin = new Thickness(15, 0, 0, 0),
MaxValue = 1,
Visible = false,
BackgroundStyleBoxOverride = new StyleBoxFlat { BackgroundColor = Color.Black },
};
MainContainer.AddChild(BatteryLevel);
BackgroundPanel = new PanelContainer
{
// Draw a half-transparent box over the battery level to make the text more readable.
PanelOverride = new StyleBoxFlat
{
BackgroundColor = new Color(0, 0, 0, 0.9f)
},
HorizontalAlignment = HAlignment.Center,
VerticalAlignment = VAlignment.Center,
HorizontalExpand = true,
VerticalExpand = true,
// Box is undersized perfectly compared to the progress bar, so a little bit of the unaffected progress bar is visible.
SetSize = new Vector2(43f, 16f)
};
BatteryLevel.AddChild(BackgroundPanel);
BatteryPercentage = new Label()
{
VerticalAlignment = VAlignment.Center,
HorizontalAlignment = HAlignment.Center,
Align = Label.AlignMode.Center,
SetWidth = 45f,
MinWidth = 20f,
Margin = new Thickness(10, -4, 10, 0),
ClipText = true,
Visible = false,
};
BackgroundPanel.AddChild(BatteryPercentage);
PowerValue = new Label() PowerValue = new Label()
{ {
HorizontalAlignment = HAlignment.Right, HorizontalAlignment = HAlignment.Right,

View File

@@ -110,7 +110,7 @@ namespace Content.Client.Sandbox
} }
// Try copy tile. // Try copy tile.
if (!_map.TryFindGridAt(_transform.ToMapCoordinates(coords), out var gridUid, out var grid) || !_mapSystem.TryGetTileRef(gridUid, grid, coords, out var tileRef)) if (!_map.TryFindGridAt(_transform.ToMapCoordinates(coords), out var gridUid, out var grid) || !_mapSystem.TryGetTileRef(gridUid, grid, coords, out var tileRef))
return false; return false;
@@ -157,5 +157,10 @@ namespace Content.Client.Sandbox
{ {
_consoleHost.ExecuteCommand("physics shapes"); _consoleHost.ExecuteCommand("physics shapes");
} }
public void MachineLinking()
{
_consoleHost.ExecuteCommand("signallink");
}
} }
} }

View File

@@ -28,7 +28,6 @@ public sealed partial class SensorMonitoringWindow : FancyWindow, IComputerWindo
public SensorMonitoringWindow() public SensorMonitoringWindow()
{ {
RobustXamlLoader.Load(this); RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
} }
public void UpdateState(ConsoleUIState state) public void UpdateState(ConsoleUIState state)

View File

@@ -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)))) + "\n"; ("currency", Loc.GetString(proto.DisplayName, ("amount", 1))));
} }
BalanceInfo.SetMarkup(balanceStr.TrimEnd()); BalanceInfo.SetMarkup(balanceStr.TrimEnd());
@@ -63,10 +63,7 @@ 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;

View File

@@ -18,7 +18,7 @@ public sealed partial class StoreWithdrawWindow : DefaultWindow
{ {
[Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
private Dictionary<CurrencyPrototype, FixedPoint2> _validCurrencies = new(); private Dictionary<FixedPoint2, CurrencyPrototype> _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(proto, currency.Value); _validCurrencies.Add(currency.Value, proto);
} }
//this shouldn't ever happen but w/e //this shouldn't ever happen but w/e
@@ -47,17 +47,14 @@ 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.Key.ID, Id = currency.Value.ID,
Amount = currency.Value, Amount = currency.Key,
MinHeight = 20, MinHeight = 20,
Text = Loc.GetString("store-withdraw-button-ui", ("currency",Loc.GetString(currency.Key.DisplayName, ("amount", currency.Value)))), Text = Loc.GetString("store-withdraw-button-ui", ("currency",Loc.GetString(currency.Value.DisplayName, ("amount", currency.Key)))),
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);
@@ -68,7 +65,7 @@ public sealed partial class StoreWithdrawWindow : DefaultWindow
ButtonContainer.AddChild(button); ButtonContainer.AddChild(button);
} }
var maxWithdrawAmount = _validCurrencies.Values.Max().Int(); var maxWithdrawAmount = _validCurrencies.Keys.Max().Int();
// setup withdraw slider // setup withdraw slider
WithdrawSlider.MinValue = 1; WithdrawSlider.MinValue = 1;

View File

@@ -110,7 +110,6 @@ 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");
@@ -1500,20 +1499,6 @@ 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 ---

View File

@@ -1,8 +0,0 @@
using Content.Shared.Telephone;
namespace Content.Client.Telephone;
public sealed class TelephoneSystem : SharedTelephoneSystem
{
}

View File

@@ -104,7 +104,7 @@ public sealed class TippyUIController : UIController
? -WaddleRotation ? -WaddleRotation
: WaddleRotation; : WaddleRotation;
if (EntityManager.TryGetComponent(_entity, out FootstepModifierComponent? step) && step.FootstepSoundCollection != null) if (EntityManager.TryGetComponent(_entity, out FootstepModifierComponent? step))
{ {
var audioParams = step.FootstepSoundCollection.Params var audioParams = step.FootstepSoundCollection.Params
.AddVolume(-7f) .AddVolume(-7f)

View File

@@ -1,34 +0,0 @@
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();
}
}

View File

@@ -572,10 +572,6 @@ 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()

View File

@@ -1,5 +1,4 @@
using System.Numerics; using Content.Client.Administration.Managers;
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;
@@ -8,7 +7,9 @@ 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;
@@ -108,13 +109,9 @@ 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;
@@ -152,6 +149,7 @@ 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()
@@ -166,7 +164,7 @@ public sealed class SandboxUIController : UIController, IOnStateChanged<Gameplay
{ {
if (_window != null) if (_window != null)
{ {
_window.Close(); _window.Dispose();
_window = null; _window = null;
} }
@@ -211,7 +209,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.Open(); _window.OpenCentered();
} }
else else
{ {

View File

@@ -4,24 +4,20 @@
Title="{Loc sandbox-window-title}" Title="{Loc sandbox-window-title}"
Resizable="False"> Resizable="False">
<BoxContainer Orientation="Vertical" SeparationOverride="4"> <BoxContainer Orientation="Vertical" SeparationOverride="4">
<Label Text="{Loc sandbox-window-map-editing-label}"/> <Button Name="AiOverlayButton" Access="Public" Text="{Loc sandbox-window-ai-overlay-button}" ToggleMode="True"/>
<Button Name="SpawnTilesButton" Access="Public" Text="{Loc sandbox-window-spawn-tiles-button}"/> <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="SpawnEntitiesButton" Access="Public" Text="{Loc sandbox-window-spawn-entities-button}"/>
<Button Name="SpawnTilesButton" Access="Public" Text="{Loc sandbox-window-spawn-tiles-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}"/>
<Label Text="{Loc sandbox-window-visibility-label}"/> <Button Name="GiveAghostButton" Access="Public" Text="{Loc sandbox-window-ghost-button}"/>
<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="AiOverlayButton" Access="Public" Text="{Loc sandbox-window-ai-overlay-button}" ToggleMode="True"/> <Button Name="SuicideButton" Access="Public" Text="{Loc sandbox-window-toggle-suicide-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>

View File

@@ -77,7 +77,6 @@ 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.
@@ -87,11 +86,6 @@ 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);

View File

@@ -1,7 +0,0 @@
using Content.Shared._CP14.Knowledge;
namespace Content.Client._CP14.Knowledge;
public sealed partial class ClientCP14KnowledgeSystem : SharedCP14KnowledgeSystem
{
}

View File

@@ -80,9 +80,6 @@ public sealed class CP14ClientModularCraftSystem : CP14SharedModularCraftSystem
start.Comp.RevealedLayers.Add(keyCode); start.Comp.RevealedLayers.Add(keyCode);
var index = sprite.AddLayer(defaultLayer); var index = sprite.AddLayer(defaultLayer);
sprite.LayerMapSet(keyCode, index); sprite.LayerMapSet(keyCode, index);
if (indexedPart.Color is not null)
sprite.LayerSetColor(keyCode, indexedPart.Color.Value);
} }
else else
{ {
@@ -151,7 +148,6 @@ public sealed class CP14ClientModularCraftSystem : CP14SharedModularCraftSystem
{ {
RsiPath = indexedPart.RsiPath, RsiPath = indexedPart.RsiPath,
State = state, State = state,
Color = indexedPart.Color,
}; };
var key = $"{defaultKey}-{counterPart}-default"; var key = $"{defaultKey}-{counterPart}-default";
@@ -207,7 +203,6 @@ public sealed class CP14ClientModularCraftSystem : CP14SharedModularCraftSystem
{ {
RsiPath = indexedPart.RsiPath, RsiPath = indexedPart.RsiPath,
State = state, State = state,
Color = indexedPart.Color,
}; };
var key = $"{defaultKey}-{counterPart}-default"; var key = $"{defaultKey}-{counterPart}-default";

View File

@@ -16,12 +16,11 @@
<Label Text="{Loc 'cp14-ui-options-main-graphics-label'}" <Label Text="{Loc 'cp14-ui-options-main-graphics-label'}"
StyleClasses="LabelKeyText"/> StyleClasses="LabelKeyText"/>
<CheckBox Name="WaveShaderEnabled" <!-- CP14 Wave shader settings -->
Text="{Loc 'cp14-ui-options-main-graphics-wave-shader'}"/> <BoxContainer Orientation="Horizontal">
<!-- TODO: Wave shader tooltip --> <Label Text="{Loc 'cp14-ui-options-main-graphics-wave-shader'}" Margin="0 0 4 0"/>
<CheckBox Name="PostProcessCheckBox" <CheckBox Name="WaveShaderEnabled"/>
Text="{Loc 'cp14-ui-options-postprocess'}" </BoxContainer>
ToolTip="{Loc 'cp14-ui-options-postprocess-tooltip'}"/>
</BoxContainer> </BoxContainer>
</ScrollContainer> </ScrollContainer>

View File

@@ -13,7 +13,6 @@ public sealed partial class CP14OptionsMenuMainTab : Control
RobustXamlLoader.Load(this); RobustXamlLoader.Load(this);
Control.AddOptionCheckBox(CP14ConfigVars.WaveShaderEnabled, WaveShaderEnabled); Control.AddOptionCheckBox(CP14ConfigVars.WaveShaderEnabled, WaveShaderEnabled);
Control.AddOptionCheckBox(CP14ConfigVars.PostProcess, PostProcessCheckBox);
Control.Initialize(); Control.Initialize();
} }

View File

@@ -1,77 +0,0 @@
using Content.Shared._CP14.Configuration;
using System.Numerics;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Shared.Configuration;
using Robust.Shared.Enums;
using Robust.Shared.Graphics;
using Robust.Shared.Physics;
using Robust.Shared.Prototypes;
namespace Content.Client.Overlays;
// This overlay serves as the foundational post processing overlay.
// Ideally, for performance reasons, post processing designed to be present at all times, such as additive light blending or tonemapping, should be done as part of a single shader pass.
public sealed class CP14BasePostProcessOverlay : Overlay
{
[Dependency] private readonly IConfigurationManager _configManager = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly ILightManager _lightManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
public override bool RequestScreenTexture => true;
public override OverlaySpace Space => OverlaySpace.WorldSpace;
private readonly ShaderInstance _basePostProcessShader;
public CP14BasePostProcessOverlay()
{
IoCManager.InjectDependencies(this);
_basePostProcessShader = _prototypeManager.Index<ShaderPrototype>("BasePostProcess").InstanceUnique();
}
protected override bool BeforeDraw(in OverlayDrawArgs args)
{
if (!_configManager.GetCVar(CP14ConfigVars.PostProcess))
return false;
if (!_entityManager.TryGetComponent(_playerManager.LocalSession?.AttachedEntity, out EyeComponent? eyeComp))
return false;
if (args.Viewport.Eye != eyeComp.Eye)
return false;
if (!_lightManager.Enabled || !eyeComp.Eye.DrawLight || !eyeComp.Eye.DrawFov)
return false;
var playerEntity = _playerManager.LocalSession?.AttachedEntity;
if (playerEntity == null)
return false;
return true;
}
protected override void Draw(in OverlayDrawArgs args)
{
if (ScreenTexture == null)
return;
if (args.Viewport.Eye == null)
return;
var playerEntity = _playerManager.LocalSession?.AttachedEntity;
var worldHandle = args.WorldHandle;
var viewport = args.WorldBounds;
_basePostProcessShader.SetParameter("SCREEN_TEXTURE", ScreenTexture);
_basePostProcessShader.SetParameter("LIGHT_TEXTURE", args.Viewport.LightRenderTarget.Texture);
_basePostProcessShader.SetParameter("Zoom", args.Viewport.Eye.Zoom.X);
worldHandle.UseShader(_basePostProcessShader);
worldHandle.DrawRect(viewport, Color.White);
worldHandle.UseShader(null);
}
}

View File

@@ -12,9 +12,6 @@ public sealed class CP14WorkbenchBoundUserInterface : BoundUserInterface
{ {
private CP14WorkbenchWindow? _window; private CP14WorkbenchWindow? _window;
[ViewVariables]
private string _search = string.Empty;
public CP14WorkbenchBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) public CP14WorkbenchBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{ {
} }
@@ -26,12 +23,6 @@ public sealed class CP14WorkbenchBoundUserInterface : BoundUserInterface
_window = this.CreateWindow<CP14WorkbenchWindow>(); _window = this.CreateWindow<CP14WorkbenchWindow>();
_window.OnCraft += entry => SendMessage(new CP14WorkbenchUiCraftMessage(entry.ProtoId)); _window.OnCraft += entry => SendMessage(new CP14WorkbenchUiCraftMessage(entry.ProtoId));
_window.OnTextUpdated += search =>
{
_search = search.Trim().ToLowerInvariant();
_window.UpdateFilter(_search);
};
} }
protected override void UpdateState(BoundUserInterfaceState state) protected override void UpdateState(BoundUserInterfaceState state)
@@ -41,7 +32,7 @@ public sealed class CP14WorkbenchBoundUserInterface : BoundUserInterface
switch (state) switch (state)
{ {
case CP14WorkbenchUiRecipesState recipesState: case CP14WorkbenchUiRecipesState recipesState:
_window?.UpdateRecipes(recipesState, _search); _window?.UpdateRecipes(recipesState);
break; break;
} }
} }

View File

@@ -1,20 +1,10 @@
<Control xmlns="https://spacestation14.io"> <Control xmlns="https://spacestation14.io">
<GridContainer Columns="2"> <GridContainer Columns="2">
<EntityPrototypeView <TextureRect Name="View"
Name="EntityView" Margin="0,0,4,0"
Margin="0,0,4,0" MinSize="48 48"
MinSize="48 48" HorizontalAlignment="Left"
MaxSize="48 48" Stretch="KeepAspectCentered"/>
Scale="2,2" <Label Name="Name"/>
HorizontalAlignment="Left"
VerticalExpand="True" />
<TextureRect
Name="View"
Margin="0,0,4,0"
MinSize="48 48"
MaxSize="48 48"
HorizontalAlignment="Left"
Stretch="KeepAspectCentered" />
<Label Name="Name" />
</GridContainer> </GridContainer>
</Control> </Control>

View File

@@ -31,9 +31,7 @@ public sealed partial class CP14WorkbenchRecipeControl : Control
{ {
var entityName = prototype.Name; var entityName = prototype.Name;
Name.Text = count <= 1 ? entityName : $"{entityName} x{count}"; Name.Text = count <= 1 ? entityName : $"{entityName} x{count}";
View.Texture = _sprite.GetPrototypeIcon(prototype).Default;
View.Visible = false;
EntityView.SetPrototype(prototype);
} }
public CP14WorkbenchRecipeControl(StackPrototype prototype, int count) : this() public CP14WorkbenchRecipeControl(StackPrototype prototype, int count) : this()
@@ -45,7 +43,6 @@ public sealed partial class CP14WorkbenchRecipeControl : Control
if (icon is null) if (icon is null)
return; return;
EntityView.Visible = false;
View.Texture = _sprite.Frame0(icon); View.Texture = _sprite.Frame0(icon);
} }
} }

View File

@@ -1,13 +1,11 @@
<Control xmlns="https://spacestation14.io"> <Control xmlns="https://spacestation14.io">
<Button Name="Button"> <Button Name="Button">
<GridContainer Columns="2"> <GridContainer Columns="2">
<EntityPrototypeView Name="View" <TextureRect Name="View"
Margin="0,0,4,0" Margin="0,0,4,0"
MinSize="48 48" MinSize="48 48"
MaxSize="48 48" HorizontalAlignment="Left"
Scale="2,2" Stretch="KeepAspectCentered"/>
HorizontalAlignment="Center"
VerticalExpand="True"/>
<Label Name="Name"/> <Label Name="Name"/>
</GridContainer> </GridContainer>
</Button> </Button>

View File

@@ -59,6 +59,6 @@ public sealed partial class CP14WorkbenchRequirementControl : Control
private void UpdateView() private void UpdateView()
{ {
View.SetPrototype(_recipePrototype.Result); View.Texture = _sprite.GetPrototypeIcon(_recipePrototype.Result).Default;
} }
} }

View File

@@ -2,68 +2,59 @@
xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client" xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls" xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
Title="{Loc 'cp14-workbench-ui-title'}" Title="{Loc 'cp14-workbench-ui-title'}"
SetSize="700 500" MinSize="700 600">
MinSize="700 300"> <BoxContainer Orientation="Vertical">
<BoxContainer Orientation="Horizontal">
<!-- Main --> <!-- Main -->
<BoxContainer HorizontalExpand="True" VerticalExpand="True" Orientation="Horizontal"> <BoxContainer HorizontalExpand="True" VerticalExpand="True" Orientation="Horizontal">
<!-- Product list (left side UI) --> <GridContainer HorizontalExpand="True" VerticalExpand="True" Columns="2">
<BoxContainer SizeFlagsStretchRatio="0.5" HorizontalExpand="True" Orientation="Vertical" Margin="0 0 10 0">
<!-- Search Bar -->
<LineEdit Name="SearchBar" Margin="4" PlaceHolder="Search" HorizontalExpand="True" />
<!-- Crafts container --> <!-- Crafts container -->
<ScrollContainer HorizontalExpand="True" VerticalExpand="True" MinSize="0 200"> <ScrollContainer HorizontalExpand="True" VerticalExpand="True" MinSize="0 200">
<BoxContainer Name="CraftsContainer" Orientation="Vertical" HorizontalExpand="True" /> <BoxContainer Name="CraftsContainer" Orientation="Vertical" HorizontalExpand="True"/>
</ScrollContainer> </ScrollContainer>
</BoxContainer>
<!-- Craft view (right side UI) --> <!-- Craft view -->
<BoxContainer SizeFlagsStretchRatio="0.5" Orientation="Vertical" HorizontalExpand="True" <BoxContainer HorizontalExpand="True" VerticalExpand="True" Orientation="Vertical">
VerticalExpand="True"> <PanelContainer HorizontalExpand="True" VerticalExpand="True">
<PanelContainer HorizontalExpand="True" VerticalExpand="True"> <!-- Background -->
<!-- Background --> <PanelContainer.PanelOverride>
<PanelContainer.PanelOverride> <graphics:StyleBoxFlat BackgroundColor="#41332f"/>
<graphics:StyleBoxFlat BackgroundColor="#41332f" /> </PanelContainer.PanelOverride>
</PanelContainer.PanelOverride>
<!-- Content --> <!-- Content -->
<BoxContainer HorizontalExpand="True" VerticalExpand="True" Orientation="Vertical"> <BoxContainer HorizontalExpand="True" VerticalExpand="True" Orientation="Vertical">
<!-- Item info --> <!-- Item info -->
<!-- icon --> <GridContainer HorizontalExpand="True" Columns="2">
<EntityPrototypeView Name="ItemView" <!-- Left panel - icon -->
Scale="2,2" <TextureRect Name="ItemView"
Margin="0,0,4,0" Margin="0,0,4,0"
MinSize="64 64" MinSize="64 64"
MaxSize="64 64" HorizontalAlignment="Left"
HorizontalAlignment="Left" /> Stretch="KeepAspectCentered"/>
<!-- name & description --> <!-- Right panel - name & description -->
<BoxContainer Margin="5 0 0 0" HorizontalExpand="True" VerticalExpand="True" <BoxContainer HorizontalExpand="True" VerticalExpand="True" Orientation="Vertical">
Orientation="Vertical"> <Label Name="ItemName" Text="Name"/>
<RichTextLabel Name="ItemName" Margin="5" /> <Label Name="ItemDescription" Text="Description" ClipText="True"/>
<RichTextLabel Name="ItemDescription" Margin="5" /> </BoxContainer>
</GridContainer>
<controls:HLine Color="#404040" Thickness="2" Margin="0 5"/>
<!-- Required title -->
<Label Text="{Loc 'cp14-workbench-recipe-list'}"/>
<!-- Craft requirements content -->
<!-- Added by code -->
<BoxContainer Name="ItemRequirements" Orientation="Vertical" VerticalExpand="True" HorizontalExpand="True"/>
<controls:HLine Color="#404040" Thickness="2" Margin="0 5"/>
<!-- Craft button -->
<Button Name="CraftButton" Text="{Loc 'cp14-workbench-craft'}"/>
</BoxContainer> </BoxContainer>
</PanelContainer>
<controls:HLine Color="#404040" Thickness="2" Margin="0 5" /> </BoxContainer>
</GridContainer>
<!-- Required title -->
<Label Margin="5 0 0 0" Text="{Loc 'cp14-workbench-recipe-list'}" />
<!-- Craft requirements content -->
<!-- Added by code -->
<BoxContainer
Margin="5 0 0 0"
Name="ItemRequirements"
Orientation="Vertical" VerticalExpand="True"
HorizontalExpand="True" />
<controls:HLine Color="#404040" Thickness="5" Margin="0 5" />
<!-- Craft button -->
<Button Name="CraftButton" Text="{Loc 'cp14-workbench-craft'}" />
</BoxContainer>
</PanelContainer>
</BoxContainer>
</BoxContainer> </BoxContainer>
</BoxContainer> </BoxContainer>
</DefaultWindow> </DefaultWindow>

View File

@@ -19,22 +19,19 @@ public sealed partial class CP14WorkbenchWindow : DefaultWindow
[Dependency] private readonly IEntityManager _entity = default!; [Dependency] private readonly IEntityManager _entity = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!; [Dependency] private readonly IPrototypeManager _prototype = default!;
private CP14WorkbenchUiRecipesState? _cachedState;
public event Action<CP14WorkbenchUiRecipesEntry>? OnCraft; public event Action<CP14WorkbenchUiRecipesEntry>? OnCraft;
public event Action<string>? OnTextUpdated;
private CP14WorkbenchUiRecipesEntry? _selectedEntry; private readonly SpriteSystem _sprite;
private CP14WorkbenchUiRecipesEntry? _selectedEntry ;
public CP14WorkbenchWindow() public CP14WorkbenchWindow()
{ {
RobustXamlLoader.Load(this); RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this); IoCManager.InjectDependencies(this);
SearchBar.OnTextChanged += _ => _sprite = _entity.System<SpriteSystem>();
{
OnTextUpdated?.Invoke(SearchBar.Text);
};
CraftButton.OnPressed += _ => CraftButton.OnPressed += _ =>
{ {
if (_selectedEntry is null) if (_selectedEntry is null)
@@ -44,34 +41,13 @@ public sealed partial class CP14WorkbenchWindow : DefaultWindow
}; };
} }
public void UpdateFilter(string? search) public void UpdateRecipes(CP14WorkbenchUiRecipesState recipesState)
{ {
if (_cachedState is null)
return;
UpdateRecipes(_cachedState, search);
}
public void UpdateRecipes(CP14WorkbenchUiRecipesState recipesState, string? search = null)
{
_cachedState = recipesState;
CraftsContainer.RemoveAllChildren(); CraftsContainer.RemoveAllChildren();
List<CP14WorkbenchUiRecipesEntry> uncraftableList = new(); List<CP14WorkbenchUiRecipesEntry> uncraftableList = new();
foreach (var entry in recipesState.Recipes) foreach (var entry in recipesState.Recipes)
{ {
if (search is not null && search != "")
{
if (!_prototype.TryIndex(entry.ProtoId, out var indexedEntry))
continue;
if (!_prototype.TryIndex(indexedEntry.Result, out var indexedResult))
continue;
if (!indexedResult.Name.Contains(search))
continue;
}
if (entry.Craftable) if (entry.Craftable)
{ {
var control = new CP14WorkbenchRequirementControl(entry); var control = new CP14WorkbenchRequirementControl(entry);
@@ -113,7 +89,7 @@ public sealed partial class CP14WorkbenchWindow : DefaultWindow
var result = _prototype.Index(recipe.Result); var result = _prototype.Index(recipe.Result);
ItemView.SetPrototype(recipe.Result); ItemView.Texture = _sprite.GetPrototypeIcon(recipe.Result).Default;
ItemName.Text = result.Name; ItemName.Text = result.Name;
ItemDescription.Text = result.Description; ItemDescription.Text = result.Description;

View File

@@ -2,7 +2,6 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using Robust.Shared; using Robust.Shared;
using Robust.Shared.Audio.Components;
using Robust.Shared.Configuration; using Robust.Shared.Configuration;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.Log; using Robust.Shared.Log;
@@ -217,17 +216,14 @@ namespace Content.IntegrationTests.Tests
/// generally not spawn unrelated / detached entities. Any entities that do get spawned should be parented to /// generally not spawn unrelated / detached entities. Any entities that do get spawned should be parented to
/// the spawned entity (e.g., in a container). If an entity needs to spawn an entity somewhere in null-space, /// the spawned entity (e.g., in a container). If an entity needs to spawn an entity somewhere in null-space,
/// it should delete that entity when it is no longer required. This test mainly exists to prevent "entity leak" /// it should delete that entity when it is no longer required. This test mainly exists to prevent "entity leak"
/// bugs, where spawning some entity starts spawning unrelated entities in null space that stick around after /// bugs, where spawning some entity starts spawning unrelated entities in null space.
/// the original entity is gone.
///
/// Note that this isn't really a strict requirement, and there are probably quite a few edge cases. Its a pretty
/// crude test to try catch issues like this, and possibly should just be disabled.
/// </remarks> /// </remarks>
[Test] [Test]
public async Task SpawnAndDeleteEntityCountTest() public async Task SpawnAndDeleteEntityCountTest()
{ {
var settings = new PoolSettings { Connected = true, Dirty = true }; var settings = new PoolSettings { Connected = true, Dirty = true };
await using var pair = await PoolManager.GetServerClient(settings); await using var pair = await PoolManager.GetServerClient(settings);
var mapManager = pair.Server.ResolveDependency<IMapManager>();
var mapSys = pair.Server.System<SharedMapSystem>(); var mapSys = pair.Server.System<SharedMapSystem>();
var server = pair.Server; var server = pair.Server;
var client = pair.Client; var client = pair.Client;
@@ -265,9 +261,6 @@ namespace Content.IntegrationTests.Tests
await pair.RunTicksSync(3); await pair.RunTicksSync(3);
// We consider only non-audio entities, as some entities will just play sounds when they spawn.
int Count(IEntityManager ent) => ent.EntityCount - ent.Count<AudioComponent>();
foreach (var protoId in protoIds) foreach (var protoId in protoIds)
{ {
// TODO fix ninja // TODO fix ninja
@@ -275,8 +268,8 @@ namespace Content.IntegrationTests.Tests
if (protoId == "MobHumanSpaceNinja") if (protoId == "MobHumanSpaceNinja")
continue; continue;
var count = Count(server.EntMan); var count = server.EntMan.EntityCount;
var clientCount = Count(client.EntMan); var clientCount = client.EntMan.EntityCount;
EntityUid uid = default; EntityUid uid = default;
await server.WaitPost(() => uid = server.EntMan.SpawnEntity(protoId, coords)); await server.WaitPost(() => uid = server.EntMan.SpawnEntity(protoId, coords));
await pair.RunTicksSync(3); await pair.RunTicksSync(3);
@@ -284,30 +277,30 @@ namespace Content.IntegrationTests.Tests
// If the entity deleted itself, check that it didn't spawn other entities // If the entity deleted itself, check that it didn't spawn other entities
if (!server.EntMan.EntityExists(uid)) if (!server.EntMan.EntityExists(uid))
{ {
if (Count(server.EntMan) != count) if (server.EntMan.EntityCount != count)
{ {
Assert.Fail($"Server prototype {protoId} failed on deleting itself"); Assert.Fail($"Server prototype {protoId} failed on deleting itself");
} }
if (Count(client.EntMan) != clientCount) if (client.EntMan.EntityCount != clientCount)
{ {
Assert.Fail($"Client prototype {protoId} failed on deleting itself\n" + Assert.Fail($"Client prototype {protoId} failed on deleting itself\n" +
$"Expected {clientCount} and found {Count(client.EntMan)}.\n" + $"Expected {clientCount} and found {client.EntMan.EntityCount}.\n" +
$"Server was {count}."); $"Server was {count}.");
} }
continue; continue;
} }
// Check that the number of entities has increased. // Check that the number of entities has increased.
if (Count(server.EntMan) <= count) if (server.EntMan.EntityCount <= count)
{ {
Assert.Fail($"Server prototype {protoId} failed on spawning as entity count didn't increase"); Assert.Fail($"Server prototype {protoId} failed on spawning as entity count didn't increase");
} }
if (Count(client.EntMan) <= clientCount) if (client.EntMan.EntityCount <= clientCount)
{ {
Assert.Fail($"Client prototype {protoId} failed on spawning as entity count didn't increase" + Assert.Fail($"Client prototype {protoId} failed on spawning as entity count didn't increase" +
$"Expected at least {clientCount} and found {Count(client.EntMan)}. " + $"Expected at least {clientCount} and found {client.EntMan.EntityCount}. " +
$"Server was {count}"); $"Server was {count}");
} }
@@ -315,15 +308,15 @@ namespace Content.IntegrationTests.Tests
await pair.RunTicksSync(3); await pair.RunTicksSync(3);
// Check that the number of entities has gone back to the original value. // Check that the number of entities has gone back to the original value.
if (Count(server.EntMan) != count) if (server.EntMan.EntityCount != count)
{ {
Assert.Fail($"Server prototype {protoId} failed on deletion count didn't reset properly"); Assert.Fail($"Server prototype {protoId} failed on deletion count didn't reset properly");
} }
if (Count(client.EntMan) != clientCount) if (client.EntMan.EntityCount != clientCount)
{ {
Assert.Fail($"Client prototype {protoId} failed on deletion count didn't reset properly:\n" + Assert.Fail($"Client prototype {protoId} failed on deletion count didn't reset properly:\n" +
$"Expected {clientCount} and found {Count(client.EntMan)}.\n" + $"Expected {clientCount} and found {client.EntMan.EntityCount}.\n" +
$"Server was {count}."); $"Server was {count}.");
} }
} }

View File

@@ -1,132 +0,0 @@
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();
}
}

View File

@@ -50,6 +50,7 @@ namespace Content.IntegrationTests.Tests
"MeteorArena", "MeteorArena",
//CrystallEdge maps //CrystallEdge maps
"Village",
"Comoss", "Comoss",
//CrystallEdge Map replacement end //CrystallEdge Map replacement end
}; };

View File

@@ -96,6 +96,26 @@ public sealed class ResearchTest
}); });
}); });
await pair.CleanReturnAsync();
}
[Test]
public async Task AllLatheRecipesValidTest()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var proto = server.ResolveDependency<IPrototypeManager>();
Assert.Multiple(() =>
{
foreach (var recipe in proto.EnumeratePrototypes<LatheRecipePrototype>())
{
if (recipe.Result == null)
Assert.That(recipe.ResultReagents, Is.Not.Null, $"Recipe '{recipe.ID}' has no result or result reagents.");
}
});
await pair.CleanReturnAsync(); await pair.CleanReturnAsync();
}*/ }*/
} }

View File

@@ -79,11 +79,11 @@ public sealed class StoreTests
var discountComponent = entManager.GetComponent<StoreDiscountComponent>(pda); var discountComponent = entManager.GetComponent<StoreDiscountComponent>(pda);
Assert.That( Assert.That(
discountComponent.Discounts, discountComponent.Discounts,
Has.Exactly(6).Items, Has.Exactly(3).Items,
$"After applying discount total discounted items count was expected to be '6' " $"After applying discount total discounted items count was expected to be '3' "
+ $"but was actually {discountComponent.Discounts.Count}- this can be due to discount " + $"but was actually {discountComponent.Discounts.Count}- this can be due to discount "
+ $"categories settings (maxItems, weight) not being realistically set, or default " + $"categories settings (maxItems, weight) not being realistically set, or default "
+ $"discounted count being changed from '6' in StoreDiscountSystem.InitializeDiscounts." + $"discounted count being changed from '3' in StoreDiscountSystem.InitializeDiscounts."
); );
var discountedListingItems = storeComponent.FullListingsCatalog var discountedListingItems = storeComponent.FullListingsCatalog
.Where(x => x.IsCostModified) .Where(x => x.IsCostModified)

View File

@@ -1,6 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Reflection; using System.Linq;
using Content.Server.Administration.Managers;
using Robust.Shared.Toolshed; using Robust.Shared.Toolshed;
namespace Content.IntegrationTests.Tests.Toolshed; namespace Content.IntegrationTests.Tests.Toolshed;
@@ -11,23 +10,10 @@ public sealed class AdminTest : ToolshedTest
[Test] [Test]
public async Task AllCommandsHavePermissions() public async Task AllCommandsHavePermissions()
{ {
var toolMan = Server.ResolveDependency<ToolshedManager>();
var admin = Server.ResolveDependency<IAdminManager>();
var ignored = new HashSet<Assembly>()
{typeof(LocTest).Assembly, typeof(Robust.UnitTesting.Shared.Toolshed.LocTest).Assembly};
await Server.WaitAssertion(() => await Server.WaitAssertion(() =>
{ {
Assert.Multiple(() => Assert.That(InvokeCommand("cmd:list where { acmd:perms isnull }", out var res));
{ Assert.That((IEnumerable<CommandSpec>) res, Is.Empty, "All commands must have admin permissions set up.");
foreach (var cmd in toolMan.DefaultEnvironment.AllCommands())
{
if (ignored.Contains(cmd.Cmd.GetType().Assembly))
continue;
Assert.That(admin.TryGetCommandFlags(cmd, out _), $"Command does not have admin permissions set up: {cmd.FullName()}");
}
});
}); });
} }
} }

View File

@@ -1,6 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Reflection; using Robust.Shared.IoC;
using Robust.Shared.Localization; using Robust.Shared.Localization;
using Robust.Shared.Toolshed; using Robust.Shared.Toolshed;
@@ -14,27 +14,10 @@ public sealed class LocTest : ToolshedTest
[Test] [Test]
public async Task AllCommandsHaveDescriptions() public async Task AllCommandsHaveDescriptions()
{ {
var locMan = Server.ResolveDependency<ILocalizationManager>();
var toolMan = Server.ResolveDependency<ToolshedManager>();
var locStrings = new HashSet<string>();
var ignored = new HashSet<Assembly>()
{typeof(LocTest).Assembly, typeof(Robust.UnitTesting.Shared.Toolshed.LocTest).Assembly};
await Server.WaitAssertion(() => await Server.WaitAssertion(() =>
{ {
Assert.Multiple(() => Assert.That(InvokeCommand("cmd:list where { cmd:descloc loc:tryloc isnull }", out var res));
{ Assert.That((IEnumerable<CommandSpec>)res!, Is.Empty, "All commands must have localized descriptions.");
foreach (var cmd in toolMan.DefaultEnvironment.AllCommands())
{
if (ignored.Contains(cmd.Cmd.GetType().Assembly))
continue;
var descLoc = cmd.DescLocStr();
Assert.That(locStrings.Add(descLoc), $"Duplicate command description key: {descLoc}");
Assert.That(locMan.TryGetString(descLoc, out _), $"Failed to get command description for command {cmd.FullName()}");
}
});
}); });
} }
} }

View File

@@ -74,15 +74,15 @@ public abstract class ToolshedTest : IInvocationContext
return (T) res!; return (T) res!;
} }
protected void ParseCommand(string command, Type? inputType = null, Type? expectedType = null) protected void ParseCommand(string command, Type? inputType = null, Type? expectedType = null, bool once = false)
{ {
var parser = new ParserContext(command, Toolshed); var parser = new ParserContext(command, Toolshed);
var success = CommandRun.TryParse(parser, inputType, expectedType, out _); var success = CommandRun.TryParse(false, parser, inputType, expectedType, once, out _, out _, out var error);
if (parser.Error is not null) if (error is not null)
ReportError(parser.Error); ReportError(error);
if (parser.Error is null) if (error is null)
Assert.That(success, $"Parse failed despite no error being reported. Parsed {command}"); Assert.That(success, $"Parse failed despite no error being reported. Parsed {command}");
} }
@@ -153,28 +153,11 @@ public abstract class ToolshedTest : IInvocationContext
return _errors; return _errors;
} }
public bool HasErrors => _errors.Count > 0;
public void ClearErrors() public void ClearErrors()
{ {
_errors.Clear(); _errors.Clear();
} }
public object? ReadVar(string name)
{
return Variables.GetValueOrDefault(name);
}
public void WriteVar(string name, object? value)
{
Variables[name] = value;
}
public IEnumerable<string> GetVars()
{
return Variables.Keys;
}
public Dictionary<string, object?> Variables { get; } = new(); public Dictionary<string, object?> Variables { get; } = new();
protected void ExpectError(Type err) protected void ExpectError(Type err)

View File

@@ -10,7 +10,11 @@ namespace Content.Server.Access;
public sealed class AddAccessLogCommand : ToolshedCommand public sealed class AddAccessLogCommand : ToolshedCommand
{ {
[CommandImplementation] [CommandImplementation]
public void AddAccessLog(IInvocationContext ctx, EntityUid input, float seconds, string accessor) public void AddAccessLog(
[CommandInvocationContext] IInvocationContext ctx,
[CommandArgument] EntityUid input,
[CommandArgument] float seconds,
[CommandArgument] ValueRef<string> accessor)
{ {
var accessReader = EnsureComp<AccessReaderComponent>(input); var accessReader = EnsureComp<AccessReaderComponent>(input);
@@ -19,14 +23,19 @@ public sealed class AddAccessLogCommand : ToolshedCommand
ctx.WriteLine($"WARNING: Surpassing the limit of the log by {accessLogCount - accessReader.AccessLogLimit+1} entries!"); ctx.WriteLine($"WARNING: Surpassing the limit of the log by {accessLogCount - accessReader.AccessLogLimit+1} entries!");
var accessTime = TimeSpan.FromSeconds(seconds); var accessTime = TimeSpan.FromSeconds(seconds);
accessReader.AccessLog.Enqueue(new AccessRecord(accessTime, accessor)); var accessName = accessor.Evaluate(ctx)!;
accessReader.AccessLog.Enqueue(new AccessRecord(accessTime, accessName));
ctx.WriteLine($"Successfully added access log to {input} with this information inside:\n " + ctx.WriteLine($"Successfully added access log to {input} with this information inside:\n " +
$"Time of access: {accessTime}\n " + $"Time of access: {accessTime}\n " +
$"Accessed by: {accessor}"); $"Accessed by: {accessName}");
} }
[CommandImplementation] [CommandImplementation]
public void AddAccessLogPiped(IInvocationContext ctx, [PipedArgument] EntityUid input, float seconds, string accessor) public void AddAccessLogPiped(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] EntityUid input,
[CommandArgument] float seconds,
[CommandArgument] ValueRef<string> accessor)
{ {
AddAccessLog(ctx, input, seconds, accessor); AddAccessLog(ctx, input, seconds, accessor);
} }

View File

@@ -408,17 +408,6 @@ namespace Content.Server.Administration.Managers
} }
private async Task<(AdminData dat, int? rankId, bool specialLogin)?> LoadAdminData(ICommonSession session) private async Task<(AdminData dat, int? rankId, bool specialLogin)?> LoadAdminData(ICommonSession session)
{
var result = await LoadAdminDataCore(session);
// Make sure admin didn't disconnect while data was loading.
if (session.Status != SessionStatus.InGame)
return null;
return result;
}
private async Task<(AdminData dat, int? rankId, bool specialLogin)?> LoadAdminDataCore(ICommonSession session)
{ {
var promoteHost = IsLocal(session) && _cfg.GetCVar(CCVars.ConsoleLoginLocal) var promoteHost = IsLocal(session) && _cfg.GetCVar(CCVars.ConsoleLoginLocal)
|| _promotedPlayers.Contains(session.UserId) || _promotedPlayers.Contains(session.UserId)

View File

@@ -337,15 +337,10 @@ public sealed class AdminSystem : EntitySystem
private void UpdatePanicBunker() private void UpdatePanicBunker()
{ {
var hasAdmins = false; var admins = PanicBunker.CountDeadminnedAdmins
foreach (var admin in _adminManager.AllAdmins) ? _adminManager.AllAdmins
{ : _adminManager.ActiveAdmins;
if (_adminManager.HasAdminFlag(admin, AdminFlags.Admin, includeDeAdmin: PanicBunker.CountDeadminnedAdmins)) var hasAdmins = admins.Any();
{
hasAdmins = true;
break;
}
}
// TODO Fix order dependent Cvars // TODO Fix order dependent Cvars
// Please for the sake of my sanity don't make cvars & order dependent. // Please for the sake of my sanity don't make cvars & order dependent.

View File

@@ -204,7 +204,6 @@ 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"),
@@ -604,7 +603,6 @@ 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,

View File

@@ -7,7 +7,7 @@ namespace Content.Server.Administration.Toolshed;
public sealed class MarkedCommand : ToolshedCommand public sealed class MarkedCommand : ToolshedCommand
{ {
[CommandImplementation] [CommandImplementation]
public IEnumerable<EntityUid> Marked(IInvocationContext ctx) public IEnumerable<EntityUid> Marked([CommandInvocationContext] IInvocationContext ctx)
{ {
var res = (IEnumerable<EntityUid>?)ctx.ReadVar("marked"); var res = (IEnumerable<EntityUid>?)ctx.ReadVar("marked");
res ??= Array.Empty<EntityUid>(); res ??= Array.Empty<EntityUid>();

View File

@@ -23,7 +23,7 @@ public sealed class RejuvenateCommand : ToolshedCommand
} }
[CommandImplementation] [CommandImplementation]
public void Rejuvenate(IInvocationContext ctx) public void Rejuvenate([CommandInvocationContext] IInvocationContext ctx)
{ {
_rejuvenate ??= GetSys<RejuvenateSystem>(); _rejuvenate ??= GetSys<RejuvenateSystem>();
if (ExecutingEntity(ctx) is not { } ent) if (ExecutingEntity(ctx) is not { } ent)

View File

@@ -8,7 +8,6 @@ using Robust.Shared.Toolshed;
using Robust.Shared.Toolshed.Syntax; using Robust.Shared.Toolshed.Syntax;
using Robust.Shared.Toolshed.TypeParsers; using Robust.Shared.Toolshed.TypeParsers;
using System.Linq; using System.Linq;
using Robust.Shared.Prototypes;
namespace Content.Server.Administration.Toolshed; namespace Content.Server.Administration.Toolshed;
@@ -18,38 +17,48 @@ public sealed class SolutionCommand : ToolshedCommand
private SharedSolutionContainerSystem? _solutionContainer; private SharedSolutionContainerSystem? _solutionContainer;
[CommandImplementation("get")] [CommandImplementation("get")]
public SolutionRef? Get([PipedArgument] EntityUid input, string name) public SolutionRef? Get(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] EntityUid input,
[CommandArgument] ValueRef<string> name
)
{ {
_solutionContainer ??= GetSys<SharedSolutionContainerSystem>(); _solutionContainer ??= GetSys<SharedSolutionContainerSystem>();
if (_solutionContainer.TryGetSolution(input, name, out var solution)) if (_solutionContainer.TryGetSolution(input, name.Evaluate(ctx)!, out var solution))
return new SolutionRef(solution.Value); return new SolutionRef(solution.Value);
return null; return null;
} }
[CommandImplementation("get")] [CommandImplementation("get")]
public IEnumerable<SolutionRef> Get([PipedArgument] IEnumerable<EntityUid> input, string name) public IEnumerable<SolutionRef> Get(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] IEnumerable<EntityUid> input,
[CommandArgument] ValueRef<string> name
)
{ {
return input.Select(x => Get(x, name)).Where(x => x is not null).Cast<SolutionRef>(); return input.Select(x => Get(ctx, x, name)).Where(x => x is not null).Cast<SolutionRef>();
} }
[CommandImplementation("adjreagent")] [CommandImplementation("adjreagent")]
public SolutionRef AdjReagent( public SolutionRef AdjReagent(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] SolutionRef input, [PipedArgument] SolutionRef input,
ProtoId<ReagentPrototype> proto, [CommandArgument] Prototype<ReagentPrototype> name,
FixedPoint2 amount [CommandArgument] ValueRef<FixedPoint2> amountRef
) )
{ {
_solutionContainer ??= GetSys<SharedSolutionContainerSystem>(); _solutionContainer ??= GetSys<SharedSolutionContainerSystem>();
var amount = amountRef.Evaluate(ctx);
if (amount > 0) if (amount > 0)
{ {
_solutionContainer.TryAddReagent(input.Solution, proto, amount, out _); _solutionContainer.TryAddReagent(input.Solution, name.Value.ID, amount, out _);
} }
else if (amount < 0) else if (amount < 0)
{ {
_solutionContainer.RemoveReagent(input.Solution, proto, -amount); _solutionContainer.RemoveReagent(input.Solution, name.Value.ID, -amount);
} }
return input; return input;
@@ -57,11 +66,12 @@ public sealed class SolutionCommand : ToolshedCommand
[CommandImplementation("adjreagent")] [CommandImplementation("adjreagent")]
public IEnumerable<SolutionRef> AdjReagent( public IEnumerable<SolutionRef> AdjReagent(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] IEnumerable<SolutionRef> input, [PipedArgument] IEnumerable<SolutionRef> input,
ProtoId<ReagentPrototype> name, [CommandArgument] Prototype<ReagentPrototype> name,
FixedPoint2 amount [CommandArgument] ValueRef<FixedPoint2> amountRef
) )
=> input.Select(x => AdjReagent(x, name, amount)); => input.Select(x => AdjReagent(ctx, x, name, amountRef));
} }
public readonly record struct SolutionRef(Entity<SolutionComponent> Solution) public readonly record struct SolutionRef(Entity<SolutionComponent> Solution)

View File

@@ -36,50 +36,82 @@ public sealed class TagCommand : ToolshedCommand
} }
[CommandImplementation("add")] [CommandImplementation("add")]
public EntityUid Add([PipedArgument] EntityUid input, ProtoId<TagPrototype> tag) public EntityUid Add(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] EntityUid input,
[CommandArgument] ValueRef<string, Prototype<TagPrototype>> @ref
)
{ {
_tag ??= GetSys<TagSystem>(); _tag ??= GetSys<TagSystem>();
_tag.AddTag(input, tag); _tag.AddTag(input, @ref.Evaluate(ctx)!);
return input; return input;
} }
[CommandImplementation("add")] [CommandImplementation("add")]
public IEnumerable<EntityUid> Add([PipedArgument] IEnumerable<EntityUid> input, ProtoId<TagPrototype> tag) public IEnumerable<EntityUid> Add(
=> input.Select(x => Add(x, tag)); [CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] IEnumerable<EntityUid> input,
[CommandArgument] ValueRef<string, Prototype<TagPrototype>> @ref
)
=> input.Select(x => Add(ctx, x, @ref));
[CommandImplementation("rm")] [CommandImplementation("rm")]
public EntityUid Rm([PipedArgument] EntityUid input, ProtoId<TagPrototype> tag) public EntityUid Rm(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] EntityUid input,
[CommandArgument] ValueRef<string, Prototype<TagPrototype>> @ref
)
{ {
_tag ??= GetSys<TagSystem>(); _tag ??= GetSys<TagSystem>();
_tag.RemoveTag(input, tag); _tag.RemoveTag(input, @ref.Evaluate(ctx)!);
return input; return input;
} }
[CommandImplementation("rm")] [CommandImplementation("rm")]
public IEnumerable<EntityUid> Rm([PipedArgument] IEnumerable<EntityUid> input, ProtoId<TagPrototype> tag) public IEnumerable<EntityUid> Rm(
=> input.Select(x => Rm(x, tag)); [CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] IEnumerable<EntityUid> input,
[CommandArgument] ValueRef<string, Prototype<TagPrototype>> @ref
)
=> input.Select(x => Rm(ctx, x, @ref));
[CommandImplementation("addmany")] [CommandImplementation("addmany")]
public EntityUid AddMany([PipedArgument] EntityUid input, IEnumerable<ProtoId<TagPrototype>> tags) public EntityUid AddMany(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] EntityUid input,
[CommandArgument] ValueRef<IEnumerable<string>, IEnumerable<string>> @ref
)
{ {
_tag ??= GetSys<TagSystem>(); _tag ??= GetSys<TagSystem>();
_tag.AddTags(input, tags); _tag.AddTags(input, (IEnumerable<ProtoId<TagPrototype>>)@ref.Evaluate(ctx)!);
return input; return input;
} }
[CommandImplementation("addmany")] [CommandImplementation("addmany")]
public IEnumerable<EntityUid> AddMany([PipedArgument] IEnumerable<EntityUid> input, IEnumerable<ProtoId<TagPrototype>> tags) public IEnumerable<EntityUid> AddMany(
=> input.Select(x => AddMany(x, tags.ToArray())); [CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] IEnumerable<EntityUid> input,
[CommandArgument] ValueRef<IEnumerable<string>, IEnumerable<string>> @ref
)
=> input.Select(x => AddMany(ctx, x, @ref));
[CommandImplementation("rmmany")] [CommandImplementation("rmmany")]
public EntityUid RmMany([PipedArgument] EntityUid input, IEnumerable<ProtoId<TagPrototype>> tags) public EntityUid RmMany(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] EntityUid input,
[CommandArgument] ValueRef<IEnumerable<string>, IEnumerable<string>> @ref
)
{ {
_tag ??= GetSys<TagSystem>(); _tag ??= GetSys<TagSystem>();
_tag.RemoveTags(input, tags); _tag.RemoveTags(input, (IEnumerable<ProtoId<TagPrototype>>)@ref.Evaluate(ctx)!);
return input; return input;
} }
[CommandImplementation("rmmany")] [CommandImplementation("rmmany")]
public IEnumerable<EntityUid> RmMany([PipedArgument] IEnumerable<EntityUid> input, IEnumerable<ProtoId<TagPrototype>> tags) public IEnumerable<EntityUid> RmMany(
=> input.Select(x => RmMany(x, tags.ToArray())); [CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] IEnumerable<EntityUid> input,
[CommandArgument] ValueRef<IEnumerable<string>, IEnumerable<string>> @ref
)
=> input.Select(x => RmMany(ctx, x, @ref));
} }

View File

@@ -169,13 +169,11 @@ public sealed class AmeNodeGroup : BaseNodeGroup
public float CalculatePower(int fuel, int cores) public float CalculatePower(int fuel, int cores)
{ {
// Balanced around a single core AME with injection level 2 producing 120KW. // Balanced around a single core AME with injection level 2 producing 120KW.
// Two core with four injection is 150kW. Two core with two injection is 90kW. // Overclocking yields diminishing returns until it evens out at around 360KW.
// Increasing core count creates diminishing returns, increasing injection amount increases // The adjustment for cores make it so that a 1 core AME at 2 injections is better than a 2 core AME at 2 injections.
// Unlike the previous solution, increasing fuel and cores always leads to an increase in power, even if by very small amounts. // However, for the relative amounts for each (1 core at 2 and 2 core at 4), more cores has more output.
// Increasing core count without increasing fuel always leads to reduced power as well. return 200000f * MathF.Log10(fuel * fuel) * MathF.Pow(0.75f, cores - 1);
// At 18+ cores and 2 inject, the power produced is less than 0, the Max ensures the AME can never produce "negative" power.
return MathF.Max(200000f * MathF.Log10(2 * fuel * MathF.Pow(cores, (float)-0.5)), 0);
} }
public int GetTotalStability() public int GetTotalStability()

View File

@@ -1,4 +1,3 @@
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;
@@ -10,47 +9,44 @@ 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, Access(typeof(EggLayerSystem)), AutoGenerateComponentPause] [RegisterComponent]
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";
[DataField] /// <summary>
public SoundSpecifier EggLaySound = new SoundPathSpecifier("/Audio/Effects/pop.ogg"); /// The amount of nutrient consumed on update.
/// </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] [DataField, ViewVariables(VVAccess.ReadWrite)]
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] [DataField, ViewVariables(VVAccess.ReadWrite)]
public float EggLayCooldownMax = 120f; public float EggLayCooldownMax = 120f;
/// <summary> /// <summary>
/// The amount of nutrient consumed on update. /// Set during component init.
/// </summary> /// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public float CurrentEggLayCooldown;
[DataField(required: true), ViewVariables(VVAccess.ReadWrite)]
public List<EntitySpawnEntry> EggSpawn = default!;
[DataField] [DataField]
public float HungerUsage = 60f; public SoundSpecifier EggLaySound = new SoundPathSpecifier("/Audio/Effects/pop.ogg");
[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;
} }

View File

@@ -0,0 +1,59 @@
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);
}
}

View File

@@ -1,22 +1,23 @@
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.Shared.Animals; namespace Content.Server.Animals.Components;
/// <summary> /// <summary>
/// Gives the ability to produce wool fibers; /// Lets an entity produce wool fibers. Uses hunger if present.
/// 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, AutoNetworkedField] [DataField, ViewVariables(VVAccess.ReadOnly)]
public ProtoId<ReagentPrototype> ReagentId = "Fiber"; public ProtoId<ReagentPrototype> ReagentId = "Fiber";
/// <summary> /// <summary>
@@ -28,30 +29,30 @@ public sealed partial class WoolyComponent : Component
/// <summary> /// <summary>
/// The solution to add reagent to. /// The solution to add reagent to.
/// </summary> /// </summary>
[DataField, ViewVariables(VVAccess.ReadOnly)] [DataField]
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, AutoNetworkedField] [DataField, ViewVariables(VVAccess.ReadOnly)]
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, AutoNetworkedField] [DataField, ViewVariables(VVAccess.ReadWrite)]
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, AutoNetworkedField] [DataField, ViewVariables(VVAccess.ReadWrite)]
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, AutoPausedField, Access(typeof(WoolySystem))] [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite)]
public TimeSpan NextGrowth = TimeSpan.Zero; public TimeSpan NextGrowth = TimeSpan.FromSeconds(0);
} }

View File

@@ -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 the ability to lay eggs/other things; /// Gives ability to produce eggs, produces endless if the
/// produces endlessly if the owner does not have a HungerComponent. /// owner has no HungerComponent
/// </summary> /// </summary>
public sealed class EggLayerSystem : EntitySystem public sealed class EggLayerSystem : EntitySystem
{ {
@@ -23,7 +23,6 @@ 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!;
@@ -38,6 +37,7 @@ 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,17 +45,13 @@ public sealed class EggLayerSystem : EntitySystem
if (HasComp<ActorComponent>(uid)) if (HasComp<ActorComponent>(uid))
continue; continue;
if (_timing.CurTime < eggLayer.NextGrowth) eggLayer.AccumulatedFrametime += frameTime;
if (eggLayer.AccumulatedFrametime < eggLayer.CurrentEggLayCooldown)
continue; continue;
// Randomize next growth time for more organic egglaying. eggLayer.AccumulatedFrametime -= eggLayer.CurrentEggLayCooldown;
eggLayer.NextGrowth += TimeSpan.FromSeconds(_random.NextFloat(eggLayer.EggLayCooldownMin, eggLayer.EggLayCooldownMax)); eggLayer.CurrentEggLayCooldown = _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);
} }
@@ -64,12 +60,11 @@ 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.NextGrowth = _timing.CurTime + TimeSpan.FromSeconds(_random.NextFloat(component.EggLayCooldownMin, component.EggLayCooldownMax)); component.CurrentEggLayCooldown = _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);
} }
@@ -81,10 +76,10 @@ 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.GetHunger(hunger) < egglayer.HungerUsage) if (hunger.CurrentHunger < egglayer.HungerUsage)
{ {
_popup.PopupEntity(Loc.GetString("action-popup-lay-egg-too-hungry"), uid, uid); _popup.PopupEntity(Loc.GetString("action-popup-lay-egg-too-hungry"), uid, uid);
return false; return false;

View File

@@ -1,7 +1,8 @@
using Content.Shared.Chemistry.Components; using Content.Server.Animals.Components;
using Content.Server.Popups;
using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Components;
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;
@@ -11,17 +12,18 @@ using Content.Shared.Udder;
using Content.Shared.Verbs; using Content.Shared.Verbs;
using Robust.Shared.Timing; using Robust.Shared.Timing;
namespace Content.Shared.Animals; namespace Content.Server.Animals.Systems;
/// <summary> /// <summary>
/// Gives the ability to produce milkable reagents; /// Gives ability to produce milkable reagents, produces endless if the
/// produces endlessly if the owner does not have a HungerComponent. /// owner has no HungerComponent
/// </summary> /// </summary>
public sealed class UdderSystem : EntitySystem internal 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 SharedPopupSystem _popupSystem = default!; [Dependency] private readonly PopupSystem _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!;
@@ -29,37 +31,26 @@ public 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 (_timing.CurTime < udder.NextGrowth) if (now < udder.NextGrowth)
continue; continue;
udder.NextGrowth += udder.GrowthDelay; udder.NextGrowth = now + 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))
{ {
@@ -70,6 +61,9 @@ public 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 _);
} }
@@ -105,7 +99,7 @@ public sealed class UdderSystem : EntitySystem
var quantity = solution.Volume; var quantity = solution.Volume;
if (quantity == 0) if (quantity == 0)
{ {
_popupSystem.PopupClient(Loc.GetString("udder-system-dry"), entity.Owner, args.Args.User); _popupSystem.PopupEntity(Loc.GetString("udder-system-dry"), entity.Owner, args.Args.User);
return; return;
} }
@@ -115,7 +109,7 @@ public 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.PopupClient(Loc.GetString("udder-system-success", ("amount", quantity), ("target", Identity.Entity(args.Args.Used.Value, EntityManager))), entity.Owner, _popupSystem.PopupEntity(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);
} }
@@ -140,50 +134,4 @@ public 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);
}
} }

View File

@@ -1,15 +1,16 @@
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.Shared.Animals; namespace Content.Server.Animals.Systems;
/// <summary> /// <summary>
/// Gives ability to produce fiber reagents; /// Gives ability to produce fiber reagents, produces endless if the
/// produces endlessly if the owner has no HungerComponent. /// owner has no HungerComponent
/// </summary> /// </summary>
public sealed class WoolySystem : EntitySystem public sealed class WoolySystem : EntitySystem
{ {
@@ -23,12 +24,6 @@ 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)
@@ -36,22 +31,17 @@ 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 (_timing.CurTime < wooly.NextGrowth) if (now < wooly.NextGrowth)
continue; continue;
wooly.NextGrowth += wooly.GrowthDelay; wooly.NextGrowth = now + 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))
{ {
@@ -62,6 +52,9 @@ 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 _);
} }
} }

View File

@@ -186,11 +186,6 @@ 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
} }

View File

@@ -1,542 +0,0 @@
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;
}
}

View File

@@ -11,7 +11,6 @@ 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;

View File

@@ -0,0 +1,31 @@
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;
}
}

View File

@@ -1,57 +1,169 @@
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.Components; using Content.Shared.Atmos.Piping;
using Content.Shared.Atmos.EntitySystems; using Content.Shared.Atmos.Piping.Binary.Components;
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
{ {
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!; [UsedImplicitly]
[Dependency] private readonly SharedAmbientSoundSystem _ambientSoundSystem = default!; public sealed class GasPressurePumpSystem : EntitySystem
[Dependency] private readonly NodeContainerSystem _nodeContainer = default!;
public override void Initialize()
{ {
base.Initialize(); [Dependency] private readonly UserInterfaceSystem _userInterfaceSystem = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
[Dependency] private readonly SharedAmbientSoundSystem _ambientSoundSystem = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly NodeContainerSystem _nodeContainer = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
SubscribeLocalEvent<GasPressurePumpComponent, AtmosDeviceUpdateEvent>(OnPumpUpdated); public override void Initialize()
}
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); base.Initialize();
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);
} }
var outputStartingPressure = outlet.Air.Pressure; private void OnInit(EntityUid uid, GasPressurePumpComponent pump, ComponentInit args)
if (outputStartingPressure >= pump.TargetPressure)
{ {
_ambientSoundSystem.SetAmbience(uid, false); UpdateAppearance(uid, pump);
return; // No need to pump gas if target has been reached.
} }
if (inlet.Air.TotalMoles > 0 && inlet.Air.Temperature > 0) private void OnExamined(EntityUid uid, GasPressurePumpComponent pump, ExaminedEvent args)
{ {
// We calculate the necessary moles to transfer using our good ol' friend PV=nRT. if (!EntityManager.GetComponent<TransformComponent>(uid).Anchored || !args.IsInDetailsRange) // Not anchored? Out of range? No status.
var pressureDelta = pump.TargetPressure - outputStartingPressure; return;
var transferMoles = (pressureDelta * outlet.Air.Volume) / (inlet.Air.Temperature * Atmospherics.R);
var removed = inlet.Air.Remove(transferMoles); if (Loc.TryGetString("gas-pressure-pump-system-examined", out var str,
_atmosphereSystem.Merge(outlet.Air, removed); ("statusColor", "lightblue"), // TODO: change with pressure?
_ambientSoundSystem.SetAmbience(uid, removed.TotalMoles > 0f); ("pressure", pump.TargetPressure)
))
{
args.PushMarkup(str);
}
}
private void OnPowerChanged(EntityUid uid, GasPressurePumpComponent component, ref PowerChangedEvent args)
{
UpdateAppearance(uid, component);
}
private void OnPumpUpdated(EntityUid uid, GasPressurePumpComponent pump, ref AtmosDeviceUpdateEvent args)
{
if (!pump.Enabled
|| (TryComp<ApcPowerReceiverComponent>(uid, out var power) && !power.Powered)
|| !_nodeContainer.TryGetNodes(uid, pump.InletName, pump.OutletName, out PipeNode? inlet, out PipeNode? outlet))
{
_ambientSoundSystem.SetAmbience(uid, false);
return;
}
var outputStartingPressure = outlet.Air.Pressure;
if (outputStartingPressure >= pump.TargetPressure)
{
_ambientSoundSystem.SetAmbience(uid, false);
return; // No need to pump gas if target has been reached.
}
if (inlet.Air.TotalMoles > 0 && inlet.Air.Temperature > 0)
{
// We calculate the necessary moles to transfer using our good ol' friend PV=nRT.
var pressureDelta = pump.TargetPressure - outputStartingPressure;
var transferMoles = (pressureDelta * outlet.Air.Volume) / (inlet.Air.Temperature * Atmospherics.R);
var removed = inlet.Air.Remove(transferMoles);
_atmosphereSystem.Merge(outlet.Air, removed);
_ambientSoundSystem.SetAmbience(uid, removed.TotalMoles > 0f);
}
}
private void OnPumpLeaveAtmosphere(EntityUid uid, GasPressurePumpComponent pump, ref AtmosDeviceDisabledEvent args)
{
pump.Enabled = false;
UpdateAppearance(uid, pump);
DirtyUI(uid, pump);
_userInterfaceSystem.CloseUi(uid, GasPressurePumpUiKey.Key);
}
private void OnPumpActivate(EntityUid uid, GasPressurePumpComponent pump, ActivateInWorldEvent args)
{
if (args.Handled || !args.Complex)
return;
if (!EntityManager.TryGetComponent(args.User, out ActorComponent? actor))
return;
if (Transform(uid).Anchored)
{
_userInterfaceSystem.OpenUi(uid, GasPressurePumpUiKey.Key, actor.PlayerSession);
DirtyUI(uid, pump);
}
else
{
_popup.PopupCursor(Loc.GetString("comp-gas-pump-ui-needs-anchor"), args.User);
}
args.Handled = true;
}
private void OnToggleStatusMessage(EntityUid uid, GasPressurePumpComponent pump, GasPressurePumpToggleStatusMessage args)
{
pump.Enabled = args.Enabled;
_adminLogger.Add(LogType.AtmosPowerChanged, LogImpact.Medium,
$"{ToPrettyString(args.Actor):player} set the power on {ToPrettyString(uid):device} to {args.Enabled}");
DirtyUI(uid, pump);
UpdateAppearance(uid, pump);
}
private void OnOutputPressureChangeMessage(EntityUid uid, GasPressurePumpComponent pump, GasPressurePumpChangeOutputPressureMessage args)
{
pump.TargetPressure = Math.Clamp(args.Pressure, 0f, Atmospherics.MaxOutputPressure);
_adminLogger.Add(LogType.AtmosPressureChanged, LogImpact.Medium,
$"{ToPrettyString(args.Actor):player} set the pressure on {ToPrettyString(uid):device} to {args.Pressure}kPa");
DirtyUI(uid, pump);
}
private void DirtyUI(EntityUid uid, GasPressurePumpComponent? pump)
{
if (!Resolve(uid, ref pump))
return;
_userInterfaceSystem.SetUiState(uid, GasPressurePumpUiKey.Key,
new GasPressurePumpBoundUserInterfaceState(EntityManager.GetComponent<MetaDataComponent>(uid).EntityName, pump.TargetPressure, pump.Enabled));
}
private void UpdateAppearance(EntityUid uid, GasPressurePumpComponent? pump = null, AppearanceComponent? appearance = null)
{
if (!Resolve(uid, ref pump, ref appearance, false))
return;
bool pumpOn = pump.Enabled && (TryComp<ApcPowerReceiverComponent>(uid, out var power) && power.Powered);
_appearance.SetData(uid, PumpVisuals.Enabled, pumpOn, appearance);
} }
} }
} }

View File

@@ -6,7 +6,6 @@ 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;

View File

@@ -10,7 +10,6 @@ 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;

View File

@@ -67,3 +67,15 @@ 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;

View File

@@ -1,24 +1,19 @@
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] [RegisterComponent]
public Color Color { get; set; } = Color.White; public sealed partial class AtmosPipeColorComponent : Component
[ViewVariables(VVAccess.ReadWrite), UsedImplicitly]
public Color ColorVV
{ {
get => Color; [DataField("color")]
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;
}

View File

@@ -1,7 +1,6 @@
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;

View File

@@ -40,9 +40,6 @@ 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);
} }
} }
} }

View File

@@ -1,7 +1,6 @@
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;
@@ -9,6 +8,7 @@ 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,12 +16,11 @@ 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, UserUnanchoredEvent>(OnUserUnanchored); SubscribeLocalEvent<AtmosUnsafeUnanchorComponent, BeforeUnanchoredEvent>(OnBeforeUnanchored);
SubscribeLocalEvent<AtmosUnsafeUnanchorComponent, UnanchorAttemptEvent>(OnUnanchorAttempt); SubscribeLocalEvent<AtmosUnsafeUnanchorComponent, UnanchorAttemptEvent>(OnUnanchorAttempt);
SubscribeLocalEvent<AtmosUnsafeUnanchorComponent, BreakageEventArgs>(OnBreak); SubscribeLocalEvent<AtmosUnsafeUnanchorComponent, BreakageEventArgs>(OnBreak);
} }
@@ -49,22 +48,15 @@ namespace Content.Server.Atmos.Piping.EntitySystems
} }
} }
// When unanchoring a pipe, leak the gas that was inside the pipe element. private void OnBeforeUnanchored(EntityUid uid, AtmosUnsafeUnanchorComponent component, BeforeUnanchoredEvent args)
// 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, false); LeakGas(uid);
// 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);
@@ -72,17 +64,32 @@ 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, bool removeFromPipe = true) public void LeakGas(EntityUid uid)
{ {
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)
@@ -90,13 +97,7 @@ namespace Content.Server.Atmos.Piping.EntitySystems
if (node is not PipeNode pipe) if (node is not PipeNode pipe)
continue; continue;
if (removeFromPipe) _atmosphere.Merge(buffer, pipe.Air.Remove(sharedLoss));
_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);

View File

@@ -7,7 +7,6 @@ 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;

View File

@@ -7,7 +7,6 @@ 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;

Some files were not shown because too many files have changed in this diff Show More