Merge remote-tracking branch 'wizards/master' into upstream-sync

# Conflicts:
#	.github/CODEOWNERS
#	.github/labeler.yml
#	Content.Server/Connection/ConnectionManager.cs
#	Resources/Prototypes/Datasets/Names/ai.yml
#	Resources/Prototypes/Datasets/Names/arachnid_first.yml
#	Resources/Prototypes/Datasets/Names/diona.yml
#	Resources/Prototypes/Datasets/Names/fake_human_first.yml
#	Resources/Prototypes/Datasets/Names/fake_human_last.yml
#	Resources/Prototypes/Datasets/Names/last.yml
#	Resources/Prototypes/Datasets/Names/vox.yml
#	Resources/Prototypes/Entities/Clothing/Shoes/specific.yml
#	Resources/Prototypes/Entities/Structures/Machines/lathe.yml
#	Resources/Prototypes/Loadouts/role_loadouts.yml
#	Resources/Prototypes/Maps/bagel.yml
#	Resources/Prototypes/SoundCollections/NukeMusic.yml
#	Resources/Prototypes/Voice/speech_emotes.yml
#	Resources/ServerInfo/Guidebook/Mobs/Vox.xml
#	Resources/ServerInfo/Guidebook/Science/APE.xml
#	Resources/Textures/Clothing/Head/Hats/warden.rsi/inhand-left.png
#	Resources/Textures/Clothing/Head/Hats/warden.rsi/inhand-right.png
#	Resources/Textures/Clothing/Head/Hats/warden.rsi/meta.json
This commit is contained in:
Morb0
2024-12-26 00:30:02 +03:00
570 changed files with 385979 additions and 38348 deletions

4
.github/labeler.yml vendored
View File

@@ -17,6 +17,10 @@
- changed-files:
- any-glob-to-any-file: '**/*.swsl'
"Changes: Audio":
- changed-files:
- any-glob-to-any-file: '**/*.ogg'
"Changes: Localization":
- changed-files:
- any-glob-to-any-file: '**/*.ftl'

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -31,19 +31,6 @@ public sealed partial class AtmosAlarmEntryContainer : BoxContainer
[AtmosAlarmType.Danger] = "atmos-alerts-window-danger-state",
};
private Dictionary<Gas, string> _gasShorthands = new Dictionary<Gas, string>()
{
[Gas.Ammonia] = "NH₃",
[Gas.CarbonDioxide] = "CO₂",
[Gas.Frezon] = "F",
[Gas.Nitrogen] = "N₂",
[Gas.NitrousOxide] = "N₂O",
[Gas.Oxygen] = "O₂",
[Gas.Plasma] = "P",
[Gas.Tritium] = "T",
[Gas.WaterVapor] = "H₂O",
};
public AtmosAlarmEntryContainer(NetEntity uid, EntityCoordinates? coordinates)
{
RobustXamlLoader.Load(this);
@@ -162,12 +149,11 @@ public sealed partial class AtmosAlarmEntryContainer : BoxContainer
foreach ((var gas, (var mol, var percent, var alert)) in keyValuePairs)
{
FixedPoint2 gasPercent = percent * 100f;
var gasShorthand = _gasShorthands.GetValueOrDefault(gas, "X");
var gasAbbreviation = Atmospherics.GasAbbreviations.GetValueOrDefault(gas, Loc.GetString("gas-unknown-abbreviation"));
var gasLabel = new Label()
{
Text = Loc.GetString("atmos-alerts-window-other-gases-value", ("shorthand", gasShorthand), ("value", gasPercent)),
Text = Loc.GetString("atmos-alerts-window-other-gases-value", ("shorthand", gasAbbreviation), ("value", gasPercent)),
FontOverride = normalFont,
FontColorOverride = GetAlarmStateColor(alert),
HorizontalAlignment = HAlignment.Center,

View File

@@ -0,0 +1,40 @@
using Content.Shared.Atmos.Components;
namespace Content.Client.Atmos.Consoles;
public sealed class AtmosMonitoringConsoleBoundUserInterface : BoundUserInterface
{
[ViewVariables]
private AtmosMonitoringConsoleWindow? _menu;
public AtmosMonitoringConsoleBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) { }
protected override void Open()
{
base.Open();
_menu = new AtmosMonitoringConsoleWindow(this, Owner);
_menu.OpenCentered();
_menu.OnClose += Close;
}
protected override void UpdateState(BoundUserInterfaceState state)
{
base.UpdateState(state);
if (state is not AtmosMonitoringConsoleBoundInterfaceState castState)
return;
EntMan.TryGetComponent<TransformComponent>(Owner, out var xform);
_menu?.UpdateUI(xform?.Coordinates, castState.AtmosNetworks);
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (!disposing)
return;
_menu?.Dispose();
}
}

View File

@@ -0,0 +1,295 @@
using Content.Client.Pinpointer.UI;
using Content.Shared.Atmos.Components;
using Content.Shared.Pinpointer;
using Robust.Client.Graphics;
using Robust.Shared.Collections;
using Robust.Shared.Map.Components;
using System.Linq;
using System.Numerics;
namespace Content.Client.Atmos.Consoles;
public sealed partial class AtmosMonitoringConsoleNavMapControl : NavMapControl
{
[Dependency] private readonly IEntityManager _entManager = default!;
public bool ShowPipeNetwork = true;
public int? FocusNetId = null;
private const int ChunkSize = 4;
private readonly Color _basePipeNetColor = Color.LightGray;
private readonly Color _unfocusedPipeNetColor = Color.DimGray;
private List<AtmosMonitoringConsoleLine> _atmosPipeNetwork = new();
private Dictionary<Color, Color> _sRGBLookUp = new Dictionary<Color, Color>();
// Look up tables for merging continuous lines. Indexed by line color
private Dictionary<Color, Dictionary<Vector2i, Vector2i>> _horizLines = new();
private Dictionary<Color, Dictionary<Vector2i, Vector2i>> _horizLinesReversed = new();
private Dictionary<Color, Dictionary<Vector2i, Vector2i>> _vertLines = new();
private Dictionary<Color, Dictionary<Vector2i, Vector2i>> _vertLinesReversed = new();
public AtmosMonitoringConsoleNavMapControl() : base()
{
PostWallDrawingAction += DrawAllPipeNetworks;
}
protected override void UpdateNavMap()
{
base.UpdateNavMap();
if (!_entManager.TryGetComponent<AtmosMonitoringConsoleComponent>(Owner, out var console))
return;
if (!_entManager.TryGetComponent<MapGridComponent>(MapUid, out var grid))
return;
_atmosPipeNetwork = GetDecodedAtmosPipeChunks(console.AtmosPipeChunks, grid);
}
private void DrawAllPipeNetworks(DrawingHandleScreen handle)
{
if (!ShowPipeNetwork)
return;
// Draw networks
if (_atmosPipeNetwork != null && _atmosPipeNetwork.Any())
DrawPipeNetwork(handle, _atmosPipeNetwork);
}
private void DrawPipeNetwork(DrawingHandleScreen handle, List<AtmosMonitoringConsoleLine> atmosPipeNetwork)
{
var offset = GetOffset();
offset = offset with { Y = -offset.Y };
if (WorldRange / WorldMaxRange > 0.5f)
{
var pipeNetworks = new Dictionary<Color, ValueList<Vector2>>();
foreach (var chunkedLine in atmosPipeNetwork)
{
var start = ScalePosition(chunkedLine.Origin - offset);
var end = ScalePosition(chunkedLine.Terminus - offset);
if (!pipeNetworks.TryGetValue(chunkedLine.Color, out var subNetwork))
subNetwork = new ValueList<Vector2>();
subNetwork.Add(start);
subNetwork.Add(end);
pipeNetworks[chunkedLine.Color] = subNetwork;
}
foreach ((var color, var subNetwork) in pipeNetworks)
{
if (subNetwork.Count > 0)
handle.DrawPrimitives(DrawPrimitiveTopology.LineList, subNetwork.Span, color);
}
}
else
{
var pipeVertexUVs = new Dictionary<Color, ValueList<Vector2>>();
foreach (var chunkedLine in atmosPipeNetwork)
{
var leftTop = ScalePosition(new Vector2
(Math.Min(chunkedLine.Origin.X, chunkedLine.Terminus.X) - 0.1f,
Math.Min(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) - 0.1f)
- offset);
var rightTop = ScalePosition(new Vector2
(Math.Max(chunkedLine.Origin.X, chunkedLine.Terminus.X) + 0.1f,
Math.Min(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) - 0.1f)
- offset);
var leftBottom = ScalePosition(new Vector2
(Math.Min(chunkedLine.Origin.X, chunkedLine.Terminus.X) - 0.1f,
Math.Max(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) + 0.1f)
- offset);
var rightBottom = ScalePosition(new Vector2
(Math.Max(chunkedLine.Origin.X, chunkedLine.Terminus.X) + 0.1f,
Math.Max(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) + 0.1f)
- offset);
if (!pipeVertexUVs.TryGetValue(chunkedLine.Color, out var pipeVertexUV))
pipeVertexUV = new ValueList<Vector2>();
pipeVertexUV.Add(leftBottom);
pipeVertexUV.Add(leftTop);
pipeVertexUV.Add(rightBottom);
pipeVertexUV.Add(leftTop);
pipeVertexUV.Add(rightBottom);
pipeVertexUV.Add(rightTop);
pipeVertexUVs[chunkedLine.Color] = pipeVertexUV;
}
foreach ((var color, var pipeVertexUV) in pipeVertexUVs)
{
if (pipeVertexUV.Count > 0)
handle.DrawPrimitives(DrawPrimitiveTopology.TriangleList, pipeVertexUV.Span, color);
}
}
}
private List<AtmosMonitoringConsoleLine> GetDecodedAtmosPipeChunks(Dictionary<Vector2i, AtmosPipeChunk>? chunks, MapGridComponent? grid)
{
var decodedOutput = new List<AtmosMonitoringConsoleLine>();
if (chunks == null || grid == null)
return decodedOutput;
// Clear stale look up table values
_horizLines.Clear();
_horizLinesReversed.Clear();
_vertLines.Clear();
_vertLinesReversed.Clear();
// Generate masks
var northMask = (ulong)1 << 0;
var southMask = (ulong)1 << 1;
var westMask = (ulong)1 << 2;
var eastMask = (ulong)1 << 3;
foreach ((var chunkOrigin, var chunk) in chunks)
{
var list = new List<AtmosMonitoringConsoleLine>();
foreach (var ((netId, hexColor), atmosPipeData) in chunk.AtmosPipeData)
{
// Determine the correct coloration for the pipe
var color = Color.FromHex(hexColor) * _basePipeNetColor;
if (FocusNetId != null && FocusNetId != netId)
color *= _unfocusedPipeNetColor;
// Get the associated line look up tables
if (!_horizLines.TryGetValue(color, out var horizLines))
{
horizLines = new();
_horizLines[color] = horizLines;
}
if (!_horizLinesReversed.TryGetValue(color, out var horizLinesReversed))
{
horizLinesReversed = new();
_horizLinesReversed[color] = horizLinesReversed;
}
if (!_vertLines.TryGetValue(color, out var vertLines))
{
vertLines = new();
_vertLines[color] = vertLines;
}
if (!_vertLinesReversed.TryGetValue(color, out var vertLinesReversed))
{
vertLinesReversed = new();
_vertLinesReversed[color] = vertLinesReversed;
}
// Loop over the chunk
for (var tileIdx = 0; tileIdx < ChunkSize * ChunkSize; tileIdx++)
{
if (atmosPipeData == 0)
continue;
var mask = (ulong)SharedNavMapSystem.AllDirMask << tileIdx * SharedNavMapSystem.Directions;
if ((atmosPipeData & mask) == 0)
continue;
var relativeTile = GetTileFromIndex(tileIdx);
var tile = (chunk.Origin * ChunkSize + relativeTile) * grid.TileSize;
tile = tile with { Y = -tile.Y };
// Calculate the draw point offsets
var vertLineOrigin = (atmosPipeData & northMask << tileIdx * SharedNavMapSystem.Directions) > 0 ?
new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 1f) : new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 0.5f);
var vertLineTerminus = (atmosPipeData & southMask << tileIdx * SharedNavMapSystem.Directions) > 0 ?
new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 0f) : new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 0.5f);
var horizLineOrigin = (atmosPipeData & eastMask << tileIdx * SharedNavMapSystem.Directions) > 0 ?
new Vector2(grid.TileSize * 1f, -grid.TileSize * 0.5f) : new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 0.5f);
var horizLineTerminus = (atmosPipeData & westMask << tileIdx * SharedNavMapSystem.Directions) > 0 ?
new Vector2(grid.TileSize * 0f, -grid.TileSize * 0.5f) : new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 0.5f);
// Since we can have pipe lines that have a length of a half tile,
// double the vectors and convert to vector2i so we can merge them
AddOrUpdateNavMapLine(ConvertVector2ToVector2i(tile + horizLineOrigin, 2), ConvertVector2ToVector2i(tile + horizLineTerminus, 2), horizLines, horizLinesReversed);
AddOrUpdateNavMapLine(ConvertVector2ToVector2i(tile + vertLineOrigin, 2), ConvertVector2ToVector2i(tile + vertLineTerminus, 2), vertLines, vertLinesReversed);
}
}
}
// Scale the vector2is back down and convert to vector2
foreach (var (color, horizLines) in _horizLines)
{
// Get the corresponding sRBG color
var sRGB = GetsRGBColor(color);
foreach (var (origin, terminal) in horizLines)
decodedOutput.Add(new AtmosMonitoringConsoleLine
(ConvertVector2iToVector2(origin, 0.5f), ConvertVector2iToVector2(terminal, 0.5f), sRGB));
}
foreach (var (color, vertLines) in _vertLines)
{
// Get the corresponding sRBG color
var sRGB = GetsRGBColor(color);
foreach (var (origin, terminal) in vertLines)
decodedOutput.Add(new AtmosMonitoringConsoleLine
(ConvertVector2iToVector2(origin, 0.5f), ConvertVector2iToVector2(terminal, 0.5f), sRGB));
}
return decodedOutput;
}
private Vector2 ConvertVector2iToVector2(Vector2i vector, float scale = 1f)
{
return new Vector2(vector.X * scale, vector.Y * scale);
}
private Vector2i ConvertVector2ToVector2i(Vector2 vector, float scale = 1f)
{
return new Vector2i((int)MathF.Round(vector.X * scale), (int)MathF.Round(vector.Y * scale));
}
private Vector2i GetTileFromIndex(int index)
{
var x = index / ChunkSize;
var y = index % ChunkSize;
return new Vector2i(x, y);
}
private Color GetsRGBColor(Color color)
{
if (!_sRGBLookUp.TryGetValue(color, out var sRGB))
{
sRGB = Color.ToSrgb(color);
_sRGBLookUp[color] = sRGB;
}
return sRGB;
}
}
public struct AtmosMonitoringConsoleLine
{
public readonly Vector2 Origin;
public readonly Vector2 Terminus;
public readonly Color Color;
public AtmosMonitoringConsoleLine(Vector2 origin, Vector2 terminus, Color color)
{
Origin = origin;
Terminus = terminus;
Color = color;
}
}

View File

@@ -0,0 +1,69 @@
using Content.Shared.Atmos.Components;
using Content.Shared.Atmos.Consoles;
using Robust.Shared.GameStates;
namespace Content.Client.Atmos.Consoles;
public sealed class AtmosMonitoringConsoleSystem : SharedAtmosMonitoringConsoleSystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<AtmosMonitoringConsoleComponent, ComponentHandleState>(OnHandleState);
}
private void OnHandleState(EntityUid uid, AtmosMonitoringConsoleComponent component, ref ComponentHandleState args)
{
Dictionary<Vector2i, Dictionary<(int, string), ulong>> modifiedChunks;
Dictionary<NetEntity, AtmosDeviceNavMapData> atmosDevices;
switch (args.Current)
{
case AtmosMonitoringConsoleDeltaState delta:
{
modifiedChunks = delta.ModifiedChunks;
atmosDevices = delta.AtmosDevices;
foreach (var index in component.AtmosPipeChunks.Keys)
{
if (!delta.AllChunks!.Contains(index))
component.AtmosPipeChunks.Remove(index);
}
break;
}
case AtmosMonitoringConsoleState state:
{
modifiedChunks = state.Chunks;
atmosDevices = state.AtmosDevices;
foreach (var index in component.AtmosPipeChunks.Keys)
{
if (!state.Chunks.ContainsKey(index))
component.AtmosPipeChunks.Remove(index);
}
break;
}
default:
return;
}
foreach (var (origin, chunk) in modifiedChunks)
{
var newChunk = new AtmosPipeChunk(origin);
newChunk.AtmosPipeData = new Dictionary<(int, string), ulong>(chunk);
component.AtmosPipeChunks[origin] = newChunk;
}
component.AtmosDevices.Clear();
foreach (var (nuid, atmosDevice) in atmosDevices)
{
component.AtmosDevices[nuid] = atmosDevice;
}
}
}

View File

@@ -0,0 +1,99 @@
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:ui="clr-namespace:Content.Client.Atmos.Consoles"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
Title="{Loc 'atmos-monitoring-window-title'}"
Resizable="False"
SetSize="1120 750"
MinSize="1120 750">
<BoxContainer Orientation="Vertical">
<!-- Main display -->
<BoxContainer Orientation="Horizontal" VerticalExpand="True" HorizontalExpand="True">
<!-- Nav map -->
<BoxContainer Orientation="Vertical" VerticalExpand="True" HorizontalExpand="True">
<ui:AtmosMonitoringConsoleNavMapControl Name="NavMap" Margin="5 5" VerticalExpand="True" HorizontalExpand="True">
<!-- System warning -->
<PanelContainer Name="SystemWarningPanel"
HorizontalAlignment="Center"
VerticalAlignment="Top"
HorizontalExpand="True"
Margin="0 48 0 0"
Visible="False">
<RichTextLabel Name="SystemWarningLabel" Margin="12 8 12 8"/>
</PanelContainer>
</ui:AtmosMonitoringConsoleNavMapControl>
<!-- Nav map legend -->
<BoxContainer Orientation="Horizontal" Margin="0 10 0 10">
<TextureRect Stretch="KeepAspectCentered"
TexturePath="/Textures/Interface/NavMap/beveled_square.png"
Modulate="#a9a9a9"
SetSize="16 16"
Margin="20 0 5 0"/>
<Label Text="{Loc 'atmos-monitoring-window-label-gas-opening'}"/>
<TextureRect Stretch="KeepAspectCentered"
TexturePath="/Textures/Interface/NavMap/beveled_circle.png"
SetSize="16 16"
Modulate="#a9a9a9"
Margin="20 0 5 0"/>
<Label Text="{Loc 'atmos-monitoring-window-label-gas-scrubber'}"/>
<TextureRect Stretch="KeepAspectCentered"
TexturePath="/Textures/Interface/NavMap/beveled_arrow_east.png"
SetSize="16 16"
Modulate="#a9a9a9"
Margin="20 0 5 0"/>
<Label Text="{Loc 'atmos-monitoring-window-label-gas-flow-regulator'}"/>
<TextureRect Stretch="KeepAspectCentered"
TexturePath="/Textures/Interface/NavMap/beveled_hexagon.png"
SetSize="16 16"
Modulate="#a9a9a9"
Margin="20 0 5 0"/>
<Label Text="{Loc 'atmos-monitoring-window-label-thermoregulator'}"/>
</BoxContainer>
</BoxContainer>
<!-- Atmosphere status -->
<BoxContainer Orientation="Vertical" VerticalExpand="True" SetWidth="440" Margin="0 0 10 10">
<!-- Station name -->
<controls:StripeBack>
<PanelContainer>
<RichTextLabel Name="StationName" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="0 5 0 3"/>
</PanelContainer>
</controls:StripeBack>
<!-- Alarm status (entries added by C# code) -->
<TabContainer Name="MasterTabContainer" VerticalExpand="True" HorizontalExpand="True" Margin="0 10 0 0">
<ScrollContainer HorizontalExpand="True" Margin="8, 8, 8, 8">
<BoxContainer Name="AtmosNetworksTable" Orientation="Vertical" VerticalExpand="True" HorizontalExpand="True" Margin="0 0 0 10"/>
</ScrollContainer>
</TabContainer>
<!-- Overlay toggles -->
<BoxContainer Orientation="Vertical" Margin="0 10 0 0">
<Label Text="{Loc 'atmos-monitoring-window-toggle-overlays'}" Margin="0 0 0 5"/>
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
<CheckBox Name="ShowPipeNetwork" Text="{Loc 'atmos-monitoring-window-show-pipe-network'}" Pressed="True" HorizontalExpand="True"/>
<CheckBox Name="ShowGasPipeSensors" Text="{Loc 'atmos-monitoring-window-show-gas-pipe-sensors'}" Pressed="False" HorizontalExpand="True"/>
</BoxContainer>
</BoxContainer>
</BoxContainer>
</BoxContainer>
<!-- Footer -->
<BoxContainer Orientation="Vertical">
<PanelContainer StyleClasses="LowDivider" />
<BoxContainer Orientation="Horizontal" Margin="10 2 5 0" VerticalAlignment="Bottom">
<Label Text="{Loc 'atmos-monitoring-window-flavor-left'}" StyleClasses="WindowFooterText" />
<Label Text="{Loc 'atmos-monitoring-window-flavor-right'}" StyleClasses="WindowFooterText"
HorizontalAlignment="Right" HorizontalExpand="True" Margin="0 0 5 0" />
<TextureRect StyleClasses="NTLogoDark" Stretch="KeepAspectCentered"
VerticalAlignment="Center" HorizontalAlignment="Right" SetSize="19 19"/>
</BoxContainer>
</BoxContainer>
</BoxContainer>
</controls:FancyWindow>

View File

@@ -0,0 +1,455 @@
using Content.Client.Pinpointer.UI;
using Content.Client.UserInterface.Controls;
using Content.Shared.Atmos.Components;
using Content.Shared.Prototypes;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
namespace Content.Client.Atmos.Consoles;
[GenerateTypedNameReferences]
public sealed partial class AtmosMonitoringConsoleWindow : FancyWindow
{
private readonly IEntityManager _entManager;
private readonly IPrototypeManager _protoManager;
private readonly SpriteSystem _spriteSystem;
private EntityUid? _owner;
private NetEntity? _focusEntity;
private int? _focusNetId;
private bool _autoScrollActive = false;
private readonly Color _unfocusedDeviceColor = Color.DimGray;
private ProtoId<NavMapBlipPrototype> _navMapConsoleProtoId = "NavMapConsole";
private ProtoId<NavMapBlipPrototype> _gasPipeSensorProtoId = "GasPipeSensor";
public AtmosMonitoringConsoleWindow(AtmosMonitoringConsoleBoundUserInterface userInterface, EntityUid? owner)
{
RobustXamlLoader.Load(this);
_entManager = IoCManager.Resolve<IEntityManager>();
_protoManager = IoCManager.Resolve<IPrototypeManager>();
_spriteSystem = _entManager.System<SpriteSystem>();
// Pass the owner to nav map
_owner = owner;
NavMap.Owner = _owner;
// Set nav map grid uid
var stationName = Loc.GetString("atmos-monitoring-window-unknown-location");
EntityCoordinates? consoleCoords = null;
if (_entManager.TryGetComponent<TransformComponent>(owner, out var xform))
{
consoleCoords = xform.Coordinates;
NavMap.MapUid = xform.GridUid;
// Assign station name
if (_entManager.TryGetComponent<MetaDataComponent>(xform.GridUid, out var stationMetaData))
stationName = stationMetaData.EntityName;
var msg = new FormattedMessage();
msg.TryAddMarkup(Loc.GetString("atmos-monitoring-window-station-name", ("stationName", stationName)), out _);
StationName.SetMessage(msg);
}
else
{
StationName.SetMessage(stationName);
NavMap.Visible = false;
}
// Set trackable entity selected action
NavMap.TrackedEntitySelectedAction += SetTrackedEntityFromNavMap;
// Update nav map
NavMap.ForceNavMapUpdate();
// Set tab container headers
MasterTabContainer.SetTabTitle(0, Loc.GetString("atmos-monitoring-window-tab-networks"));
// Set UI toggles
ShowPipeNetwork.OnToggled += _ => OnShowPipeNetworkToggled();
ShowGasPipeSensors.OnToggled += _ => OnShowGasPipeSensors();
// Set nav map colors
if (!_entManager.TryGetComponent<AtmosMonitoringConsoleComponent>(_owner, out var console))
return;
NavMap.TileColor = console.NavMapTileColor;
NavMap.WallColor = console.NavMapWallColor;
// Initalize
UpdateUI(consoleCoords, Array.Empty<AtmosMonitoringConsoleEntry>());
}
#region Toggle handling
private void OnShowPipeNetworkToggled()
{
if (_owner == null)
return;
if (!_entManager.TryGetComponent<AtmosMonitoringConsoleComponent>(_owner.Value, out var console))
return;
NavMap.ShowPipeNetwork = ShowPipeNetwork.Pressed;
foreach (var (netEnt, device) in console.AtmosDevices)
{
if (device.NavMapBlip == _gasPipeSensorProtoId)
continue;
if (ShowPipeNetwork.Pressed)
AddTrackedEntityToNavMap(device);
else
NavMap.TrackedEntities.Remove(netEnt);
}
}
private void OnShowGasPipeSensors()
{
if (_owner == null)
return;
if (!_entManager.TryGetComponent<AtmosMonitoringConsoleComponent>(_owner.Value, out var console))
return;
foreach (var (netEnt, device) in console.AtmosDevices)
{
if (device.NavMapBlip != _gasPipeSensorProtoId)
continue;
if (ShowGasPipeSensors.Pressed)
AddTrackedEntityToNavMap(device, true);
else
NavMap.TrackedEntities.Remove(netEnt);
}
}
#endregion
public void UpdateUI
(EntityCoordinates? consoleCoords,
AtmosMonitoringConsoleEntry[] atmosNetworks)
{
if (_owner == null)
return;
if (!_entManager.TryGetComponent<AtmosMonitoringConsoleComponent>(_owner.Value, out var console))
return;
// Reset nav map values
NavMap.TrackedCoordinates.Clear();
NavMap.TrackedEntities.Clear();
if (_focusEntity != null && !console.AtmosDevices.Any(x => x.Key == _focusEntity))
ClearFocus();
// Add tracked entities to the nav map
UpdateNavMapBlips();
// Show the monitor location
var consoleNetEnt = _entManager.GetNetEntity(_owner);
if (consoleCoords != null && consoleNetEnt != null)
{
var proto = _protoManager.Index(_navMapConsoleProtoId);
if (proto.TexturePaths != null && proto.TexturePaths.Length != 0)
{
var texture = _spriteSystem.Frame0(new SpriteSpecifier.Texture(proto.TexturePaths[0]));
var blip = new NavMapBlip(consoleCoords.Value, texture, proto.Color, proto.Blinks, proto.Selectable);
NavMap.TrackedEntities[consoleNetEnt.Value] = blip;
}
}
// Update the nav map
NavMap.ForceNavMapUpdate();
// Clear excess children from the tables
while (AtmosNetworksTable.ChildCount > atmosNetworks.Length)
AtmosNetworksTable.RemoveChild(AtmosNetworksTable.GetChild(AtmosNetworksTable.ChildCount - 1));
// Update all entries in each table
for (int index = 0; index < atmosNetworks.Length; index++)
{
var entry = atmosNetworks.ElementAt(index);
UpdateUIEntry(entry, index, AtmosNetworksTable, console);
}
}
private void UpdateNavMapBlips()
{
if (_owner == null || !_entManager.TryGetComponent<AtmosMonitoringConsoleComponent>(_owner.Value, out var console))
return;
if (NavMap.Visible)
{
foreach (var (netEnt, device) in console.AtmosDevices)
{
// Update the focus network ID, incase it has changed
if (_focusEntity == netEnt)
{
_focusNetId = device.NetId;
NavMap.FocusNetId = _focusNetId;
}
var isSensor = device.NavMapBlip == _gasPipeSensorProtoId;
// Skip network devices if the toggled is off
if (!ShowPipeNetwork.Pressed && !isSensor)
continue;
// Skip gas pipe sensors if the toggle is off
if (!ShowGasPipeSensors.Pressed && isSensor)
continue;
AddTrackedEntityToNavMap(device, isSensor);
}
}
}
private void AddTrackedEntityToNavMap(AtmosDeviceNavMapData metaData, bool isSensor = false)
{
var proto = _protoManager.Index(metaData.NavMapBlip);
if (proto.TexturePaths == null || proto.TexturePaths.Length == 0)
return;
var idx = Math.Clamp((int)metaData.Direction / 2, 0, proto.TexturePaths.Length - 1);
var texture = proto.TexturePaths.Length > 0 ? proto.TexturePaths[idx] : proto.TexturePaths[0];
var color = isSensor ? proto.Color : proto.Color * metaData.PipeColor;
if (_focusNetId != null && metaData.NetId != _focusNetId)
color *= _unfocusedDeviceColor;
var blinks = proto.Blinks || _focusEntity == metaData.NetEntity;
var coords = _entManager.GetCoordinates(metaData.NetCoordinates);
var blip = new NavMapBlip(coords, _spriteSystem.Frame0(new SpriteSpecifier.Texture(texture)), color, blinks, proto.Selectable, proto.Scale);
NavMap.TrackedEntities[metaData.NetEntity] = blip;
}
private void UpdateUIEntry(AtmosMonitoringConsoleEntry data, int index, Control table, AtmosMonitoringConsoleComponent console)
{
// Make new UI entry if required
if (index >= table.ChildCount)
{
var newEntryContainer = new AtmosMonitoringEntryContainer(data);
// On click
newEntryContainer.FocusButton.OnButtonUp += args =>
{
if (_focusEntity == newEntryContainer.Data.NetEntity)
{
ClearFocus();
}
else
{
SetFocus(newEntryContainer.Data.NetEntity, newEntryContainer.Data.NetId);
var coords = _entManager.GetCoordinates(newEntryContainer.Data.Coordinates);
NavMap.CenterToCoordinates(coords);
}
// Update affected UI elements across all tables
UpdateConsoleTable(console, AtmosNetworksTable, _focusEntity);
};
// Add the entry to the current table
table.AddChild(newEntryContainer);
}
// Update values and UI elements
var tableChild = table.GetChild(index);
if (tableChild is not AtmosMonitoringEntryContainer)
{
table.RemoveChild(tableChild);
UpdateUIEntry(data, index, table, console);
return;
}
var entryContainer = (AtmosMonitoringEntryContainer)tableChild;
entryContainer.UpdateEntry(data, data.NetEntity == _focusEntity);
}
private void UpdateConsoleTable(AtmosMonitoringConsoleComponent console, Control table, NetEntity? currTrackedEntity)
{
foreach (var tableChild in table.Children)
{
if (tableChild is not AtmosAlarmEntryContainer)
continue;
var entryContainer = (AtmosAlarmEntryContainer)tableChild;
if (entryContainer.NetEntity != currTrackedEntity)
entryContainer.RemoveAsFocus();
else if (entryContainer.NetEntity == currTrackedEntity)
entryContainer.SetAsFocus();
}
}
private void SetTrackedEntityFromNavMap(NetEntity? focusEntity)
{
if (focusEntity == null)
return;
if (!_entManager.TryGetComponent<AtmosMonitoringConsoleComponent>(_owner, out var console))
return;
foreach (var (netEnt, device) in console.AtmosDevices)
{
if (netEnt != focusEntity)
continue;
if (device.NavMapBlip != _gasPipeSensorProtoId)
return;
// Set new focus
SetFocus(focusEntity.Value, device.NetId);
// Get the scroll position of the selected entity on the selected button the UI
ActivateAutoScrollToFocus();
break;
}
}
protected override void FrameUpdate(FrameEventArgs args)
{
AutoScrollToFocus();
}
private void ActivateAutoScrollToFocus()
{
_autoScrollActive = true;
}
private void AutoScrollToFocus()
{
if (!_autoScrollActive)
return;
var scroll = AtmosNetworksTable.Parent as ScrollContainer;
if (scroll == null)
return;
if (!TryGetVerticalScrollbar(scroll, out var vScrollbar))
return;
if (!TryGetNextScrollPosition(out float? nextScrollPosition))
return;
vScrollbar.ValueTarget = nextScrollPosition.Value;
if (MathHelper.CloseToPercent(vScrollbar.Value, vScrollbar.ValueTarget))
_autoScrollActive = false;
}
private bool TryGetVerticalScrollbar(ScrollContainer scroll, [NotNullWhen(true)] out VScrollBar? vScrollBar)
{
vScrollBar = null;
foreach (var control in scroll.Children)
{
if (control is not VScrollBar)
continue;
vScrollBar = (VScrollBar)control;
return true;
}
return false;
}
private bool TryGetNextScrollPosition([NotNullWhen(true)] out float? nextScrollPosition)
{
nextScrollPosition = null;
var scroll = AtmosNetworksTable.Parent as ScrollContainer;
if (scroll == null)
return false;
var container = scroll.Children.ElementAt(0) as BoxContainer;
if (container == null || container.Children.Count() == 0)
return false;
// Exit if the heights of the children haven't been initialized yet
if (!container.Children.Any(x => x.Height > 0))
return false;
nextScrollPosition = 0;
foreach (var control in container.Children)
{
if (control is not AtmosMonitoringEntryContainer)
continue;
var entry = (AtmosMonitoringEntryContainer)control;
if (entry.Data.NetEntity == _focusEntity)
return true;
nextScrollPosition += control.Height;
}
// Failed to find control
nextScrollPosition = null;
return false;
}
private void SetFocus(NetEntity focusEntity, int focusNetId)
{
_focusEntity = focusEntity;
_focusNetId = focusNetId;
NavMap.FocusNetId = focusNetId;
OnFocusChanged();
}
private void ClearFocus()
{
_focusEntity = null;
_focusNetId = null;
NavMap.FocusNetId = null;
OnFocusChanged();
}
private void OnFocusChanged()
{
UpdateNavMapBlips();
NavMap.ForceNavMapUpdate();
if (!_entManager.TryGetComponent<AtmosMonitoringConsoleComponent>(_owner, out var console))
return;
for (int index = 0; index < AtmosNetworksTable.ChildCount; index++)
{
var entry = (AtmosMonitoringEntryContainer)AtmosNetworksTable.GetChild(index);
if (entry == null)
continue;
UpdateUIEntry(entry.Data, index, AtmosNetworksTable, console);
}
}
}

View File

@@ -0,0 +1,74 @@
<BoxContainer xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:s="clr-namespace:Content.Client.Stylesheets"
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
Orientation="Vertical" HorizontalExpand ="True" Margin="0 0 0 3">
<!-- Network selection button -->
<Button Name="FocusButton" HorizontalExpand="True" VerticalExpand="True" Margin="0 0 6 8" StyleClasses="OpenLeft" Access="Public">
<BoxContainer HorizontalExpand="True" VerticalExpand="True" Orientation="Vertical">
<BoxContainer HorizontalExpand="True" VerticalExpand="True" Orientation="Horizontal" SetHeight="32">
<PanelContainer Name="NetworkColorStripe" HorizontalAlignment="Left" SetWidth="8" VerticalExpand="True" Margin="-8 -2 0 0">
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#d7d7d7"/>
</PanelContainer.PanelOverride>
</PanelContainer>
<Label Name="NetworkNameLabel" Text="???" HorizontalExpand="True" HorizontalAlignment="Center"/>
</BoxContainer>
<!-- Panel that appears on selecting the device -->
<PanelContainer HorizontalExpand="True" Margin="-8 0 -14 -4" Access="Public">
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#25252a"/>
</PanelContainer.PanelOverride>
<BoxContainer Name="MainDataContainer" HorizontalExpand="True" VerticalExpand="True" Orientation="Vertical">
<Control>
<BoxContainer HorizontalExpand="True" VerticalExpand="True" Orientation="Vertical">
<BoxContainer HorizontalExpand="True" Orientation="Horizontal">
<Label Name="TemperatureHeaderLabel" Text="{Loc 'atmos-alerts-window-temperature-label'}" HorizontalAlignment="Center" HorizontalExpand="True" FontColorOverride="#a9a9a9" Margin="0 2 0 0" SetHeight="24"></Label>
<Label Name="PressureHeaderLabel" Text="{Loc 'atmos-alerts-window-pressure-label'}" HorizontalAlignment="Center" HorizontalExpand="True" FontColorOverride="#a9a9a9" Margin="0 2 0 0" SetHeight="24"></Label>
<Label Name="TotalMolHeaderLabel" Text="{Loc 'atmos-alerts-window-total-mol-label'}" HorizontalAlignment="Center" HorizontalExpand="True" FontColorOverride="#a9a9a9" Margin="0 2 0 0" SetHeight="24"></Label>
</BoxContainer>
<PanelContainer HorizontalExpand="True">
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#202023"/>
</PanelContainer.PanelOverride>
<BoxContainer HorizontalExpand="True" Orientation="Horizontal">
<Label Name="TemperatureLabel" Text="???" HorizontalAlignment="Center" HorizontalExpand="True" FontColorOverride="#a9a9a9" Margin="0 2 0 0" SetHeight="24"></Label>
<Label Name="PressureLabel" Text="???" HorizontalAlignment="Center" HorizontalExpand="True" FontColorOverride="#a9a9a9" Margin="0 2 0 0" SetHeight="24"></Label>
<Label Name="TotalMolLabel" Text="???" HorizontalAlignment="Center" HorizontalExpand="True" FontColorOverride="#a9a9a9" Margin="0 2 0 0" SetHeight="24"></Label>
</BoxContainer>
</PanelContainer>
<BoxContainer HorizontalExpand="True" Orientation="Horizontal" Margin="8 0">
<TextureRect Name="ArrowTexture" VerticalAlignment="Center" SetSize="12 12" Stretch="KeepAspectCentered" Margin="3 0" TexturePath="/Textures/Interface/Nano/triangle_right.png"></TextureRect>
<Label Name="GasesHeaderLabel" Text="{Loc 'atmos-monitoring-window-label-gases'}" HorizontalAlignment="Left" HorizontalExpand="True" FontColorOverride="#a9a9a9" Margin="4 0 0 0" SetHeight="24"></Label>
</BoxContainer>
</BoxContainer>
</Control>
<!-- Atmosphere status -->
<Control Name="FocusContainer" ReservesSpace="False" Visible="False">
<!-- Main container for displaying atmospheric data -->
<BoxContainer HorizontalExpand="True" VerticalExpand="True" Orientation="Vertical">
<PanelContainer HorizontalExpand="True">
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#202023"/>
</PanelContainer.PanelOverride>
<!-- Gas entries added via C# code -->
<GridContainer Name="GasGridContainer" HorizontalExpand="True" Columns = "4"></GridContainer>
</PanelContainer>
</BoxContainer>
</Control>
</BoxContainer>
<!-- If the alarm is inactive, this is label is displayed instead -->
<Label Name="NoDataLabel" Text="{Loc 'atmos-alerts-window-no-data-available'}" HorizontalAlignment="Center" Margin="0 15" FontColorOverride="#a9a9a9" ReservesSpace="False" Visible="False"></Label>
</PanelContainer>
</BoxContainer>
</Button>
</BoxContainer>

View File

@@ -0,0 +1,166 @@
using Content.Client.Stylesheets;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Components;
using Content.Shared.FixedPoint;
using Content.Shared.Temperature;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using System.Linq;
namespace Content.Client.Atmos.Consoles;
[GenerateTypedNameReferences]
public sealed partial class AtmosMonitoringEntryContainer : BoxContainer
{
public AtmosMonitoringConsoleEntry Data;
private readonly IEntityManager _entManager;
private readonly IResourceCache _cache;
public AtmosMonitoringEntryContainer(AtmosMonitoringConsoleEntry data)
{
RobustXamlLoader.Load(this);
_entManager = IoCManager.Resolve<IEntityManager>();
_cache = IoCManager.Resolve<IResourceCache>();
Data = data;
// Modulate colored stripe
NetworkColorStripe.Modulate = data.Color;
// Load fonts
var headerFont = new VectorFont(_cache.GetResource<FontResource>("/Fonts/NotoSans/NotoSans-Bold.ttf"), 11);
var normalFont = new VectorFont(_cache.GetResource<FontResource>("/Fonts/NotoSansDisplay/NotoSansDisplay-Regular.ttf"), 11);
// Set fonts
TemperatureHeaderLabel.FontOverride = headerFont;
PressureHeaderLabel.FontOverride = headerFont;
TotalMolHeaderLabel.FontOverride = headerFont;
GasesHeaderLabel.FontOverride = headerFont;
TemperatureLabel.FontOverride = normalFont;
PressureLabel.FontOverride = normalFont;
TotalMolLabel.FontOverride = normalFont;
NoDataLabel.FontOverride = headerFont;
}
public void UpdateEntry(AtmosMonitoringConsoleEntry updatedData, bool isFocus)
{
// Load fonts
var normalFont = new VectorFont(_cache.GetResource<FontResource>("/Fonts/NotoSansDisplay/NotoSansDisplay-Regular.ttf"), 11);
// Update name and values
if (!string.IsNullOrEmpty(updatedData.Address))
NetworkNameLabel.Text = Loc.GetString("atmos-alerts-window-alarm-label", ("name", updatedData.EntityName), ("address", updatedData.Address));
else
NetworkNameLabel.Text = Loc.GetString(updatedData.EntityName);
Data = updatedData;
// Modulate colored stripe
NetworkColorStripe.Modulate = Data.Color;
// Focus updates
if (isFocus)
SetAsFocus();
else
RemoveAsFocus();
// Check if powered
if (!updatedData.IsPowered)
{
MainDataContainer.Visible = false;
NoDataLabel.Visible = true;
return;
}
// Set container visibility
MainDataContainer.Visible = true;
NoDataLabel.Visible = false;
// Update temperature
var isNotVacuum = updatedData.TotalMolData > 1e-6f;
var tempK = (FixedPoint2)updatedData.TemperatureData;
var tempC = (FixedPoint2)TemperatureHelpers.KelvinToCelsius(tempK.Float());
TemperatureLabel.Text = isNotVacuum ?
Loc.GetString("atmos-alerts-window-temperature-value", ("valueInC", tempC), ("valueInK", tempK)) :
Loc.GetString("atmos-alerts-window-invalid-value");
TemperatureLabel.FontColorOverride = isNotVacuum ? Color.DarkGray : StyleNano.DisabledFore;
// Update pressure
PressureLabel.Text = Loc.GetString("atmos-alerts-window-pressure-value", ("value", (FixedPoint2)updatedData.PressureData));
PressureLabel.FontColorOverride = isNotVacuum ? Color.DarkGray : StyleNano.DisabledFore;
// Update total mol
TotalMolLabel.Text = Loc.GetString("atmos-alerts-window-total-mol-value", ("value", (FixedPoint2)updatedData.TotalMolData));
TotalMolLabel.FontColorOverride = isNotVacuum ? Color.DarkGray : StyleNano.DisabledFore;
// Update other present gases
GasGridContainer.RemoveAllChildren();
if (updatedData.GasData.Count() == 0)
{
// No gases
var gasLabel = new Label()
{
Text = Loc.GetString("atmos-alerts-window-other-gases-value-nil"),
FontOverride = normalFont,
FontColorOverride = StyleNano.DisabledFore,
HorizontalAlignment = HAlignment.Center,
VerticalAlignment = VAlignment.Center,
HorizontalExpand = true,
Margin = new Thickness(0, 2, 0, 0),
SetHeight = 24f,
};
GasGridContainer.AddChild(gasLabel);
}
else
{
// Add an entry for each gas
foreach (var (gas, percent) in updatedData.GasData)
{
var gasPercent = (FixedPoint2)0f;
gasPercent = percent * 100f;
var gasAbbreviation = Atmospherics.GasAbbreviations.GetValueOrDefault(gas, Loc.GetString("gas-unknown-abbreviation"));
var gasLabel = new Label()
{
Text = Loc.GetString("atmos-alerts-window-other-gases-value", ("shorthand", gasAbbreviation), ("value", gasPercent)),
FontOverride = normalFont,
HorizontalAlignment = HAlignment.Center,
VerticalAlignment = VAlignment.Center,
HorizontalExpand = true,
Margin = new Thickness(0, 2, 0, 0),
SetHeight = 24f,
};
GasGridContainer.AddChild(gasLabel);
}
}
}
public void SetAsFocus()
{
FocusButton.AddStyleClass(StyleNano.StyleClassButtonColorGreen);
ArrowTexture.TexturePath = "/Textures/Interface/Nano/inverted_triangle.svg.png";
FocusContainer.Visible = true;
}
public void RemoveAsFocus()
{
FocusButton.RemoveStyleClass(StyleNano.StyleClassButtonColorGreen);
ArrowTexture.TexturePath = "/Textures/Interface/Nano/triangle_right.png";
FocusContainer.Visible = false;
}
}

View File

@@ -0,0 +1,23 @@
using Content.Client.Atmos.UI;
using Content.Shared.Atmos.Components;
using Content.Shared.Atmos.EntitySystems;
using Content.Shared.Atmos.Piping.Binary.Components;
namespace Content.Client.Atmos.EntitySystems;
public sealed class GasPressurePumpSystem : SharedGasPressurePumpSystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<GasPressurePumpComponent, AfterAutoHandleStateEvent>(OnPumpUpdate);
}
private void OnPumpUpdate(Entity<GasPressurePumpComponent> ent, ref AfterAutoHandleStateEvent args)
{
if (UserInterfaceSystem.TryGetOpenUi<GasPressurePumpBoundUserInterface>(ent.Owner, GasPressurePumpUiKey.Key, out var bui))
{
bui.Update();
}
}
}

View File

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

View File

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

View File

@@ -1,14 +1,8 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using Content.Client.Atmos.EntitySystems;
using Content.Client.UserInterface.Controls;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Prototypes;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Localization;
namespace Content.Client.Atmos.UI
{
@@ -16,12 +10,25 @@ namespace Content.Client.Atmos.UI
/// Client-side UI used to control a gas pressure pump.
/// </summary>
[GenerateTypedNameReferences]
public sealed partial class GasPressurePumpWindow : DefaultWindow
public sealed partial class GasPressurePumpWindow : FancyWindow
{
public bool PumpStatus = true;
public event Action? ToggleStatusButtonPressed;
public event Action<string>? PumpOutputPressureChanged;
public event Action<float>? PumpOutputPressureChanged;
public float MaxPressure
{
get => _maxPressure;
set
{
_maxPressure = value;
PumpPressureOutputInput.Value = MathF.Min(value, PumpPressureOutputInput.Value);
}
}
private float _maxPressure = Atmospherics.MaxOutputPressure;
public GasPressurePumpWindow()
{
@@ -30,23 +37,25 @@ namespace Content.Client.Atmos.UI
ToggleStatusButton.OnPressed += _ => SetPumpStatus(!PumpStatus);
ToggleStatusButton.OnPressed += _ => ToggleStatusButtonPressed?.Invoke();
PumpPressureOutputInput.OnTextChanged += _ => SetOutputPressureButton.Disabled = false;
PumpPressureOutputInput.OnValueChanged += _ => SetOutputPressureButton.Disabled = false;
SetOutputPressureButton.OnPressed += _ =>
{
PumpOutputPressureChanged?.Invoke(PumpPressureOutputInput.Text ??= "");
PumpPressureOutputInput.Value = Math.Clamp(PumpPressureOutputInput.Value, 0f, _maxPressure);
PumpOutputPressureChanged?.Invoke(PumpPressureOutputInput.Value);
SetOutputPressureButton.Disabled = true;
};
SetMaxPressureButton.OnPressed += _ =>
{
PumpPressureOutputInput.Text = Atmospherics.MaxOutputPressure.ToString(CultureInfo.CurrentCulture);
PumpPressureOutputInput.Value = _maxPressure;
SetOutputPressureButton.Disabled = false;
};
}
public void SetOutputPressure(float pressure)
{
PumpPressureOutputInput.Text = pressure.ToString(CultureInfo.CurrentCulture);
PumpPressureOutputInput.Value = pressure;
}
public void SetPumpStatus(bool enabled)

View File

@@ -2,6 +2,7 @@ using System.Numerics;
using Content.Client.Chat.Managers;
using Content.Shared.CCVar;
using Content.Shared.Chat;
using Content.Shared.Speech;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
@@ -141,7 +142,12 @@ namespace Content.Client.Chat.UI
Modulate = Color.White;
}
var offset = (-_eyeManager.CurrentEye.Rotation).ToWorldVec() * -EntityVerticalOffset;
var baseOffset = 0f;
if (_entityManager.TryGetComponent<SpeechComponent>(_senderEntity, out var speech))
baseOffset = speech.SpeechBubbleOffset;
var offset = (-_eyeManager.CurrentEye.Rotation).ToWorldVec() * -(EntityVerticalOffset + baseOffset);
var worldPos = _transformSystem.GetWorldPosition(xform) + offset;
var lowerCenter = _eyeManager.WorldToScreen(worldPos) / UIScale;

View File

@@ -12,6 +12,7 @@ using Robust.Shared.Utility;
using System.Linq;
using System.Numerics;
using Content.Shared.FixedPoint;
using Robust.Client.Graphics;
using static Robust.Client.UserInterface.Controls.BoxContainer;
namespace Content.Client.Chemistry.UI
@@ -90,10 +91,40 @@ namespace Content.Client.Chemistry.UI
private ReagentButton MakeReagentButton(string text, ChemMasterReagentAmount amount, ReagentId id, bool isBuffer, string styleClass)
{
var button = new ReagentButton(text, amount, id, isBuffer, styleClass);
button.OnPressed += args
=> OnReagentButtonPressed?.Invoke(args, button);
return button;
var reagentTransferButton = new ReagentButton(text, amount, id, isBuffer, styleClass);
reagentTransferButton.OnPressed += args
=> OnReagentButtonPressed?.Invoke(args, reagentTransferButton);
return reagentTransferButton;
}
/// <summary>
/// Conditionally generates a set of reagent buttons based on the supplied boolean argument.
/// This was moved outside of BuildReagentRow to facilitate conditional logic, stops indentation depth getting out of hand as well.
/// </summary>
private List<ReagentButton> CreateReagentTransferButtons(ReagentId reagent, bool isBuffer, bool addReagentButtons)
{
if (!addReagentButtons)
return new List<ReagentButton>(); // Return an empty list if reagentTransferButton creation is disabled.
var buttonConfigs = new (string text, ChemMasterReagentAmount amount, string styleClass)[]
{
("1", ChemMasterReagentAmount.U1, StyleBase.ButtonOpenBoth),
("5", ChemMasterReagentAmount.U5, StyleBase.ButtonOpenBoth),
("10", ChemMasterReagentAmount.U10, StyleBase.ButtonOpenBoth),
("25", ChemMasterReagentAmount.U25, StyleBase.ButtonOpenBoth),
("50", ChemMasterReagentAmount.U50, StyleBase.ButtonOpenBoth),
("100", ChemMasterReagentAmount.U100, StyleBase.ButtonOpenBoth),
(Loc.GetString("chem-master-window-buffer-all-amount"), ChemMasterReagentAmount.All, StyleBase.ButtonOpenLeft),
};
var buttons = new List<ReagentButton>();
foreach (var (text, amount, styleClass) in buttonConfigs)
{
var reagentTransferButton = MakeReagentButton(text, amount, reagent, isBuffer, styleClass);
buttons.Add(reagentTransferButton);
}
return buttons;
}
/// <summary>
@@ -102,25 +133,36 @@ namespace Content.Client.Chemistry.UI
/// <param name="state">State data sent by the server.</param>
public void UpdateState(BoundUserInterfaceState state)
{
var castState = (ChemMasterBoundUserInterfaceState) state;
var castState = (ChemMasterBoundUserInterfaceState)state;
if (castState.UpdateLabel)
LabelLine = GenerateLabel(castState);
// Ensure the Panel Info is updated, including UI elements for Buffer Volume, Output Container and so on
UpdatePanelInfo(castState);
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;
OutputEjectButton.Disabled = castState.OutputContainerInfo is null;
CreateBottleButton.Disabled = castState.OutputContainerInfo?.Reagents == null;
CreatePillButton.Disabled = castState.OutputContainerInfo?.Entities == null;
UpdateDosageFields(castState);
}
//assign default values for pill and bottle fields.
private void UpdateDosageFields(ChemMasterBoundUserInterfaceState castState)
{
var output = castState.OutputContainerInfo;
var remainingCapacity = output is null ? 0 : (output.MaxVolume - output.CurrentVolume).Int();
var holdsReagents = output?.Reagents != null;
var pillNumberMax = holdsReagents ? 0 : remainingCapacity;
var bottleAmountMax = holdsReagents ? remainingCapacity : 0;
var bufferVolume = castState.BufferCurrentVolume?.Int() ?? 0;
PillDosage.Value = (int)Math.Min(bufferVolume, castState.PillDosageLimit);
PillTypeButtons[castState.SelectedPillType].Pressed = true;
PillNumber.IsValid = x => x >= 0 && x <= pillNumberMax;
PillDosage.IsValid = x => x > 0 && x <= castState.PillDosageLimit;
@@ -130,8 +172,19 @@ namespace Content.Client.Chemistry.UI
PillNumber.Value = pillNumberMax;
if (BottleDosage.Value > bottleAmountMax)
BottleDosage.Value = bottleAmountMax;
}
// Avoid division by zero
if (PillDosage.Value > 0)
{
PillNumber.Value = Math.Min(bufferVolume / PillDosage.Value, pillNumberMax);
}
else
{
PillNumber.Value = 0;
}
BottleDosage.Value = Math.Min(bottleAmountMax, bufferVolume);
}
/// <summary>
/// Generate a product label based on reagents in the buffer.
/// </summary>
@@ -178,46 +231,23 @@ namespace Content.Client.Chemistry.UI
var bufferVol = new Label
{
Text = $"{state.BufferCurrentVolume}u",
StyleClasses = {StyleNano.StyleClassLabelSecondaryColor}
StyleClasses = { StyleNano.StyleClassLabelSecondaryColor }
};
bufferHBox.AddChild(bufferVol);
// initialises rowCount to allow for striped rows
var rowCount = 0;
foreach (var (reagent, quantity) in state.BufferReagents)
{
// Try to get the prototype for the given reagent. This gives us its name.
_prototypeManager.TryIndex(reagent.Prototype, out ReagentPrototype? proto);
var reagentId = reagent;
_prototypeManager.TryIndex(reagentId.Prototype, out ReagentPrototype? proto);
var name = proto?.LocalizedName ?? Loc.GetString("chem-master-window-unknown-reagent-text");
if (proto != null)
{
BufferInfo.Children.Add(new BoxContainer
{
Orientation = LayoutOrientation.Horizontal,
Children =
{
new Label {Text = $"{name}: "},
new Label
{
Text = $"{quantity}u",
StyleClasses = {StyleNano.StyleClassLabelSecondaryColor}
},
// Padding
new Control {HorizontalExpand = true},
MakeReagentButton("1", ChemMasterReagentAmount.U1, reagent, true, StyleBase.ButtonOpenRight),
MakeReagentButton("5", ChemMasterReagentAmount.U5, reagent, true, StyleBase.ButtonOpenBoth),
MakeReagentButton("10", ChemMasterReagentAmount.U10, reagent, true, StyleBase.ButtonOpenBoth),
MakeReagentButton("25", ChemMasterReagentAmount.U25, reagent, true, StyleBase.ButtonOpenBoth),
MakeReagentButton("50", ChemMasterReagentAmount.U50, reagent, true, StyleBase.ButtonOpenBoth),
MakeReagentButton("100", ChemMasterReagentAmount.U100, reagent, true, StyleBase.ButtonOpenBoth),
MakeReagentButton(Loc.GetString("chem-master-window-buffer-all-amount"), ChemMasterReagentAmount.All, reagent, true, StyleBase.ButtonOpenLeft),
}
});
}
var reagentColor = proto?.SubstanceColor ?? default(Color);
BufferInfo.Children.Add(BuildReagentRow(reagentColor, rowCount++, name, reagentId, quantity, true, true));
}
}
private void BuildContainerUI(Control control, ContainerInfo? info, bool addReagentButtons)
{
control.Children.Clear();
@@ -228,104 +258,111 @@ namespace Content.Client.Chemistry.UI
{
Text = Loc.GetString("chem-master-window-no-container-loaded-text")
});
return;
}
else
// Name of the container and its fill status (Ex: 44/100u)
control.Children.Add(new BoxContainer
{
// Name of the container and its fill status (Ex: 44/100u)
control.Children.Add(new BoxContainer
Orientation = LayoutOrientation.Horizontal,
Children =
{
Orientation = LayoutOrientation.Horizontal,
Children =
new Label { Text = $"{info.DisplayName}: " },
new Label
{
new Label {Text = $"{info.DisplayName}: "},
new Label
{
Text = $"{info.CurrentVolume}/{info.MaxVolume}",
StyleClasses = {StyleNano.StyleClassLabelSecondaryColor}
}
Text = $"{info.CurrentVolume}/{info.MaxVolume}",
StyleClasses = { StyleNano.StyleClassLabelSecondaryColor }
}
});
IEnumerable<(string Name, ReagentId Id, FixedPoint2 Quantity)> contents;
if (info.Entities != null)
{
contents = info.Entities.Select(x => (x.Id, default(ReagentId), x.Quantity));
}
else if (info.Reagents != null)
});
// Initialises rowCount to allow for striped rows
var rowCount = 0;
// Handle entities if they are not null
if (info.Entities != null)
{
foreach (var (id, quantity) in info.Entities.Select(x => (x.Id, x.Quantity)))
{
contents = info.Reagents.Select(x =>
{
_prototypeManager.TryIndex(x.Reagent.Prototype, out ReagentPrototype? proto);
var name = proto?.LocalizedName
?? Loc.GetString("chem-master-window-unknown-reagent-text");
return (name, Id: x.Reagent, x.Quantity);
})
.OrderBy(r => r.Item1);
control.Children.Add(BuildReagentRow(default(Color), rowCount++, id, default(ReagentId), quantity, false, addReagentButtons));
}
else
}
// Handle reagents if they are not null
if (info.Reagents != null)
{
foreach (var reagent in info.Reagents)
{
return;
_prototypeManager.TryIndex(reagent.Reagent.Prototype, out ReagentPrototype? proto);
var name = proto?.LocalizedName ?? Loc.GetString("chem-master-window-unknown-reagent-text");
var reagentColor = proto?.SubstanceColor ?? default(Color);
control.Children.Add(BuildReagentRow(reagentColor, rowCount++, name, reagent.Reagent, reagent.Quantity, false, addReagentButtons));
}
foreach (var (name, id, quantity) in contents)
{
var inner = new BoxContainer
{
Orientation = LayoutOrientation.Horizontal,
Children =
{
new Label { Text = $"{name}: " },
new Label
{
Text = $"{quantity}u",
StyleClasses = { StyleNano.StyleClassLabelSecondaryColor },
}
}
};
if (addReagentButtons)
{
var cs = inner.Children;
// Padding
cs.Add(new Control { HorizontalExpand = true });
cs.Add(MakeReagentButton(
"1", ChemMasterReagentAmount.U1, id, false, StyleBase.ButtonOpenRight));
cs.Add(MakeReagentButton(
"5", ChemMasterReagentAmount.U5, id, false, StyleBase.ButtonOpenBoth));
cs.Add(MakeReagentButton(
"10", ChemMasterReagentAmount.U10, id, false, StyleBase.ButtonOpenBoth));
cs.Add(MakeReagentButton(
"25", ChemMasterReagentAmount.U25, id, false, StyleBase.ButtonOpenBoth));
cs.Add(MakeReagentButton(
"50", ChemMasterReagentAmount.U50, id, false, StyleBase.ButtonOpenBoth));
cs.Add(MakeReagentButton(
"100", ChemMasterReagentAmount.U100, id, false, StyleBase.ButtonOpenBoth));
cs.Add(MakeReagentButton(
Loc.GetString("chem-master-window-buffer-all-amount"),
ChemMasterReagentAmount.All, id, false, StyleBase.ButtonOpenLeft));
}
control.Children.Add(inner);
}
}
}
public String LabelLine
/// <summary>
/// Take reagent/entity data and present rows, labels, and buttons appropriately. todo sprites?
/// </summary>
private Control BuildReagentRow(Color reagentColor, int rowCount, string name, ReagentId reagent, FixedPoint2 quantity, bool isBuffer, bool addReagentButtons)
{
get
//Colors rows and sets fallback for reagentcolor to the same as background, this will hide colorPanel for entities hopefully
var rowColor1 = Color.FromHex("#1B1B1E");
var rowColor2 = Color.FromHex("#202025");
var currentRowColor = (rowCount % 2 == 1) ? rowColor1 : rowColor2;
if ((reagentColor == default(Color))|(!addReagentButtons))
{
return LabelLineEdit.Text;
reagentColor = currentRowColor;
}
set
//this calls the separated button builder, and stores the return to render after labels
var reagentButtonConstructors = CreateReagentTransferButtons(reagent, isBuffer, addReagentButtons);
// Create the row layout with the color panel
var rowContainer = new BoxContainer
{
LabelLineEdit.Text = value;
Orientation = LayoutOrientation.Horizontal,
Children =
{
new Label { Text = $"{name}: " },
new Label
{
Text = $"{quantity}u",
StyleClasses = { StyleNano.StyleClassLabelSecondaryColor }
},
// Padding
new Control { HorizontalExpand = true },
// Colored panels for reagents
new PanelContainer
{
Name = "colorPanel",
VerticalExpand = true,
MinWidth = 4,
PanelOverride = new StyleBoxFlat
{
BackgroundColor = reagentColor
},
Margin = new Thickness(0, 1)
}
}
};
// Add the reagent buttons after the color panel
foreach (var reagentTransferButton in reagentButtonConstructors)
{
rowContainer.AddChild(reagentTransferButton);
}
//Apply panencontainer to allow for striped rows
return new PanelContainer
{
PanelOverride = new StyleBoxFlat(currentRowColor),
Children = { rowContainer }
};
}
public string LabelLine
{
get => LabelLineEdit.Text;
set => LabelLineEdit.Text = value;
}
}

View File

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

View File

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

View File

@@ -0,0 +1,101 @@
using Content.Shared.Holopad;
using Content.Shared.Silicons.StationAi;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Shared.Player;
using System.Numerics;
namespace Content.Client.Holopad;
public sealed class HolopadBoundUserInterface : BoundUserInterface
{
[Dependency] private readonly ISharedPlayerManager _playerManager = default!;
[Dependency] private readonly IClyde _displayManager = default!;
[ViewVariables]
private HolopadWindow? _window;
public HolopadBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
IoCManager.InjectDependencies(this);
}
protected override void Open()
{
base.Open();
_window = this.CreateWindow<HolopadWindow>();
_window.Title = Loc.GetString("holopad-window-title", ("title", EntMan.GetComponent<MetaDataComponent>(Owner).EntityName));
if (this.UiKey is not HolopadUiKey)
{
Close();
return;
}
var uiKey = (HolopadUiKey)this.UiKey;
// AIs will see a different holopad interface to crew when interacting with them in the world
if (uiKey == HolopadUiKey.InteractionWindow && EntMan.HasComponent<StationAiHeldComponent>(_playerManager.LocalEntity))
uiKey = HolopadUiKey.InteractionWindowForAi;
_window.SetState(Owner, uiKey);
_window.UpdateState(new Dictionary<NetEntity, string>());
// Set message actions
_window.SendHolopadStartNewCallMessageAction += SendHolopadStartNewCallMessage;
_window.SendHolopadAnswerCallMessageAction += SendHolopadAnswerCallMessage;
_window.SendHolopadEndCallMessageAction += SendHolopadEndCallMessage;
_window.SendHolopadStartBroadcastMessageAction += SendHolopadStartBroadcastMessage;
_window.SendHolopadActivateProjectorMessageAction += SendHolopadActivateProjectorMessage;
_window.SendHolopadRequestStationAiMessageAction += SendHolopadRequestStationAiMessage;
// If this call is addressed to an AI, open the window in the bottom right hand corner of the screen
if (uiKey == HolopadUiKey.AiRequestWindow)
_window.OpenCenteredAt(new Vector2(1f, 1f));
// Otherwise offset to the left so the holopad can still be seen
else
_window.OpenCenteredAt(new Vector2(0.3333f, 0.50f));
}
protected override void UpdateState(BoundUserInterfaceState state)
{
base.UpdateState(state);
var castState = (HolopadBoundInterfaceState)state;
EntMan.TryGetComponent<TransformComponent>(Owner, out var xform);
_window?.UpdateState(castState.Holopads);
}
public void SendHolopadStartNewCallMessage(NetEntity receiver)
{
SendMessage(new HolopadStartNewCallMessage(receiver));
}
public void SendHolopadAnswerCallMessage()
{
SendMessage(new HolopadAnswerCallMessage());
}
public void SendHolopadEndCallMessage()
{
SendMessage(new HolopadEndCallMessage());
}
public void SendHolopadStartBroadcastMessage()
{
SendMessage(new HolopadStartBroadcastMessage());
}
public void SendHolopadActivateProjectorMessage()
{
SendMessage(new HolopadActivateProjectorMessage());
}
public void SendHolopadRequestStationAiMessage()
{
SendMessage(new HolopadStationAiRequestMessage());
}
}

View File

@@ -0,0 +1,172 @@
using Content.Shared.Chat.TypingIndicator;
using Content.Shared.Holopad;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using System.Linq;
namespace Content.Client.Holopad;
public sealed class HolopadSystem : SharedHolopadSystem
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IGameTiming _timing = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<HolopadHologramComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<HolopadHologramComponent, BeforePostShaderRenderEvent>(OnShaderRender);
SubscribeAllEvent<TypingChangedEvent>(OnTypingChanged);
SubscribeNetworkEvent<PlayerSpriteStateRequest>(OnPlayerSpriteStateRequest);
SubscribeNetworkEvent<PlayerSpriteStateMessage>(OnPlayerSpriteStateMessage);
}
private void OnComponentInit(EntityUid uid, HolopadHologramComponent component, ComponentInit ev)
{
if (!TryComp<SpriteComponent>(uid, out var sprite))
return;
UpdateHologramSprite(uid);
}
private void OnShaderRender(EntityUid uid, HolopadHologramComponent component, BeforePostShaderRenderEvent ev)
{
if (ev.Sprite.PostShader == null)
return;
ev.Sprite.PostShader.SetParameter("t", (float)_timing.CurTime.TotalSeconds * component.ScrollRate);
}
private void OnTypingChanged(TypingChangedEvent ev, EntitySessionEventArgs args)
{
var uid = args.SenderSession.AttachedEntity;
if (!Exists(uid))
return;
if (!HasComp<HolopadUserComponent>(uid))
return;
var netEv = new HolopadUserTypingChangedEvent(GetNetEntity(uid.Value), ev.IsTyping);
RaiseNetworkEvent(netEv);
}
private void OnPlayerSpriteStateRequest(PlayerSpriteStateRequest ev)
{
var targetPlayer = GetEntity(ev.TargetPlayer);
var player = _playerManager.LocalSession?.AttachedEntity;
// Ignore the request if received by a player who isn't the target
if (targetPlayer != player)
return;
if (!TryComp<SpriteComponent>(player, out var playerSprite))
return;
var spriteLayerData = new List<PrototypeLayerData>();
if (playerSprite.Visible)
{
// Record the RSI paths, state names and shader paramaters of all visible layers
for (int i = 0; i < playerSprite.AllLayers.Count(); i++)
{
if (!playerSprite.TryGetLayer(i, out var layer))
continue;
if (!layer.Visible ||
string.IsNullOrEmpty(layer.ActualRsi?.Path.ToString()) ||
string.IsNullOrEmpty(layer.State.Name))
continue;
var layerDatum = new PrototypeLayerData();
layerDatum.RsiPath = layer.ActualRsi.Path.ToString();
layerDatum.State = layer.State.Name;
if (layer.CopyToShaderParameters != null)
{
var key = (string)layer.CopyToShaderParameters.LayerKey;
if (playerSprite.LayerMapTryGet(key, out var otherLayerIdx) &&
playerSprite.TryGetLayer(otherLayerIdx, out var otherLayer) &&
otherLayer.Visible)
{
layerDatum.MapKeys = new() { key };
layerDatum.CopyToShaderParameters = new PrototypeCopyToShaderParameters()
{
LayerKey = key,
ParameterTexture = layer.CopyToShaderParameters.ParameterTexture,
ParameterUV = layer.CopyToShaderParameters.ParameterUV
};
}
}
spriteLayerData.Add(layerDatum);
}
}
// Return the recorded data to the server
var evResponse = new PlayerSpriteStateMessage(ev.TargetPlayer, spriteLayerData.ToArray());
RaiseNetworkEvent(evResponse);
}
private void OnPlayerSpriteStateMessage(PlayerSpriteStateMessage ev)
{
UpdateHologramSprite(GetEntity(ev.SpriteEntity), ev.SpriteLayerData);
}
private void UpdateHologramSprite(EntityUid uid, PrototypeLayerData[]? layerData = null)
{
if (!TryComp<SpriteComponent>(uid, out var hologramSprite))
return;
if (!TryComp<HolopadHologramComponent>(uid, out var holopadhologram))
return;
for (int i = hologramSprite.AllLayers.Count() - 1; i >= 0; i--)
hologramSprite.RemoveLayer(i);
if (layerData == null || layerData.Length == 0)
{
layerData = new PrototypeLayerData[1];
layerData[0] = new PrototypeLayerData()
{
RsiPath = holopadhologram.RsiPath,
State = holopadhologram.RsiState
};
}
for (int i = 0; i < layerData.Length; i++)
{
var layer = layerData[i];
layer.Shader = "unshaded";
hologramSprite.AddLayer(layerData[i], i);
}
UpdateHologramShader(uid, hologramSprite, holopadhologram);
}
private void UpdateHologramShader(EntityUid uid, SpriteComponent sprite, HolopadHologramComponent holopadHologram)
{
// Find the texture height of the largest layer
float texHeight = sprite.AllLayers.Max(x => x.PixelSize.Y);
var instance = _prototypeManager.Index<ShaderPrototype>(holopadHologram.ShaderName).InstanceUnique();
instance.SetParameter("color1", new Vector3(holopadHologram.Color1.R, holopadHologram.Color1.G, holopadHologram.Color1.B));
instance.SetParameter("color2", new Vector3(holopadHologram.Color2.R, holopadHologram.Color2.G, holopadHologram.Color2.B));
instance.SetParameter("alpha", holopadHologram.Alpha);
instance.SetParameter("intensity", holopadHologram.Intensity);
instance.SetParameter("texHeight", texHeight);
instance.SetParameter("t", (float)_timing.CurTime.TotalSeconds * holopadHologram.ScrollRate);
sprite.PostShader = instance;
sprite.RaiseShaderEvent = true;
}
}

View File

@@ -0,0 +1,107 @@
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
Resizable="False"
MaxSize="400 800"
MinSize="400 150">
<BoxContainer Orientation="Vertical" VerticalExpand="True" HorizontalExpand="True">
<BoxContainer Name="ControlsLockOutContainer" Orientation="Vertical" VerticalExpand="True" HorizontalExpand="True" ReservesSpace="False" Visible="False">
<!-- Header text -->
<controls:StripeBack>
<PanelContainer>
<RichTextLabel Name="EmergencyBroadcastText" VerticalAlignment="Center" HorizontalAlignment="Center" Margin="10 10 10 10" ReservesSpace="False"/>
</PanelContainer>
</controls:StripeBack>
<Label Text="{Loc 'holopad-window-controls-locked-out'}" HorizontalAlignment="Center" Margin="10 5 10 0" ReservesSpace="False"/>
<RichTextLabel Name="LockOutIdText" HorizontalAlignment="Center" Margin="10 5 10 0" ReservesSpace="False"/>
<Label Name="LockOutCountDownText" Text="{Loc 'holopad-window-controls-unlock-countdown'}" HorizontalAlignment="Center" Margin="10 15 10 10" ReservesSpace="False"/>
</BoxContainer>
<BoxContainer Name="ControlsContainer" Orientation="Vertical" VerticalExpand="True" HorizontalExpand="True" ReservesSpace="False">
<!-- Active call controls (either this or the call placement controls will be active) -->
<BoxContainer Name="ActiveCallControlsContainer" Orientation="Vertical" VerticalExpand="True" HorizontalExpand="True" ReservesSpace="False">
<!-- Header text -->
<BoxContainer MinHeight="60" Orientation="Vertical" VerticalAlignment="Center">
<Label Name="CallStatusText" Margin="10 5 10 0" ReservesSpace="False"/>
<RichTextLabel Name="CallerIdText" HorizontalAlignment="Center" Margin="0 0 0 0" ReservesSpace="False"/>
</BoxContainer>
<!-- Controls (the answer call button is absent when the phone is not ringing) -->
<GridContainer Columns="2" ReservesSpace="False">
<Control HorizontalExpand="True" Margin="10 0 2 5">
<Button Name="AnswerCallButton" Text="{Loc 'holopad-window-answer-call'}" StyleClasses="OpenRight" Margin="0 0 0 5" Disabled="True"/>
</Control>
<Control HorizontalExpand="True" Margin="2 0 10 5">
<Button Name="EndCallButton" Text="{Loc 'holopad-window-end-call'}" StyleClasses="OpenLeft" Margin="0 0 0 5" Disabled="True"/>
</Control>
</GridContainer>
</BoxContainer>
<!-- Call placement controls (either this or the active call controls will be active) -->
<BoxContainer Name="CallPlacementControlsContainer" Orientation="Vertical" VerticalExpand="True" HorizontalExpand="True" ReservesSpace="False">
<controls:StripeBack>
<PanelContainer>
<BoxContainer Orientation="Vertical">
<RichTextLabel Name="SubtitleText" HorizontalAlignment="Center" Margin="0 5 0 0"/>
<RichTextLabel Name="OptionsText" HorizontalAlignment="Center" Margin="0 0 0 5"/>
</BoxContainer>
</PanelContainer>
</controls:StripeBack>
<!-- Request the station AI or activate the holopad projector (only one of these should be active at a time) -->
<BoxContainer Name="RequestStationAiContainer" Orientation="Vertical" ReservesSpace="False" Visible="False">
<Button Name="RequestStationAiButton" Text="{Loc 'holopad-window-request-station-ai'}" Margin="10 5 10 5" Disabled="False"/>
</BoxContainer>
<BoxContainer Name="ActivateProjectorContainer" Orientation="Vertical" ReservesSpace="False" Visible="False">
<Button Name="ActivateProjectorButton" Text="{Loc 'holopad-window-activate-projector'}" Margin="10 5 10 5" Disabled="False"/>
</BoxContainer>
<!-- List of contactable holopads (the list is created in C#) -->
<BoxContainer Name="HolopadContactListContainer" Orientation="Vertical" Margin="10 0 10 5" ReservesSpace="False" Visible="False">
<PanelContainer Name="HolopadContactListHeaderPanel">
<Label Text="{Loc 'holopad-window-select-contact-from-list'}" HorizontalAlignment="Center" Margin="0 3 0 3"/>
</PanelContainer>
<PanelContainer Name="HolopadContactListPanel">
<ScrollContainer HorizontalExpand="True" VerticalExpand="True" Margin="8, 8, 8, 8" MinHeight="256">
<!-- If there is no data yet, this will be displayed -->
<BoxContainer Name="FetchingAvailableHolopadsContainer" HorizontalAlignment="Center" HorizontalExpand="True" VerticalExpand="True" ReservesSpace="False">
<Label Text="{Loc 'holopad-window-fetching-contacts-list'}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</BoxContainer>
<!-- Container for the contacts -->
<BoxContainer Name="ContactsList" Orientation="Vertical" HorizontalExpand="True" VerticalExpand="True" Margin="10 0 10 0"/>
</ScrollContainer>
</PanelContainer>
</BoxContainer>
<!-- Button to start an emergency broadcast (the user requires a certain level of access to interact with it) -->
<BoxContainer Name="StartBroadcastContainer" Orientation="Vertical" ReservesSpace="False" Visible="False">
<Button Name="StartBroadcastButton" Text="{Loc 'holopad-window-emergency-broadcast'}" Margin="10 0 10 5" Disabled="False" ReservesSpace="False"/>
</BoxContainer>
</BoxContainer>
</BoxContainer>
<!-- Footer -->
<BoxContainer Orientation="Vertical">
<PanelContainer StyleClasses="LowDivider" />
<BoxContainer Orientation="Horizontal" Margin="10 2 5 0" VerticalAlignment="Bottom">
<Label Text="{Loc 'holopad-window-flavor-left'}" StyleClasses="WindowFooterText" />
<Label Text="{Loc 'holopad-window-flavor-right'}" StyleClasses="WindowFooterText"
HorizontalAlignment="Right" HorizontalExpand="True" Margin="0 0 5 0" />
<TextureRect StyleClasses="NTLogoDark" Stretch="KeepAspectCentered"
VerticalAlignment="Center" HorizontalAlignment="Right" SetSize="19 19"/>
</BoxContainer>
</BoxContainer>
</BoxContainer>
</controls:FancyWindow>

View File

@@ -0,0 +1,338 @@
using Content.Client.Popups;
using Content.Client.UserInterface.Controls;
using Content.Shared.Access.Systems;
using Content.Shared.Holopad;
using Content.Shared.Telephone;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using System.Linq;
namespace Content.Client.Holopad;
[GenerateTypedNameReferences]
public sealed partial class HolopadWindow : FancyWindow
{
[Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IGameTiming _timing = default!;
private readonly SharedHolopadSystem _holopadSystem = default!;
private readonly SharedTelephoneSystem _telephoneSystem = default!;
private readonly AccessReaderSystem _accessReaderSystem = default!;
private readonly PopupSystem _popupSystem = default!;
private EntityUid? _owner = null;
private HolopadUiKey _currentUiKey;
private TelephoneState _currentState;
private TelephoneState _previousState;
private TimeSpan _buttonUnlockTime;
private float _updateTimer = 0.25f;
private const float UpdateTime = 0.25f;
private TimeSpan _buttonUnlockDelay = TimeSpan.FromSeconds(0.5f);
public event Action<NetEntity>? SendHolopadStartNewCallMessageAction;
public event Action? SendHolopadAnswerCallMessageAction;
public event Action? SendHolopadEndCallMessageAction;
public event Action? SendHolopadStartBroadcastMessageAction;
public event Action? SendHolopadActivateProjectorMessageAction;
public event Action? SendHolopadRequestStationAiMessageAction;
public HolopadWindow()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_holopadSystem = _entManager.System<SharedHolopadSystem>();
_telephoneSystem = _entManager.System<SharedTelephoneSystem>();
_accessReaderSystem = _entManager.System<AccessReaderSystem>();
_popupSystem = _entManager.System<PopupSystem>();
_buttonUnlockTime = _timing.CurTime + _buttonUnlockDelay;
// Assign button actions
AnswerCallButton.OnPressed += args => { OnHolopadAnswerCallMessage(); };
EndCallButton.OnPressed += args => { OnHolopadEndCallMessage(); };
StartBroadcastButton.OnPressed += args => { OnHolopadStartBroadcastMessage(); };
ActivateProjectorButton.OnPressed += args => { OnHolopadActivateProjectorMessage(); };
RequestStationAiButton.OnPressed += args => { OnHolopadRequestStationAiMessage(); };
// XML formatting
AnswerCallButton.AddStyleClass("ButtonAccept");
EndCallButton.AddStyleClass("Caution");
StartBroadcastButton.AddStyleClass("Caution");
HolopadContactListPanel.PanelOverride = new StyleBoxFlat
{
BackgroundColor = new Color(47, 47, 59) * Color.DarkGray,
BorderColor = new Color(82, 82, 82), //new Color(70, 73, 102),
BorderThickness = new Thickness(2),
};
HolopadContactListHeaderPanel.PanelOverride = new StyleBoxFlat
{
BackgroundColor = new Color(82, 82, 82),
};
EmergencyBroadcastText.SetMessage(FormattedMessage.FromMarkupOrThrow(Loc.GetString("holopad-window-emergency-broadcast-in-progress")));
SubtitleText.SetMessage(FormattedMessage.FromMarkupOrThrow(Loc.GetString("holopad-window-subtitle")));
OptionsText.SetMessage(FormattedMessage.FromMarkupOrThrow(Loc.GetString("holopad-window-options")));
}
#region: Button actions
private void OnSendHolopadStartNewCallMessage(NetEntity receiver)
{
SendHolopadStartNewCallMessageAction?.Invoke(receiver);
}
private void OnHolopadAnswerCallMessage()
{
SendHolopadAnswerCallMessageAction?.Invoke();
}
private void OnHolopadEndCallMessage()
{
SendHolopadEndCallMessageAction?.Invoke();
if (_currentUiKey == HolopadUiKey.AiRequestWindow)
Close();
}
private void OnHolopadStartBroadcastMessage()
{
if (_playerManager.LocalSession?.AttachedEntity == null || _owner == null)
return;
var player = _playerManager.LocalSession.AttachedEntity;
if (!_accessReaderSystem.IsAllowed(player.Value, _owner.Value))
{
_popupSystem.PopupClient(Loc.GetString("holopad-window-access-denied"), _owner.Value, player.Value);
return;
}
SendHolopadStartBroadcastMessageAction?.Invoke();
}
private void OnHolopadActivateProjectorMessage()
{
SendHolopadActivateProjectorMessageAction?.Invoke();
}
private void OnHolopadRequestStationAiMessage()
{
SendHolopadRequestStationAiMessageAction?.Invoke();
}
#endregion
public void SetState(EntityUid owner, HolopadUiKey uiKey)
{
_owner = owner;
_currentUiKey = uiKey;
// Determines what UI containers are available to the user.
// Components of these will be toggled on and off when
// UpdateAppearance() is called
switch (uiKey)
{
case HolopadUiKey.InteractionWindow:
RequestStationAiContainer.Visible = true;
HolopadContactListContainer.Visible = true;
StartBroadcastContainer.Visible = true;
break;
case HolopadUiKey.InteractionWindowForAi:
ActivateProjectorContainer.Visible = true;
StartBroadcastContainer.Visible = true;
break;
case HolopadUiKey.AiActionWindow:
HolopadContactListContainer.Visible = true;
StartBroadcastContainer.Visible = true;
break;
case HolopadUiKey.AiRequestWindow:
break;
}
}
public void UpdateState(Dictionary<NetEntity, string> holopads)
{
if (_owner == null || !_entManager.TryGetComponent<TelephoneComponent>(_owner.Value, out var telephone))
return;
// Caller ID text
var callerId = _telephoneSystem.GetFormattedCallerIdForEntity(telephone.LastCallerId.Item1, telephone.LastCallerId.Item2, Color.LightGray, "Default", 11);
CallerIdText.SetMessage(FormattedMessage.FromMarkupOrThrow(callerId));
LockOutIdText.SetMessage(FormattedMessage.FromMarkupOrThrow(callerId));
// Sort holopads alphabetically
var holopadArray = holopads.ToArray();
Array.Sort(holopadArray, AlphabeticalSort);
// Clear excess children from the contact list
while (ContactsList.ChildCount > holopadArray.Length)
ContactsList.RemoveChild(ContactsList.GetChild(ContactsList.ChildCount - 1));
// Make / update required children
for (int i = 0; i < holopadArray.Length; i++)
{
var (netEntity, label) = holopadArray[i];
if (i >= ContactsList.ChildCount)
{
var newContactButton = new HolopadContactButton();
newContactButton.OnPressed += args => { OnSendHolopadStartNewCallMessage(newContactButton.NetEntity); };
ContactsList.AddChild(newContactButton);
}
var child = ContactsList.GetChild(i);
if (child is not HolopadContactButton)
continue;
var contactButton = (HolopadContactButton)child;
contactButton.UpdateValues(netEntity, label);
}
// Update buttons
UpdateAppearance();
}
private void UpdateAppearance()
{
if (_owner == null || !_entManager.TryGetComponent<TelephoneComponent>(_owner.Value, out var telephone))
return;
if (_owner == null || !_entManager.TryGetComponent<HolopadComponent>(_owner.Value, out var holopad))
return;
var hasBroadcastAccess = !_holopadSystem.IsHolopadBroadcastOnCoolDown((_owner.Value, holopad));
var localPlayer = _playerManager.LocalSession?.AttachedEntity;
ControlsLockOutContainer.Visible = _holopadSystem.IsHolopadControlLocked((_owner.Value, holopad), localPlayer);
ControlsContainer.Visible = !ControlsLockOutContainer.Visible;
// Temporarily disable the interface buttons when the call state changes to prevent any misclicks
if (_currentState != telephone.CurrentState)
{
_previousState = _currentState;
_currentState = telephone.CurrentState;
_buttonUnlockTime = _timing.CurTime + _buttonUnlockDelay;
}
var lockButtons = _timing.CurTime < _buttonUnlockTime;
// Make / update required children
foreach (var child in ContactsList.Children)
{
if (child is not HolopadContactButton)
continue;
var contactButton = (HolopadContactButton)child;
contactButton.Disabled = (_currentState != TelephoneState.Idle || lockButtons);
}
// Update control text
var cooldown = _holopadSystem.GetHolopadBroadcastCoolDown((_owner.Value, holopad));
var cooldownString = $"{cooldown.Minutes:00}:{cooldown.Seconds:00}";
StartBroadcastButton.Text = _holopadSystem.IsHolopadBroadcastOnCoolDown((_owner.Value, holopad)) ?
Loc.GetString("holopad-window-emergency-broadcast-with-countdown", ("countdown", cooldownString)) :
Loc.GetString("holopad-window-emergency-broadcast");
var lockout = _holopadSystem.GetHolopadControlLockedPeriod((_owner.Value, holopad));
var lockoutString = $"{lockout.Minutes:00}:{lockout.Seconds:00}";
LockOutCountDownText.Text = Loc.GetString("holopad-window-controls-unlock-countdown", ("countdown", lockoutString));
switch (_currentState)
{
case TelephoneState.Idle:
CallStatusText.Text = Loc.GetString("holopad-window-no-calls-in-progress"); break;
case TelephoneState.Calling:
CallStatusText.Text = Loc.GetString("holopad-window-outgoing-call"); break;
case TelephoneState.Ringing:
CallStatusText.Text = (_currentUiKey == HolopadUiKey.AiRequestWindow) ?
Loc.GetString("holopad-window-ai-request") : Loc.GetString("holopad-window-incoming-call"); break;
case TelephoneState.InCall:
CallStatusText.Text = Loc.GetString("holopad-window-call-in-progress"); break;
case TelephoneState.EndingCall:
if (_previousState == TelephoneState.Calling || _previousState == TelephoneState.Idle)
CallStatusText.Text = Loc.GetString("holopad-window-call-rejected");
else
CallStatusText.Text = Loc.GetString("holopad-window-call-ending");
break;
}
// Update control disability
AnswerCallButton.Disabled = (_currentState != TelephoneState.Ringing || lockButtons);
EndCallButton.Disabled = (_currentState == TelephoneState.Idle || _currentState == TelephoneState.EndingCall || lockButtons);
StartBroadcastButton.Disabled = (_currentState != TelephoneState.Idle || !hasBroadcastAccess || lockButtons);
RequestStationAiButton.Disabled = (_currentState != TelephoneState.Idle || lockButtons);
ActivateProjectorButton.Disabled = (_currentState != TelephoneState.Idle || lockButtons);
// Update control visibility
FetchingAvailableHolopadsContainer.Visible = (ContactsList.ChildCount == 0);
ActiveCallControlsContainer.Visible = (_currentState != TelephoneState.Idle || _currentUiKey == HolopadUiKey.AiRequestWindow);
CallPlacementControlsContainer.Visible = !ActiveCallControlsContainer.Visible;
CallerIdText.Visible = (_currentState == TelephoneState.Ringing);
AnswerCallButton.Visible = (_currentState == TelephoneState.Ringing);
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
_updateTimer += args.DeltaSeconds;
if (_updateTimer >= UpdateTime)
{
_updateTimer -= UpdateTime;
UpdateAppearance();
}
}
private sealed class HolopadContactButton : Button
{
public NetEntity NetEntity;
public HolopadContactButton()
{
HorizontalExpand = true;
SetHeight = 32;
Margin = new Thickness(0f, 1f, 0f, 1f);
}
public void UpdateValues(NetEntity netEntity, string label)
{
NetEntity = netEntity;
Text = Loc.GetString("holopad-window-contact-label", ("label", label));
}
}
private int AlphabeticalSort(KeyValuePair<NetEntity, string> x, KeyValuePair<NetEntity, string> y)
{
if (string.IsNullOrEmpty(x.Value))
return -1;
if (string.IsNullOrEmpty(y.Value))
return 1;
return x.Value.CompareTo(y.Value);
}
}

View File

@@ -456,7 +456,21 @@ public sealed partial class LobbyUIController : UIController, IOnStateEntered<Lo
{
EntityUid dummyEnt;
if (humanoid is not null)
EntProtoId? previewEntity = null;
if (humanoid != null && jobClothes)
{
job ??= GetPreferredJob(humanoid);
previewEntity = job.JobPreviewEntity ?? (EntProtoId?)job?.JobEntity;
}
if (previewEntity != null)
{
// Special type like borg or AI, do not spawn a human just spawn the entity.
dummyEnt = EntityManager.SpawnEntity(previewEntity, MapCoordinates.Nullspace);
return dummyEnt;
}
else if (humanoid is not null)
{
var dummy = _prototypeManager.Index<SpeciesPrototype>(humanoid.Species).DollPrototype;
dummyEnt = EntityManager.SpawnEntity(dummy, MapCoordinates.Nullspace);
@@ -470,7 +484,8 @@ public sealed partial class LobbyUIController : UIController, IOnStateEntered<Lo
if (humanoid != null && jobClothes)
{
job ??= GetPreferredJob(humanoid);
DebugTools.Assert(job != null);
GiveDummyJobClothes(dummyEnt, humanoid, job);
if (_prototypeManager.HasIndex<RoleLoadoutPrototype>(LoadoutSystem.GetJobPrototype(job.ID)))

View File

@@ -6,6 +6,7 @@
SeparationOverride="0"
Name="InternalHBox">
<SpriteView Scale="2 2"
Margin="0 4 4 4"
OverrideDirection="South"
Name="View"/>
<Label Name="DescriptionLabel"

View File

@@ -148,7 +148,7 @@
</BoxContainer>
<!-- Right side -->
<BoxContainer Orientation="Vertical" VerticalExpand="True" VerticalAlignment="Center">
<SpriteView Name="SpriteView" Scale="8 8" SizeFlagsStretchRatio="1" />
<SpriteView Name="SpriteView" Scale="8 8" Margin="4" SizeFlagsStretchRatio="1" />
<BoxContainer Orientation="Horizontal" HorizontalAlignment="Center" Margin="0 5">
<Button Name="SpriteRotateLeft" Text="◀" StyleClasses="OpenRight" />
<cc:VSeparator Margin="2 0 3 0" />

View File

@@ -36,17 +36,18 @@ public sealed partial class LoadoutContainer : BoxContainer
if (_protoManager.TryIndex(proto, out var loadProto))
{
var ent = _entManager.System<LoadoutSystem>().GetFirstOrNull(loadProto);
var ent = loadProto.DummyEntity ?? _entManager.System<LoadoutSystem>().GetFirstOrNull(loadProto);
if (ent != null)
{
_entity = _entManager.SpawnEntity(ent, MapCoordinates.Nullspace);
Sprite.SetEntity(_entity);
if (ent == null)
return;
var spriteTooltip = new Tooltip();
spriteTooltip.SetMessage(FormattedMessage.FromUnformatted(_entManager.GetComponent<MetaDataComponent>(_entity.Value).EntityDescription));
TooltipSupplier = _ => spriteTooltip;
}
_entity = _entManager.SpawnEntity(ent, MapCoordinates.Nullspace);
Sprite.SetEntity(_entity);
var spriteTooltip = new Tooltip();
spriteTooltip.SetMessage(FormattedMessage.FromUnformatted(_entManager.GetComponent<MetaDataComponent>(_entity.Value).EntityDescription));
TooltipSupplier = _ => spriteTooltip;
}
}

View File

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

@@ -1,51 +0,0 @@
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,6 +9,7 @@ using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Utility;
using Robust.Client.UserInterface.RichText;
using Content.Client.UserInterface.RichText;
using Robust.Shared.Input;
namespace Content.Client.Paper.UI
@@ -43,7 +44,8 @@ namespace Content.Client.Paper.UI
typeof(BulletTag),
typeof(ColorTag),
typeof(HeadingTag),
typeof(ItalicTag)
typeof(ItalicTag),
typeof(MonoTag)
};
public event Action<string>? OnSaved;

View File

@@ -385,26 +385,6 @@ public partial class NavMapControl : MapGridControl
if (PostWallDrawingAction != null)
PostWallDrawingAction.Invoke(handle);
// Beacons
if (_beacons.Pressed)
{
var rectBuffer = new Vector2(5f, 3f);
// Calculate font size for current zoom level
var fontSize = (int) Math.Round(1 / WorldRange * DefaultDisplayedRange * UIScale * _targetFontsize, 0);
var font = new VectorFont(_cache.GetResource<FontResource>("/Fonts/NotoSans/NotoSans-Bold.ttf"), fontSize);
foreach (var beacon in _navMap.Beacons.Values)
{
var position = beacon.Position - offset;
position = ScalePosition(position with { Y = -position.Y });
var textDimensions = handle.GetDimensions(font, beacon.Text, 1f);
handle.DrawRect(new UIBox2(position - textDimensions / 2 - rectBuffer, position + textDimensions / 2 + rectBuffer), BackgroundColor);
handle.DrawString(font, position - textDimensions / 2, beacon.Text, beacon.Color);
}
}
var curTime = Timing.RealTime;
var blinkFrequency = 1f / 1f;
var lit = curTime.TotalSeconds % blinkFrequency > blinkFrequency / 2f;
@@ -443,11 +423,31 @@ public partial class NavMapControl : MapGridControl
position = ScalePosition(new Vector2(position.X, -position.Y));
var scalingCoefficient = MinmapScaleModifier * float.Sqrt(MinimapScale);
var positionOffset = new Vector2(scalingCoefficient * blip.Texture.Width, scalingCoefficient * blip.Texture.Height);
var positionOffset = new Vector2(scalingCoefficient * blip.Scale * blip.Texture.Width, scalingCoefficient * blip.Scale * blip.Texture.Height);
handle.DrawTextureRect(blip.Texture, new UIBox2(position - positionOffset, position + positionOffset), blip.Color);
}
}
// Beacons
if (_beacons.Pressed)
{
var rectBuffer = new Vector2(5f, 3f);
// Calculate font size for current zoom level
var fontSize = (int)Math.Round(1 / WorldRange * DefaultDisplayedRange * UIScale * _targetFontsize, 0);
var font = new VectorFont(_cache.GetResource<FontResource>("/Fonts/NotoSans/NotoSans-Bold.ttf"), fontSize);
foreach (var beacon in _navMap.Beacons.Values)
{
var position = beacon.Position - offset;
position = ScalePosition(position with { Y = -position.Y });
var textDimensions = handle.GetDimensions(font, beacon.Text, 1f);
handle.DrawRect(new UIBox2(position - textDimensions / 2 - rectBuffer, position + textDimensions / 2 + rectBuffer), BackgroundColor);
handle.DrawString(font, position - textDimensions / 2, beacon.Text, beacon.Color);
}
}
}
protected override void FrameUpdate(FrameEventArgs args)
@@ -689,6 +689,9 @@ public partial class NavMapControl : MapGridControl
Vector2i foundTermius;
Vector2i foundOrigin;
if (origin == terminus)
return;
// Does our new line end at the beginning of an existing line?
if (lookup.Remove(terminus, out foundTermius))
{
@@ -739,13 +742,15 @@ public struct NavMapBlip
public Color Color;
public bool Blinks;
public bool Selectable;
public float Scale;
public NavMapBlip(EntityCoordinates coordinates, Texture texture, Color color, bool blinks, bool selectable = true)
public NavMapBlip(EntityCoordinates coordinates, Texture texture, Color color, bool blinks, bool selectable = true, float scale = 1f)
{
Coordinates = coordinates;
Texture = texture;
Color = color;
Blinks = blinks;
Selectable = selectable;
Scale = scale;
}
}

View File

@@ -6,6 +6,7 @@ using Robust.Shared.Utility;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Numerics;
using Vector4 = Robust.Shared.Maths.Vector4;
namespace Content.Client.Power;
@@ -104,6 +105,26 @@ public sealed partial class PowerMonitoringWindow
// 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.
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)
@@ -443,6 +464,11 @@ public sealed class PowerMonitoringButton : Button
public BoxContainer MainContainer;
public TextureRect TextureRect;
public Label NameLocalized;
public ProgressBar BatteryLevel;
public PanelContainer BackgroundPanel;
public Label BatteryPercentage;
public Label PowerValue;
public PowerMonitoringButton()
@@ -478,6 +504,49 @@ public sealed class PowerMonitoringButton : Button
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()
{
HorizontalAlignment = HAlignment.Right,

View File

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

View File

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

View File

@@ -54,7 +54,7 @@ public sealed partial class StoreMenu : DefaultWindow
foreach (var ((_, amount), proto) in currency)
{
balanceStr += Loc.GetString("store-ui-balance-display", ("amount", amount),
("currency", Loc.GetString(proto.DisplayName, ("amount", 1))));
("currency", Loc.GetString(proto.DisplayName, ("amount", 1)))) + "\n";
}
BalanceInfo.SetMarkup(balanceStr.TrimEnd());
@@ -63,7 +63,10 @@ public sealed partial class StoreMenu : DefaultWindow
foreach (var type in currency)
{
if (type.Value.CanWithdraw && type.Value.Cash != null && type.Key.Item2 > 0)
{
disabled = false;
break;
}
}
WithdrawButton.Disabled = disabled;

View File

@@ -18,7 +18,7 @@ public sealed partial class StoreWithdrawWindow : DefaultWindow
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
private Dictionary<FixedPoint2, CurrencyPrototype> _validCurrencies = new();
private Dictionary<CurrencyPrototype, FixedPoint2> _validCurrencies = new();
private HashSet<CurrencyWithdrawButton> _buttons = new();
public event Action<BaseButton.ButtonEventArgs, string, int>? OnWithdrawAttempt;
@@ -36,7 +36,7 @@ public sealed partial class StoreWithdrawWindow : DefaultWindow
if (!_prototypeManager.TryIndex(currency.Key, out var proto))
continue;
_validCurrencies.Add(currency.Value, proto);
_validCurrencies.Add(proto, currency.Value);
}
//this shouldn't ever happen but w/e
@@ -47,14 +47,17 @@ public sealed partial class StoreWithdrawWindow : DefaultWindow
_buttons.Clear();
foreach (var currency in _validCurrencies)
{
if (!currency.Key.CanWithdraw)
continue;
var button = new CurrencyWithdrawButton()
{
Id = currency.Value.ID,
Amount = currency.Key,
Id = currency.Key.ID,
Amount = currency.Value,
MinHeight = 20,
Text = Loc.GetString("store-withdraw-button-ui", ("currency",Loc.GetString(currency.Value.DisplayName, ("amount", currency.Key)))),
Text = Loc.GetString("store-withdraw-button-ui", ("currency",Loc.GetString(currency.Key.DisplayName, ("amount", currency.Value)))),
Disabled = false,
};
button.Disabled = false;
button.OnPressed += args =>
{
OnWithdrawAttempt?.Invoke(args, button.Id, WithdrawSlider.Value);
@@ -65,7 +68,7 @@ public sealed partial class StoreWithdrawWindow : DefaultWindow
ButtonContainer.AddChild(button);
}
var maxWithdrawAmount = _validCurrencies.Keys.Max().Int();
var maxWithdrawAmount = _validCurrencies.Values.Max().Int();
// setup withdraw slider
WithdrawSlider.MinValue = 1;

View File

@@ -110,6 +110,7 @@ namespace Content.Client.Stylesheets
public static readonly Color ButtonColorGoodDefault = Color.FromHex("#3E6C45");
public static readonly Color ButtonColorGoodHovered = Color.FromHex("#31843E");
public static readonly Color ButtonColorGoodDisabled = Color.FromHex("#164420");
//NavMap
public static readonly Color PointRed = Color.FromHex("#B02E26");
@@ -1499,6 +1500,20 @@ namespace Content.Client.Stylesheets
Element<Button>().Class("ButtonColorGreen").Pseudo(ContainerButton.StylePseudoClassHover)
.Prop(Control.StylePropertyModulateSelf, ButtonColorGoodHovered),
// Accept button (merge with green button?) ---
Element<Button>().Class("ButtonAccept")
.Prop(Control.StylePropertyModulateSelf, ButtonColorGoodDefault),
Element<Button>().Class("ButtonAccept").Pseudo(ContainerButton.StylePseudoClassNormal)
.Prop(Control.StylePropertyModulateSelf, ButtonColorGoodDefault),
Element<Button>().Class("ButtonAccept").Pseudo(ContainerButton.StylePseudoClassHover)
.Prop(Control.StylePropertyModulateSelf, ButtonColorGoodHovered),
Element<Button>().Class("ButtonAccept").Pseudo(ContainerButton.StylePseudoClassDisabled)
.Prop(Control.StylePropertyModulateSelf, ButtonColorGoodDisabled),
// ---
// Small Button ---

View File

@@ -0,0 +1,8 @@
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;
if (EntityManager.TryGetComponent(_entity, out FootstepModifierComponent? step))
if (EntityManager.TryGetComponent(_entity, out FootstepModifierComponent? step) && step.FootstepSoundCollection != null)
{
var audioParams = step.FootstepSoundCollection.Params
.AddVolume(-7f)

View File

@@ -0,0 +1,34 @@
using System.Linq;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface.RichText;
using Robust.Shared.IoC;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Client.UserInterface.RichText;
/// <summary>
/// Sets the font to a monospaced variant
/// </summary>
public sealed class MonoTag : IMarkupTag
{
[ValidatePrototypeId<FontPrototype>] public const string MonoFont = "Monospace";
[Dependency] private readonly IResourceCache _resourceCache = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
public string Name => "mono";
/// <inheritdoc/>
public void PushDrawContext(MarkupNode node, MarkupDrawingContext context)
{
var font = FontTag.CreateFont(context.Font, node, _resourceCache, _prototypeManager, MonoFont);
context.Font.Push(font);
}
/// <inheritdoc/>
public void PopDrawContext(MarkupNode node, MarkupDrawingContext context)
{
context.Font.Pop();
}
}

View File

@@ -572,6 +572,10 @@ public sealed class UserAHelpUIHandler : IAHelpUIHandler
_window.OnClose += () => { OnClose?.Invoke(); };
_window.OnOpen += () => { OnOpen?.Invoke(); };
_window.Contents.AddChild(_chatPanel);
var introText = Loc.GetString("bwoink-system-introductory-message");
var introMessage = new SharedBwoinkSystem.BwoinkTextMessage( _ownerId, SharedBwoinkSystem.SystemUserId, introText);
Receive(introMessage);
}
public void Dispose()

View File

@@ -11,21 +11,17 @@
Orientation="Horizontal"
HorizontalAlignment="Stretch"
VerticalAlignment="Top"
SeparationOverride="5"
>
<GridContainer
Rows="2"
HSeparationOverride="5"
VSeparationOverride="5"
HorizontalExpand="True">
<ui:MenuButton
Name="EscapeButton"
Access="Internal"
Icon="{xe:Tex '/Textures/Interface/hamburger.svg.192dpi.png'}"
BoundKey = "{x:Static ic:EngineKeyFunctions.EscapeMenu}"
ToolTip="{Loc 'game-hud-open-escape-menu-button-tooltip'}"
MinSize="48 64"
MinSize="70 64"
HorizontalExpand="True"
AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}"
AppendStyleClass="{x:Static style:StyleBase.ButtonOpenRight}"
/>
<ui:MenuButton
Name="GuidebookButton"
@@ -33,7 +29,7 @@
Icon="{xe:Tex '/Textures/Interface/VerbIcons/information.svg.192dpi.png'}"
ToolTip="{Loc 'game-hud-open-guide-menu-button-tooltip'}"
BoundKey = "{x:Static is:ContentKeyFunctions.OpenGuidebook}"
MinSize="48 64"
MinSize="42 64"
HorizontalExpand="True"
AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}"
/>
@@ -43,7 +39,7 @@
Icon="{xe:Tex '/Textures/Interface/character.svg.192dpi.png'}"
ToolTip="{Loc 'game-hud-open-character-menu-button-tooltip'}"
BoundKey = "{x:Static is:ContentKeyFunctions.OpenCharacterMenu}"
MinSize="48 64"
MinSize="42 64"
HorizontalExpand="True"
AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}"
/>
@@ -53,7 +49,7 @@
Icon="{xe:Tex '/Textures/Interface/emotes.svg.192dpi.png'}"
ToolTip="{Loc 'game-hud-open-emotes-menu-button-tooltip'}"
BoundKey = "{x:Static is:ContentKeyFunctions.OpenEmotesMenu}"
MinSize="48 64"
MinSize="42 64"
HorizontalExpand="True"
AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}"
/>
@@ -63,7 +59,7 @@
Icon="{xe:Tex '/Textures/Interface/hammer.svg.192dpi.png'}"
BoundKey = "{x:Static is:ContentKeyFunctions.OpenCraftingMenu}"
ToolTip="{Loc 'game-hud-open-crafting-menu-button-tooltip'}"
MinSize="48 64"
MinSize="42 64"
HorizontalExpand="True"
AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}"
/>
@@ -73,7 +69,7 @@
Icon="{xe:Tex '/Textures/Interface/fist.svg.192dpi.png'}"
BoundKey = "{x:Static is:ContentKeyFunctions.OpenActionsMenu}"
ToolTip="{Loc 'game-hud-open-actions-menu-button-tooltip'}"
MinSize="48 64"
MinSize="42 64"
HorizontalExpand="True"
AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}"
/>
@@ -83,7 +79,7 @@
Icon="{xe:Tex '/Textures/Interface/gavel.svg.192dpi.png'}"
BoundKey = "{x:Static is:ContentKeyFunctions.OpenAdminMenu}"
ToolTip="{Loc 'game-hud-open-admin-menu-button-tooltip'}"
MinSize="48 64"
MinSize="42 64"
HorizontalExpand="True"
AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}"
/>
@@ -93,7 +89,7 @@
Icon="{xe:Tex '/Textures/Interface/sandbox.svg.192dpi.png'}"
BoundKey = "{x:Static is:ContentKeyFunctions.OpenSandboxWindow}"
ToolTip="{Loc 'game-hud-open-sandbox-menu-button-tooltip'}"
MinSize="48 64"
MinSize="42 64"
HorizontalExpand="True"
AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}"
/>
@@ -103,9 +99,8 @@
Icon="{xe:Tex '/Textures/Interface/info.svg.192dpi.png'}"
BoundKey = "{x:Static is:ContentKeyFunctions.OpenAHelp}"
ToolTip="{Loc 'ui-options-function-open-a-help'}"
MinSize="48 64"
MinSize="42 64"
HorizontalExpand="True"
AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}"
AppendStyleClass="{x:Static style:StyleBase.ButtonOpenLeft}"
/>
</GridContainer>
</widgets:GameTopMenuBar>

View File

@@ -1,4 +1,5 @@
using Content.Client.Administration.Managers;
using System.Numerics;
using Content.Client.Administration.Managers;
using Content.Client.Gameplay;
using Content.Client.Markers;
using Content.Client.Sandbox;
@@ -7,9 +8,7 @@ using Content.Client.UserInterface.Controls;
using Content.Client.UserInterface.Systems.DecalPlacer;
using Content.Client.UserInterface.Systems.Sandbox.Windows;
using Content.Shared.Input;
using Content.Shared.Silicons.StationAi;
using JetBrains.Annotations;
using Robust.Client.Console;
using Robust.Client.Debugging;
using Robust.Client.Graphics;
using Robust.Client.Input;
@@ -109,9 +108,13 @@ public sealed class SandboxUIController : UIController, IOnStateChanged<Gameplay
private void EnsureWindow()
{
if(_window is { Disposed: false })
if (_window is { Disposed: false })
return;
_window = UIManager.CreateWindow<SandboxWindow>();
// Pre-center the window without forcing it to the center every time.
_window.OpenCentered();
_window.Close();
_window.OnOpen += () => { SandboxButton!.Pressed = true; };
_window.OnClose += () => { SandboxButton!.Pressed = false; };
_window.ToggleLightButton.Pressed = !_light.Enabled;
@@ -149,7 +152,6 @@ public sealed class SandboxUIController : UIController, IOnStateChanged<Gameplay
_window.ToggleSubfloorButton.OnPressed += _ => _sandbox.ToggleSubFloor();
_window.ShowMarkersButton.OnPressed += _ => _sandbox.ShowMarkers();
_window.ShowBbButton.OnPressed += _ => _sandbox.ShowBb();
_window.MachineLinkingButton.OnPressed += _ => _sandbox.MachineLinking();
}
private void CheckSandboxVisibility()
@@ -164,7 +166,7 @@ public sealed class SandboxUIController : UIController, IOnStateChanged<Gameplay
{
if (_window != null)
{
_window.Dispose();
_window.Close();
_window = null;
}
@@ -209,7 +211,7 @@ public sealed class SandboxUIController : UIController, IOnStateChanged<Gameplay
if (_sandbox.SandboxAllowed && _window.IsOpen != true)
{
UIManager.ClickSound();
_window.OpenCentered();
_window.Open();
}
else
{

View File

@@ -4,20 +4,24 @@
Title="{Loc sandbox-window-title}"
Resizable="False">
<BoxContainer Orientation="Vertical" SeparationOverride="4">
<Button Name="AiOverlayButton" Access="Public" Text="{Loc sandbox-window-ai-overlay-button}" ToggleMode="True"/>
<Button Name="RespawnButton" Access="Public" Text="{Loc sandbox-window-respawn-button}"/>
<Button Name="SpawnEntitiesButton" Access="Public" Text="{Loc sandbox-window-spawn-entities-button}"/>
<Label Text="{Loc sandbox-window-map-editing-label}"/>
<Button Name="SpawnTilesButton" Access="Public" Text="{Loc sandbox-window-spawn-tiles-button}"/>
<Button Name="SpawnEntitiesButton" Access="Public" Text="{Loc sandbox-window-spawn-entities-button}"/>
<Button Name="SpawnDecalsButton" Access="Public" Text="{Loc sandbox-window-spawn-decals-button}"/>
<Button Name="GiveFullAccessButton" Access="Public" Text="{Loc sandbox-window-grant-full-access-button}"/>
<Button Name="GiveAghostButton" Access="Public" Text="{Loc sandbox-window-ghost-button}"/>
<Label Text="{Loc sandbox-window-visibility-label}"/>
<Button Name="ToggleLightButton" Access="Public" Text="{Loc sandbox-window-toggle-lights-button}" ToggleMode="True"/>
<Button Name="ToggleFovButton" Access="Public" Text="{Loc sandbox-window-toggle-fov-button}" ToggleMode="True"/>
<Button Name="ToggleShadowsButton" Access="Public" Text="{Loc sandbox-window-toggle-shadows-button}" ToggleMode="True"/>
<Button Name="ToggleSubfloorButton" Access="Public" Text="{Loc sandbox-window-toggle-subfloor-button}" ToggleMode="True"/>
<Button Name="SuicideButton" Access="Public" Text="{Loc sandbox-window-toggle-suicide-button}" ToggleMode="True"/>
<Button Name="AiOverlayButton" Access="Public" Text="{Loc sandbox-window-ai-overlay-button}" ToggleMode="True"/>
<Button Name="ShowMarkersButton" Access="Public" Text="{Loc sandbox-window-show-spawns-button}" ToggleMode="True"/>
<Button Name="ShowBbButton" Access="Public" Text="{Loc sandbox-window-show-bb-button}" ToggleMode="True"/>
<Button Name="MachineLinkingButton" Access="Public" Text="{Loc sandbox-window-link-machines-button}" ToggleMode="True"/>
<Label Text="{Loc sandbox-window-your-character-label}"/>
<Button Name="GiveAghostButton" Access="Public" Text="{Loc sandbox-window-ghost-button}"/>
<Button Name="GiveFullAccessButton" Access="Public" Text="{Loc sandbox-window-grant-full-access-button}"/>
<Button Name="SuicideButton" Access="Public" Text="{Loc sandbox-window-toggle-suicide-button}"/>
<Button Name="RespawnButton" Access="Public" Text="{Loc sandbox-window-respawn-button}"/>
</BoxContainer>
</windows:SandboxWindow>

View File

@@ -77,6 +77,7 @@ public sealed partial class GunSystem : SharedGunSystem
base.Initialize();
UpdatesOutsidePrediction = true;
SubscribeLocalEvent<AmmoCounterComponent, ItemStatusCollectMessage>(OnAmmoCounterCollect);
SubscribeLocalEvent<AmmoCounterComponent, UpdateClientAmmoEvent>(OnUpdateClientAmmo);
SubscribeAllEvent<MuzzleFlashEvent>(OnMuzzleFlash);
// Plays animated effects on the client.
@@ -86,6 +87,11 @@ public sealed partial class GunSystem : SharedGunSystem
InitializeSpentAmmo();
}
private void OnUpdateClientAmmo(EntityUid uid, AmmoCounterComponent ammoComp, ref UpdateClientAmmoEvent args)
{
UpdateAmmoCount(uid, ammoComp);
}
private void OnMuzzleFlash(MuzzleFlashEvent args)
{
var gunUid = GetEntity(args.Uid);

View File

@@ -0,0 +1,132 @@
using System.Collections.Generic;
using System.Linq;
using Content.Shared.Lathe;
using Content.Shared.Materials;
using Content.Shared.Prototypes;
using Content.Shared.Research.Prototypes;
using Content.Shared.Whitelist;
using Robust.Shared.GameObjects;
using Robust.Shared.Prototypes;
namespace Content.IntegrationTests.Tests.Lathe;
[TestFixture]
public sealed class LatheTest
{
[Test]
public async Task TestLatheRecipeIngredientsFitLathe()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var mapData = await pair.CreateTestMap();
var entMan = server.EntMan;
var protoMan = server.ProtoMan;
var compFactory = server.ResolveDependency<IComponentFactory>();
var materialStorageSystem = server.System<SharedMaterialStorageSystem>();
var whitelistSystem = server.System<EntityWhitelistSystem>();
await server.WaitAssertion(() =>
{
// Find all the lathes
var latheProtos = protoMan.EnumeratePrototypes<EntityPrototype>()
.Where(p => !p.Abstract)
.Where(p => !pair.IsTestPrototype(p))
.Where(p => p.HasComponent<LatheComponent>());
// Find every EntityPrototype that can be inserted into a MaterialStorage
var materialEntityProtos = protoMan.EnumeratePrototypes<EntityPrototype>()
.Where(p => !p.Abstract)
.Where(p => !pair.IsTestPrototype(p))
.Where(p => p.HasComponent<PhysicalCompositionComponent>());
// Spawn all of the above material EntityPrototypes - we need actual entities to do whitelist checks
var materialEntities = new List<EntityUid>(materialEntityProtos.Count());
foreach (var materialEntityProto in materialEntityProtos)
{
materialEntities.Add(entMan.SpawnEntity(materialEntityProto.ID, mapData.GridCoords));
}
Assert.Multiple(() =>
{
// Check each lathe individually
foreach (var latheProto in latheProtos)
{
if (!latheProto.TryGetComponent<LatheComponent>(out var latheComp, compFactory))
continue;
if (!latheProto.TryGetComponent<MaterialStorageComponent>(out var storageComp, compFactory))
continue;
// Test which material-containing entities are accepted by this lathe
var acceptedMaterials = new HashSet<ProtoId<MaterialPrototype>>();
foreach (var materialEntity in materialEntities)
{
Assert.That(entMan.TryGetComponent<PhysicalCompositionComponent>(materialEntity, out var compositionComponent));
if (whitelistSystem.IsWhitelistFail(storageComp.Whitelist, materialEntity))
continue;
// Mark the lathe as accepting each material in the entity
foreach (var (material, _) in compositionComponent.MaterialComposition)
{
acceptedMaterials.Add(material);
}
}
// Collect all the recipes assigned to this lathe
var recipes = new List<ProtoId<LatheRecipePrototype>>();
recipes.AddRange(latheComp.StaticRecipes);
recipes.AddRange(latheComp.DynamicRecipes);
if (latheProto.TryGetComponent<EmagLatheRecipesComponent>(out var emagRecipesComp, compFactory))
{
recipes.AddRange(emagRecipesComp.EmagStaticRecipes);
recipes.AddRange(emagRecipesComp.EmagDynamicRecipes);
}
// Check each recipe assigned to this lathe
foreach (var recipeId in recipes)
{
Assert.That(protoMan.TryIndex(recipeId, out var recipeProto));
// Track the total material volume of the recipe
var totalQuantity = 0;
// Check each material called for by the recipe
foreach (var (materialId, quantity) in recipeProto.Materials)
{
Assert.That(protoMan.TryIndex(materialId, out var materialProto));
// Make sure the material is accepted by the lathe
Assert.That(acceptedMaterials, Does.Contain(materialId), $"Lathe {latheProto.ID} has recipe {recipeId} but does not accept any materials containing {materialId}");
totalQuantity += quantity;
}
// Make sure the recipe doesn't call for more material than the lathe can hold
if (storageComp.StorageLimit != null)
Assert.That(totalQuantity, Is.LessThanOrEqualTo(storageComp.StorageLimit), $"Lathe {latheProto.ID} has recipe {recipeId} which calls for {totalQuantity} units of materials but can only hold {storageComp.StorageLimit}");
}
}
});
});
await pair.CleanReturnAsync();
}
[Test]
public async Task AllLatheRecipesValidTest()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var proto = server.ProtoMan;
Assert.Multiple(() =>
{
foreach (var recipe in proto.EnumeratePrototypes<LatheRecipePrototype>())
{
if (recipe.Result == null)
Assert.That(recipe.ResultReagents, Is.Not.Null, $"Recipe '{recipe.ID}' has no result or result reagents.");
}
});
await pair.CleanReturnAsync();
}
}

View File

@@ -81,7 +81,11 @@ namespace Content.IntegrationTests.Tests
"Train",
"Oasis",
"Cog",
"Amber"
"Gate",
"Amber",
"Loop"
};
/// <summary>

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Content.Server.Administration.Managers;
using Robust.Shared.Toolshed;
namespace Content.IntegrationTests.Tests.Toolshed;
@@ -10,10 +11,23 @@ public sealed class AdminTest : ToolshedTest
[Test]
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(() =>
{
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.");
Assert.Multiple(() =>
{
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.Globalization;
using Robust.Shared.IoC;
using System.Reflection;
using Robust.Shared.Localization;
using Robust.Shared.Toolshed;
@@ -14,10 +14,27 @@ public sealed class LocTest : ToolshedTest
[Test]
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(() =>
{
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.");
Assert.Multiple(() =>
{
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!;
}
protected void ParseCommand(string command, Type? inputType = null, Type? expectedType = null, bool once = false)
protected void ParseCommand(string command, Type? inputType = null, Type? expectedType = null)
{
var parser = new ParserContext(command, Toolshed);
var success = CommandRun.TryParse(false, parser, inputType, expectedType, once, out _, out _, out var error);
var success = CommandRun.TryParse(parser, inputType, expectedType, out _);
if (error is not null)
ReportError(error);
if (parser.Error is not null)
ReportError(parser.Error);
if (error is null)
if (parser.Error is null)
Assert.That(success, $"Parse failed despite no error being reported. Parsed {command}");
}
@@ -153,11 +153,28 @@ public abstract class ToolshedTest : IInvocationContext
return _errors;
}
public bool HasErrors => _errors.Count > 0;
public void ClearErrors()
{
_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();
protected void ExpectError(Type err)

View File

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

View File

@@ -408,6 +408,17 @@ namespace Content.Server.Administration.Managers
}
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)
|| _promotedPlayers.Contains(session.UserId)

View File

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

View File

@@ -204,6 +204,7 @@ public sealed partial class AdminVerbSystem
var recharger = EnsureComp<BatterySelfRechargerComponent>(args.Target);
recharger.AutoRecharge = true;
recharger.AutoRechargeRate = battery.MaxCharge; // Instant refill.
recharger.AutoRechargePause = false; // No delay.
},
Impact = LogImpact.Medium,
Message = Loc.GetString("admin-trick-infinite-battery-object-description"),
@@ -603,6 +604,7 @@ public sealed partial class AdminVerbSystem
recharger.AutoRecharge = true;
recharger.AutoRechargeRate = battery.MaxCharge; // Instant refill.
recharger.AutoRechargePause = false; // No delay.
}
},
Impact = LogImpact.Extreme,

View File

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

View File

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

View File

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

View File

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

View File

@@ -169,11 +169,13 @@ public sealed class AmeNodeGroup : BaseNodeGroup
public float CalculatePower(int fuel, int cores)
{
// Balanced around a single core AME with injection level 2 producing 120KW.
// Overclocking yields diminishing returns until it evens out at around 360KW.
// Two core with four injection is 150kW. Two core with two injection is 90kW.
// 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.
// However, for the relative amounts for each (1 core at 2 and 2 core at 4), more cores has more output.
return 200000f * MathF.Log10(fuel * fuel) * MathF.Pow(0.75f, cores - 1);
// Increasing core count creates diminishing returns, increasing injection amount increases
// Unlike the previous solution, increasing fuel and cores always leads to an increase in power, even if by very small amounts.
// Increasing core count without increasing fuel always leads to reduced power as well.
// 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()

View File

@@ -1,3 +1,4 @@
using Content.Server.Animals.Systems;
using Content.Shared.Storage;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
@@ -9,44 +10,47 @@ namespace Content.Server.Animals.Components;
/// It also grants an action to players who are controlling these entities, allowing them to do it manually.
/// </summary>
[RegisterComponent]
[RegisterComponent, Access(typeof(EggLayerSystem)), AutoGenerateComponentPause]
public sealed partial class EggLayerComponent : Component
{
/// <summary>
/// The item that gets laid/spawned, retrieved from animal prototype.
/// </summary>
[DataField(required: true)]
public List<EntitySpawnEntry> EggSpawn = new();
/// <summary>
/// Player action.
/// </summary>
[DataField]
public EntProtoId EggLayAction = "ActionAnimalLayEgg";
/// <summary>
/// The amount of nutrient consumed on update.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public float HungerUsage = 60f;
[DataField]
public SoundSpecifier EggLaySound = new SoundPathSpecifier("/Audio/Effects/pop.ogg");
/// <summary>
/// Minimum cooldown used for the automatic egg laying.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
[DataField]
public float EggLayCooldownMin = 60f;
/// <summary>
/// Maximum cooldown used for the automatic egg laying.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
[DataField]
public float EggLayCooldownMax = 120f;
/// <summary>
/// Set during component init.
/// The amount of nutrient consumed on update.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public float CurrentEggLayCooldown;
[DataField(required: true), ViewVariables(VVAccess.ReadWrite)]
public List<EntitySpawnEntry> EggSpawn = default!;
[DataField]
public SoundSpecifier EggLaySound = new SoundPathSpecifier("/Audio/Effects/pop.ogg");
[DataField]
public float AccumulatedFrametime;
public float HungerUsage = 60f;
[DataField] public EntityUid? Action;
/// <summary>
/// When to next try to produce.
/// </summary>
[DataField, AutoPausedField]
public TimeSpan NextGrowth = TimeSpan.Zero;
}

View File

@@ -1,59 +0,0 @@
using Content.Server.Animals.Systems;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.FixedPoint;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Server.Animals.Components
/// <summary>
/// Lets an entity produce milk. Uses hunger if present.
/// </summary>
{
[RegisterComponent, Access(typeof(UdderSystem))]
internal sealed partial class UdderComponent : Component
{
/// <summary>
/// The reagent to produce.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadOnly)]
public ProtoId<ReagentPrototype> ReagentId = "Milk";
/// <summary>
/// The name of <see cref="Solution"/>.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadOnly)]
public string SolutionName = "udder";
/// <summary>
/// The solution to add reagent to.
/// </summary>
[DataField]
public Entity<SolutionComponent>? Solution = null;
/// <summary>
/// The amount of reagent to be generated on update.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadOnly)]
public FixedPoint2 QuantityPerUpdate = 25;
/// <summary>
/// The amount of nutrient consumed on update.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public float HungerUsage = 10f;
/// <summary>
/// How long to wait before producing.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public TimeSpan GrowthDelay = TimeSpan.FromMinutes(1);
/// <summary>
/// When to next try to produce.
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite)]
public TimeSpan NextGrowth = TimeSpan.FromSeconds(0);
}
}

View File

@@ -7,15 +7,15 @@ using Content.Shared.Nutrition.Components;
using Content.Shared.Nutrition.EntitySystems;
using Content.Shared.Storage;
using Robust.Server.Audio;
using Robust.Server.GameObjects;
using Robust.Shared.Player;
using Robust.Shared.Random;
using Robust.Shared.Timing;
namespace Content.Server.Animals.Systems;
/// <summary>
/// Gives ability to produce eggs, produces endless if the
/// owner has no HungerComponent
/// Gives the ability to lay eggs/other things;
/// produces endlessly if the owner does not have a HungerComponent.
/// </summary>
public sealed class EggLayerSystem : EntitySystem
{
@@ -23,6 +23,7 @@ public sealed class EggLayerSystem : EntitySystem
[Dependency] private readonly ActionsSystem _actions = default!;
[Dependency] private readonly AudioSystem _audio = default!;
[Dependency] private readonly HungerSystem _hunger = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
@@ -37,7 +38,6 @@ public sealed class EggLayerSystem : EntitySystem
public override void Update(float frameTime)
{
base.Update(frameTime);
var query = EntityQueryEnumerator<EggLayerComponent>();
while (query.MoveNext(out var uid, out var eggLayer))
{
@@ -45,13 +45,17 @@ public sealed class EggLayerSystem : EntitySystem
if (HasComp<ActorComponent>(uid))
continue;
eggLayer.AccumulatedFrametime += frameTime;
if (eggLayer.AccumulatedFrametime < eggLayer.CurrentEggLayCooldown)
if (_timing.CurTime < eggLayer.NextGrowth)
continue;
eggLayer.AccumulatedFrametime -= eggLayer.CurrentEggLayCooldown;
eggLayer.CurrentEggLayCooldown = _random.NextFloat(eggLayer.EggLayCooldownMin, eggLayer.EggLayCooldownMax);
// Randomize next growth time for more organic egglaying.
eggLayer.NextGrowth += TimeSpan.FromSeconds(_random.NextFloat(eggLayer.EggLayCooldownMin, eggLayer.EggLayCooldownMax));
if (_mobState.IsDead(uid))
continue;
// Hungerlevel check/modification is done in TryLayEgg()
// so it's used for player controlled chickens as well.
TryLayEgg(uid, eggLayer);
}
@@ -60,11 +64,12 @@ public sealed class EggLayerSystem : EntitySystem
private void OnMapInit(EntityUid uid, EggLayerComponent component, MapInitEvent args)
{
_actions.AddAction(uid, ref component.Action, component.EggLayAction);
component.CurrentEggLayCooldown = _random.NextFloat(component.EggLayCooldownMin, component.EggLayCooldownMax);
component.NextGrowth = _timing.CurTime + TimeSpan.FromSeconds(_random.NextFloat(component.EggLayCooldownMin, component.EggLayCooldownMax));
}
private void OnEggLayAction(EntityUid uid, EggLayerComponent egglayer, EggLayInstantActionEvent args)
{
// Cooldown is handeled by ActionAnimalLayEgg in types.yml.
args.Handled = TryLayEgg(uid, egglayer);
}
@@ -76,10 +81,10 @@ public sealed class EggLayerSystem : EntitySystem
if (_mobState.IsDead(uid))
return false;
// Allow infinitely laying eggs if they can't get hungry
// Allow infinitely laying eggs if they can't get hungry.
if (TryComp<HungerComponent>(uid, out var hunger))
{
if (hunger.CurrentHunger < egglayer.HungerUsage)
if (_hunger.GetHunger(hunger) < egglayer.HungerUsage)
{
_popup.PopupEntity(Loc.GetString("action-popup-lay-egg-too-hungry"), uid, uid);
return false;

View File

@@ -186,6 +186,11 @@ public sealed class InnerBodyAnomalySystem : SharedInnerBodyAnomalySystem
if (args.NewMobState != MobState.Dead)
return;
var ev = new BeforeRemoveAnomalyOnDeathEvent();
RaiseLocalEvent(args.Target, ref ev);
if (ev.Cancelled)
return;
_anomaly.ChangeAnomalyHealth(ent, -2); //Shutdown it
}

View File

@@ -0,0 +1,542 @@
using Content.Server.Atmos.Components;
using Content.Server.Atmos.Piping.Components;
using Content.Server.DeviceNetwork.Components;
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.EntitySystems;
using Content.Server.NodeContainer.NodeGroups;
using Content.Server.NodeContainer.Nodes;
using Content.Server.Power.Components;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Components;
using Content.Shared.Atmos.Consoles;
using Content.Shared.Labels.Components;
using Content.Shared.Pinpointer;
using Robust.Server.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Timing;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
namespace Content.Server.Atmos.Consoles;
public sealed class AtmosMonitoringConsoleSystem : SharedAtmosMonitoringConsoleSystem
{
[Dependency] private readonly UserInterfaceSystem _userInterfaceSystem = default!;
[Dependency] private readonly SharedMapSystem _sharedMapSystem = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
// Private variables
// Note: this data does not need to be saved
private Dictionary<EntityUid, Dictionary<Vector2i, AtmosPipeChunk>> _gridAtmosPipeChunks = new();
private float _updateTimer = 1.0f;
// Constants
private const float UpdateTime = 1.0f;
private const int ChunkSize = 4;
public override void Initialize()
{
base.Initialize();
// Console events
SubscribeLocalEvent<AtmosMonitoringConsoleComponent, ComponentInit>(OnConsoleInit);
SubscribeLocalEvent<AtmosMonitoringConsoleComponent, AnchorStateChangedEvent>(OnConsoleAnchorChanged);
SubscribeLocalEvent<AtmosMonitoringConsoleComponent, EntParentChangedMessage>(OnConsoleParentChanged);
// Tracked device events
SubscribeLocalEvent<AtmosMonitoringConsoleDeviceComponent, NodeGroupsRebuilt>(OnEntityNodeGroupsRebuilt);
SubscribeLocalEvent<AtmosMonitoringConsoleDeviceComponent, AtmosPipeColorChangedEvent>(OnEntityPipeColorChanged);
SubscribeLocalEvent<AtmosMonitoringConsoleDeviceComponent, EntityTerminatingEvent>(OnEntityShutdown);
// Grid events
SubscribeLocalEvent<GridSplitEvent>(OnGridSplit);
}
#region Event handling
private void OnConsoleInit(EntityUid uid, AtmosMonitoringConsoleComponent component, ComponentInit args)
{
InitializeAtmosMonitoringConsole(uid, component);
}
private void OnConsoleAnchorChanged(EntityUid uid, AtmosMonitoringConsoleComponent component, AnchorStateChangedEvent args)
{
InitializeAtmosMonitoringConsole(uid, component);
}
private void OnConsoleParentChanged(EntityUid uid, AtmosMonitoringConsoleComponent component, EntParentChangedMessage args)
{
component.ForceFullUpdate = true;
InitializeAtmosMonitoringConsole(uid, component);
}
private void OnEntityNodeGroupsRebuilt(EntityUid uid, AtmosMonitoringConsoleDeviceComponent component, NodeGroupsRebuilt args)
{
InitializeAtmosMonitoringDevice(uid, component);
}
private void OnEntityPipeColorChanged(EntityUid uid, AtmosMonitoringConsoleDeviceComponent component, AtmosPipeColorChangedEvent args)
{
InitializeAtmosMonitoringDevice(uid, component);
}
private void OnEntityShutdown(EntityUid uid, AtmosMonitoringConsoleDeviceComponent component, EntityTerminatingEvent args)
{
ShutDownAtmosMonitoringEntity(uid, component);
}
private void OnGridSplit(ref GridSplitEvent args)
{
// Collect grids
var allGrids = args.NewGrids.ToList();
if (!allGrids.Contains(args.Grid))
allGrids.Add(args.Grid);
// Rebuild the pipe networks on the affected grids
foreach (var ent in allGrids)
{
if (!TryComp<MapGridComponent>(ent, out var grid))
continue;
RebuildAtmosPipeGrid(ent, grid);
}
// Update atmos monitoring consoles that stand upon an updated grid
var query = AllEntityQuery<AtmosMonitoringConsoleComponent, TransformComponent>();
while (query.MoveNext(out var ent, out var entConsole, out var entXform))
{
if (entXform.GridUid == null)
continue;
if (!allGrids.Contains(entXform.GridUid.Value))
continue;
InitializeAtmosMonitoringConsole(ent, entConsole);
}
}
#endregion
#region UI updates
public override void Update(float frameTime)
{
base.Update(frameTime);
_updateTimer += frameTime;
if (_updateTimer >= UpdateTime)
{
_updateTimer -= UpdateTime;
var query = AllEntityQuery<AtmosMonitoringConsoleComponent, TransformComponent>();
while (query.MoveNext(out var ent, out var entConsole, out var entXform))
{
if (entXform?.GridUid == null)
continue;
UpdateUIState(ent, entConsole, entXform);
}
}
}
public void UpdateUIState
(EntityUid uid,
AtmosMonitoringConsoleComponent component,
TransformComponent xform)
{
if (!_userInterfaceSystem.IsUiOpen(uid, AtmosMonitoringConsoleUiKey.Key))
return;
var gridUid = xform.GridUid!.Value;
if (!TryComp<MapGridComponent>(gridUid, out var mapGrid))
return;
if (!TryComp<GridAtmosphereComponent>(gridUid, out var atmosphere))
return;
// The grid must have a NavMapComponent to visualize the map in the UI
EnsureComp<NavMapComponent>(gridUid);
// Gathering data to be send to the client
var atmosNetworks = new List<AtmosMonitoringConsoleEntry>();
var query = AllEntityQuery<GasPipeSensorComponent, TransformComponent>();
while (query.MoveNext(out var ent, out var entSensor, out var entXform))
{
if (entXform?.GridUid != xform.GridUid)
continue;
if (!entXform.Anchored)
continue;
var entry = CreateAtmosMonitoringConsoleEntry(ent, entXform);
if (entry != null)
atmosNetworks.Add(entry.Value);
}
// Set the UI state
_userInterfaceSystem.SetUiState(uid, AtmosMonitoringConsoleUiKey.Key,
new AtmosMonitoringConsoleBoundInterfaceState(atmosNetworks.ToArray()));
}
private AtmosMonitoringConsoleEntry? CreateAtmosMonitoringConsoleEntry(EntityUid uid, TransformComponent xform)
{
AtmosMonitoringConsoleEntry? entry = null;
var netEnt = GetNetEntity(uid);
var name = MetaData(uid).EntityName;
var address = string.Empty;
if (xform.GridUid == null)
return null;
if (!TryGettingFirstPipeNode(uid, out var pipeNode, out var netId) ||
pipeNode == null ||
netId == null)
return null;
var pipeColor = TryComp<AtmosPipeColorComponent>(uid, out var colorComponent) ? colorComponent.Color : Color.White;
// Name the entity based on its label, if available
if (TryComp<LabelComponent>(uid, out var label) && label.CurrentLabel != null)
name = label.CurrentLabel;
// Otherwise use its base name and network address
else if (TryComp<DeviceNetworkComponent>(uid, out var deviceNet))
address = deviceNet.Address;
// Entry for unpowered devices
if (TryComp<ApcPowerReceiverComponent>(uid, out var apcPowerReceiver) && !apcPowerReceiver.Powered)
{
entry = new AtmosMonitoringConsoleEntry(netEnt, GetNetCoordinates(xform.Coordinates), netId.Value, name, address)
{
IsPowered = false,
Color = pipeColor
};
return entry;
}
// Entry for powered devices
var gasData = new Dictionary<Gas, float>();
var isAirPresent = pipeNode.Air.TotalMoles > 0;
if (isAirPresent)
{
foreach (var gas in Enum.GetValues<Gas>())
{
if (pipeNode.Air[(int)gas] > 0)
gasData.Add(gas, pipeNode.Air[(int)gas] / pipeNode.Air.TotalMoles);
}
}
entry = new AtmosMonitoringConsoleEntry(netEnt, GetNetCoordinates(xform.Coordinates), netId.Value, name, address)
{
TemperatureData = isAirPresent ? pipeNode.Air.Temperature : 0f,
PressureData = pipeNode.Air.Pressure,
TotalMolData = pipeNode.Air.TotalMoles,
GasData = gasData,
Color = pipeColor
};
return entry;
}
private Dictionary<NetEntity, AtmosDeviceNavMapData> GetAllAtmosDeviceNavMapData(EntityUid gridUid)
{
var atmosDeviceNavMapData = new Dictionary<NetEntity, AtmosDeviceNavMapData>();
var query = AllEntityQuery<AtmosMonitoringConsoleDeviceComponent, TransformComponent>();
while (query.MoveNext(out var ent, out var entComponent, out var entXform))
{
if (TryGetAtmosDeviceNavMapData(ent, entComponent, entXform, gridUid, out var data))
atmosDeviceNavMapData.Add(data.Value.NetEntity, data.Value);
}
return atmosDeviceNavMapData;
}
private bool TryGetAtmosDeviceNavMapData
(EntityUid uid,
AtmosMonitoringConsoleDeviceComponent component,
TransformComponent xform,
EntityUid gridUid,
[NotNullWhen(true)] out AtmosDeviceNavMapData? device)
{
device = null;
if (component.NavMapBlip == null)
return false;
if (xform.GridUid != gridUid)
return false;
if (!xform.Anchored)
return false;
var direction = xform.LocalRotation.GetCardinalDir();
if (!TryGettingFirstPipeNode(uid, out var _, out var netId))
netId = -1;
var color = Color.White;
if (TryComp<AtmosPipeColorComponent>(uid, out var atmosPipeColor))
color = atmosPipeColor.Color;
device = new AtmosDeviceNavMapData(GetNetEntity(uid), GetNetCoordinates(xform.Coordinates), netId.Value, component.NavMapBlip.Value, direction, color);
return true;
}
#endregion
#region Pipe net functions
private void RebuildAtmosPipeGrid(EntityUid gridUid, MapGridComponent grid)
{
var allChunks = new Dictionary<Vector2i, AtmosPipeChunk>();
// Adds all atmos pipes to the nav map via bit mask chunks
var queryPipes = AllEntityQuery<AtmosPipeColorComponent, NodeContainerComponent, TransformComponent>();
while (queryPipes.MoveNext(out var ent, out var entAtmosPipeColor, out var entNodeContainer, out var entXform))
{
if (entXform.GridUid != gridUid)
continue;
if (!entXform.Anchored)
continue;
var tile = _sharedMapSystem.GetTileRef(gridUid, grid, entXform.Coordinates);
var chunkOrigin = SharedMapSystem.GetChunkIndices(tile.GridIndices, ChunkSize);
var relative = SharedMapSystem.GetChunkRelative(tile.GridIndices, ChunkSize);
if (!allChunks.TryGetValue(chunkOrigin, out var chunk))
{
chunk = new AtmosPipeChunk(chunkOrigin);
allChunks[chunkOrigin] = chunk;
}
UpdateAtmosPipeChunk(ent, entNodeContainer, entAtmosPipeColor, GetTileIndex(relative), ref chunk);
}
// Add or update the chunks on the associated grid
_gridAtmosPipeChunks[gridUid] = allChunks;
// Update the consoles that are on the same grid
var queryConsoles = AllEntityQuery<AtmosMonitoringConsoleComponent, TransformComponent>();
while (queryConsoles.MoveNext(out var ent, out var entConsole, out var entXform))
{
if (gridUid != entXform.GridUid)
continue;
entConsole.AtmosPipeChunks = allChunks;
Dirty(ent, entConsole);
}
}
private void RebuildSingleTileOfPipeNetwork(EntityUid gridUid, MapGridComponent grid, EntityCoordinates coords)
{
if (!_gridAtmosPipeChunks.TryGetValue(gridUid, out var allChunks))
allChunks = new Dictionary<Vector2i, AtmosPipeChunk>();
var tile = _sharedMapSystem.GetTileRef(gridUid, grid, coords);
var chunkOrigin = SharedMapSystem.GetChunkIndices(tile.GridIndices, ChunkSize);
var relative = SharedMapSystem.GetChunkRelative(tile.GridIndices, ChunkSize);
var tileIdx = GetTileIndex(relative);
if (!allChunks.TryGetValue(chunkOrigin, out var chunk))
chunk = new AtmosPipeChunk(chunkOrigin);
// Remove all stale values for the tile
foreach (var (index, atmosPipeData) in chunk.AtmosPipeData)
{
var mask = (ulong)SharedNavMapSystem.AllDirMask << tileIdx * SharedNavMapSystem.Directions;
chunk.AtmosPipeData[index] = atmosPipeData & ~mask;
}
// Rebuild the tile's pipe data
foreach (var ent in _sharedMapSystem.GetAnchoredEntities(gridUid, grid, coords))
{
if (!TryComp<AtmosPipeColorComponent>(ent, out var entAtmosPipeColor))
continue;
if (!TryComp<NodeContainerComponent>(ent, out var entNodeContainer))
continue;
UpdateAtmosPipeChunk(ent, entNodeContainer, entAtmosPipeColor, tileIdx, ref chunk);
}
// Add or update the chunk on the associated grid
// Only the modified chunk will be sent to the client
chunk.LastUpdate = _gameTiming.CurTick;
allChunks[chunkOrigin] = chunk;
_gridAtmosPipeChunks[gridUid] = allChunks;
// Update the components of the monitoring consoles that are attached to the same grid
var query = AllEntityQuery<AtmosMonitoringConsoleComponent, TransformComponent>();
while (query.MoveNext(out var ent, out var entConsole, out var entXform))
{
if (gridUid != entXform.GridUid)
continue;
entConsole.AtmosPipeChunks = allChunks;
Dirty(ent, entConsole);
}
}
private void UpdateAtmosPipeChunk(EntityUid uid, NodeContainerComponent nodeContainer, AtmosPipeColorComponent pipeColor, int tileIdx, ref AtmosPipeChunk chunk)
{
// Entities that are actively being deleted are not to be drawn
if (MetaData(uid).EntityLifeStage >= EntityLifeStage.Terminating)
return;
foreach ((var id, var node) in nodeContainer.Nodes)
{
if (node is not PipeNode)
continue;
var pipeNode = (PipeNode)node;
var netId = GetPipeNodeNetId(pipeNode);
var pipeDirection = pipeNode.CurrentPipeDirection;
chunk.AtmosPipeData.TryGetValue((netId, pipeColor.Color.ToHex()), out var atmosPipeData);
atmosPipeData |= (ulong)pipeDirection << tileIdx * SharedNavMapSystem.Directions;
chunk.AtmosPipeData[(netId, pipeColor.Color.ToHex())] = atmosPipeData;
}
}
private bool TryGettingFirstPipeNode(EntityUid uid, [NotNullWhen(true)] out PipeNode? pipeNode, [NotNullWhen(true)] out int? netId)
{
pipeNode = null;
netId = null;
if (!TryComp<NodeContainerComponent>(uid, out var nodeContainer))
return false;
foreach (var node in nodeContainer.Nodes.Values)
{
if (node is PipeNode)
{
pipeNode = (PipeNode)node;
netId = GetPipeNodeNetId(pipeNode);
return true;
}
}
return false;
}
private int GetPipeNodeNetId(PipeNode pipeNode)
{
if (pipeNode.NodeGroup is BaseNodeGroup)
{
var nodeGroup = (BaseNodeGroup)pipeNode.NodeGroup;
return nodeGroup.NetId;
}
return -1;
}
#endregion
#region Initialization functions
private void InitializeAtmosMonitoringConsole(EntityUid uid, AtmosMonitoringConsoleComponent component)
{
var xform = Transform(uid);
if (xform.GridUid == null)
return;
var grid = xform.GridUid.Value;
if (!TryComp<MapGridComponent>(grid, out var map))
return;
component.AtmosDevices = GetAllAtmosDeviceNavMapData(grid);
if (!_gridAtmosPipeChunks.TryGetValue(grid, out var chunks))
{
RebuildAtmosPipeGrid(grid, map);
}
else
{
component.AtmosPipeChunks = chunks;
Dirty(uid, component);
}
}
private void InitializeAtmosMonitoringDevice(EntityUid uid, AtmosMonitoringConsoleDeviceComponent component)
{
// Rebuild tile
var xform = Transform(uid);
var gridUid = xform.GridUid;
if (gridUid != null && TryComp<MapGridComponent>(gridUid, out var grid))
RebuildSingleTileOfPipeNetwork(gridUid.Value, grid, xform.Coordinates);
// Update blips on affected consoles
if (component.NavMapBlip == null)
return;
var netEntity = EntityManager.GetNetEntity(uid);
var query = AllEntityQuery<AtmosMonitoringConsoleComponent, TransformComponent>();
while (query.MoveNext(out var ent, out var entConsole, out var entXform))
{
var isDirty = entConsole.AtmosDevices.Remove(netEntity);
if (gridUid != null &&
gridUid == entXform.GridUid &&
xform.Anchored &&
TryGetAtmosDeviceNavMapData(uid, component, xform, gridUid.Value, out var data))
{
entConsole.AtmosDevices.Add(netEntity, data.Value);
isDirty = true;
}
if (isDirty)
Dirty(ent, entConsole);
}
}
private void ShutDownAtmosMonitoringEntity(EntityUid uid, AtmosMonitoringConsoleDeviceComponent component)
{
// Rebuild tile
var xform = Transform(uid);
var gridUid = xform.GridUid;
if (gridUid != null && TryComp<MapGridComponent>(gridUid, out var grid))
RebuildSingleTileOfPipeNetwork(gridUid.Value, grid, xform.Coordinates);
// Update blips on affected consoles
if (component.NavMapBlip == null)
return;
var netEntity = EntityManager.GetNetEntity(uid);
var query = AllEntityQuery<AtmosMonitoringConsoleComponent>();
while (query.MoveNext(out var ent, out var entConsole))
{
if (entConsole.AtmosDevices.Remove(netEntity))
Dirty(ent, entConsole);
}
}
#endregion
private int GetTileIndex(Vector2i relativeTile)
{
return relativeTile.X * ChunkSize + relativeTile.Y;
}
}

View File

@@ -11,6 +11,7 @@ using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Monitor;
using Content.Shared.Atmos.Piping.Components;
using Content.Shared.DeviceNetwork;
using Content.Shared.Power;
using Content.Shared.Tag;

View File

@@ -1,31 +0,0 @@
using Content.Shared.Atmos;
namespace Content.Server.Atmos.Piping.Binary.Components
{
[RegisterComponent]
public sealed partial class GasPressurePumpComponent : Component
{
[ViewVariables(VVAccess.ReadWrite)]
[DataField("enabled")]
public bool Enabled { get; set; } = true;
[ViewVariables(VVAccess.ReadWrite)]
[DataField("inlet")]
public string InletName { get; set; } = "inlet";
[ViewVariables(VVAccess.ReadWrite)]
[DataField("outlet")]
public string OutletName { get; set; } = "outlet";
[ViewVariables(VVAccess.ReadWrite)]
[DataField("targetPressure")]
public float TargetPressure { get; set; } = Atmospherics.OneAtmosphere;
/// <summary>
/// Max pressure of the target gas (NOT relative to source).
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("maxTargetPressure")]
public float MaxTargetPressure = Atmospherics.MaxOutputPressure;
}
}

View File

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

View File

@@ -6,6 +6,7 @@ using Content.Server.NodeContainer.EntitySystems;
using Content.Server.NodeContainer.Nodes;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Piping;
using Content.Shared.Atmos.Piping.Components;
using Content.Shared.Audio;
using Content.Shared.Examine;
using JetBrains.Annotations;

View File

@@ -10,6 +10,7 @@ using Content.Server.NodeContainer.EntitySystems;
using Content.Server.NodeContainer.Nodes;
using Content.Server.Power.Components;
using Content.Shared.Atmos.Piping.Binary.Components;
using Content.Shared.Atmos.Piping.Components;
using Content.Shared.Atmos.Visuals;
using Content.Shared.Audio;
using Content.Shared.Database;

View File

@@ -67,15 +67,3 @@ public readonly struct AtmosDeviceUpdateEvent(float dt, Entity<GridAtmosphereCom
/// </summary>
public readonly Entity<MapAtmosphereComponent?>? Map = map;
}
/// <summary>
/// Raised directed on an atmos device when it is enabled.
/// </summary>
[ByRefEvent]
public record struct AtmosDeviceEnabledEvent;
/// <summary>
/// Raised directed on an atmos device when it is enabled.
/// </summary>
[ByRefEvent]
public record struct AtmosDeviceDisabledEvent;

View File

@@ -1,19 +1,24 @@
using Content.Server.Atmos.Piping.EntitySystems;
using JetBrains.Annotations;
namespace Content.Server.Atmos.Piping.Components
{
[RegisterComponent]
public sealed partial class AtmosPipeColorComponent : Component
{
[DataField("color")]
public Color Color { get; set; } = Color.White;
namespace Content.Server.Atmos.Piping.Components;
[ViewVariables(VVAccess.ReadWrite), UsedImplicitly]
public Color ColorVV
{
get => Color;
set => IoCManager.Resolve<IEntityManager>().System<AtmosPipeColorSystem>().SetColor(Owner, this, value);
}
[RegisterComponent]
public sealed partial class AtmosPipeColorComponent : Component
{
[DataField]
public Color Color { get; set; } = Color.White;
[ViewVariables(VVAccess.ReadWrite), UsedImplicitly]
public Color ColorVV
{
get => Color;
set => IoCManager.Resolve<IEntityManager>().System<AtmosPipeColorSystem>().SetColor(Owner, this, value);
}
}
[ByRefEvent]
public record struct AtmosPipeColorChangedEvent(Color color)
{
public Color Color = color;
}

View File

@@ -1,6 +1,7 @@
using Content.Server.Atmos.Components;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Atmos.Piping.Components;
using Content.Shared.Atmos.Piping.Components;
using JetBrains.Annotations;
using Robust.Shared.Timing;
using Robust.Shared.Utility;

View File

@@ -40,6 +40,9 @@ namespace Content.Server.Atmos.Piping.EntitySystems
return;
_appearance.SetData(uid, PipeColorVisuals.Color, color, appearance);
var ev = new AtmosPipeColorChangedEvent(color);
RaiseLocalEvent(uid, ref ev);
}
}
}

View File

@@ -1,6 +1,7 @@
using Content.Server.Atmos.EntitySystems;
using Content.Server.Atmos.Piping.Components;
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.EntitySystems;
using Content.Server.NodeContainer.Nodes;
using Content.Server.Popups;
using Content.Shared.Atmos;
@@ -8,7 +9,6 @@ using Content.Shared.Construction.Components;
using Content.Shared.Destructible;
using Content.Shared.Popups;
using JetBrains.Annotations;
using Robust.Shared.Player;
namespace Content.Server.Atmos.Piping.EntitySystems
{
@@ -16,11 +16,12 @@ namespace Content.Server.Atmos.Piping.EntitySystems
public sealed class AtmosUnsafeUnanchorSystem : EntitySystem
{
[Dependency] private readonly AtmosphereSystem _atmosphere = default!;
[Dependency] private readonly NodeGroupSystem _group = default!;
[Dependency] private readonly PopupSystem _popup = default!;
public override void Initialize()
{
SubscribeLocalEvent<AtmosUnsafeUnanchorComponent, BeforeUnanchoredEvent>(OnBeforeUnanchored);
SubscribeLocalEvent<AtmosUnsafeUnanchorComponent, UserUnanchoredEvent>(OnUserUnanchored);
SubscribeLocalEvent<AtmosUnsafeUnanchorComponent, UnanchorAttemptEvent>(OnUnanchorAttempt);
SubscribeLocalEvent<AtmosUnsafeUnanchorComponent, BreakageEventArgs>(OnBreak);
}
@@ -48,15 +49,22 @@ namespace Content.Server.Atmos.Piping.EntitySystems
}
}
private void OnBeforeUnanchored(EntityUid uid, AtmosUnsafeUnanchorComponent component, BeforeUnanchoredEvent args)
// When unanchoring a pipe, leak the gas that was inside the pipe element.
// At this point the pipe has been scheduled to be removed from the group, but that won't happen until the next Update() call in NodeGroupSystem,
// so we have to force an update.
// This way the gas inside other connected pipes stays unchanged, while the removed pipe is completely emptied.
private void OnUserUnanchored(EntityUid uid, AtmosUnsafeUnanchorComponent component, UserUnanchoredEvent args)
{
if (component.Enabled)
{
_group.ForceUpdate();
LeakGas(uid);
}
}
private void OnBreak(EntityUid uid, AtmosUnsafeUnanchorComponent component, BreakageEventArgs args)
{
LeakGas(uid);
LeakGas(uid, false);
// Can't use DoActsBehavior["Destruction"] in the same trigger because that would prevent us
// from leaking. So we make up for this by queueing deletion here.
QueueDel(uid);
@@ -64,32 +72,17 @@ namespace Content.Server.Atmos.Piping.EntitySystems
/// <summary>
/// Leak gas from the uid's NodeContainer into the tile atmosphere.
/// Setting removeFromPipe to false will duplicate the gas inside the pipe intead of moving it.
/// This is needed to properly handle the gas in the pipe getting deleted with the pipe.
/// </summary>
public void LeakGas(EntityUid uid)
public void LeakGas(EntityUid uid, bool removeFromPipe = true)
{
if (!EntityManager.TryGetComponent(uid, out NodeContainerComponent? nodes))
return;
if (_atmosphere.GetContainingMixture(uid, true, true) is not {} environment)
if (_atmosphere.GetContainingMixture(uid, true, true) is not { } environment)
environment = GasMixture.SpaceGas;
var lost = 0f;
var timesLost = 0;
foreach (var node in nodes.Nodes.Values)
{
if (node is not PipeNode pipe)
continue;
var difference = pipe.Air.Pressure - environment.Pressure;
lost += Math.Min(
pipe.Volume / pipe.Air.Volume * pipe.Air.TotalMoles,
difference * environment.Volume / (environment.Temperature * Atmospherics.R)
);
timesLost++;
}
var sharedLoss = lost / timesLost;
var buffer = new GasMixture();
foreach (var node in nodes.Nodes.Values)
@@ -97,7 +90,13 @@ namespace Content.Server.Atmos.Piping.EntitySystems
if (node is not PipeNode pipe)
continue;
_atmosphere.Merge(buffer, pipe.Air.Remove(sharedLoss));
if (removeFromPipe)
_atmosphere.Merge(buffer, pipe.Air.RemoveVolume(pipe.Volume));
else
{
var copy = new GasMixture(pipe.Air); //clone, then remove to keep the original untouched
_atmosphere.Merge(buffer, copy.RemoveVolume(pipe.Volume));
}
}
_atmosphere.Merge(environment, buffer);

View File

@@ -7,6 +7,7 @@ using Content.Server.NodeContainer.EntitySystems;
using Content.Server.NodeContainer.Nodes;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Piping;
using Content.Shared.Atmos.Piping.Components;
using Content.Shared.Atmos.Piping.Trinary.Components;
using Content.Shared.Audio;
using Content.Shared.Database;

View File

@@ -7,6 +7,7 @@ using Content.Server.NodeContainer.EntitySystems;
using Content.Server.NodeContainer.Nodes;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Piping;
using Content.Shared.Atmos.Piping.Components;
using Content.Shared.Atmos.Piping.Trinary.Components;
using Content.Shared.Audio;
using Content.Shared.Database;

View File

@@ -5,6 +5,7 @@ using Content.Server.NodeContainer;
using Content.Server.NodeContainer.EntitySystems;
using Content.Server.NodeContainer.Nodes;
using Content.Shared.Atmos.Piping;
using Content.Shared.Atmos.Piping.Components;
using Content.Shared.Audio;
using JetBrains.Annotations;

View File

@@ -44,11 +44,6 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
return;
portableNode.ConnectionsEnabled = args.Anchored;
if (EntityManager.TryGetComponent(uid, out AppearanceComponent? appearance))
{
_appearance.SetData(uid, GasPortableVisuals.ConnectedState, args.Anchored, appearance);
}
}
public bool FindGasPortIn(EntityUid? gridId, EntityCoordinates coordinates, [NotNullWhen(true)] out GasPortComponent? port)

View File

@@ -11,6 +11,7 @@ using Content.Server.NodeContainer.EntitySystems;
using Content.Server.NodeContainer.Nodes;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Monitor;
using Content.Shared.Atmos.Piping.Components;
using Content.Shared.Atmos.Piping.Unary;
using Content.Shared.Atmos.Piping.Unary.Components;
using Content.Shared.Atmos.Visuals;

View File

@@ -13,6 +13,7 @@ using Content.Server.Power.Components;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Piping.Unary.Visuals;
using Content.Shared.Atmos.Monitor;
using Content.Shared.Atmos.Piping.Components;
using Content.Shared.Atmos.Piping.Unary.Components;
using Content.Shared.Audio;
using Content.Shared.DeviceNetwork;

View File

@@ -0,0 +1,17 @@
using Content.Shared.Dataset;
using Robust.Shared.Prototypes;
namespace Content.Server.Chat;
/// <summary>
/// Makes the entity speak when triggered. If the item has UseDelay component, the system will respect that cooldown.
/// </summary>
[RegisterComponent]
public sealed partial class SpeakOnTriggerComponent : Component
{
/// <summary>
/// The identifier for the dataset prototype containing messages to be spoken by this entity.
/// </summary>
[DataField(required: true)]
public ProtoId<LocalizedDatasetPrototype> Pack = string.Empty;
}

View File

@@ -1,18 +0,0 @@
using Content.Shared.Dataset;
using Robust.Shared.Prototypes;
namespace Content.Server.Chat;
/// <summary>
/// Entity will say the things when activated
/// </summary>
[RegisterComponent]
public sealed partial class SpeakOnUseComponent : Component
{
/// <summary>
/// The identifier for the dataset prototype containing messages to be spoken by this entity.
/// </summary>
[DataField(required: true)]
public ProtoId<LocalizedDatasetPrototype> Pack { get; private set; }
}

View File

@@ -0,0 +1,42 @@
using Content.Server.Explosion.EntitySystems;
using Content.Shared.Timing;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Server.Chat.Systems;
public sealed class SpeakOnTriggerSystem : EntitySystem
{
[Dependency] private readonly UseDelaySystem _useDelay = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly ChatSystem _chat = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SpeakOnTriggerComponent, TriggerEvent>(OnTrigger);
}
private void OnTrigger(Entity<SpeakOnTriggerComponent> ent, ref TriggerEvent args)
{
TrySpeak(ent);
args.Handled = true;
}
private void TrySpeak(Entity<SpeakOnTriggerComponent> ent)
{
// If it doesn't have the use delay component, still send the message.
if (TryComp<UseDelayComponent>(ent.Owner, out var useDelay) && _useDelay.IsDelayed((ent.Owner, useDelay)))
return;
if (!_prototypeManager.TryIndex(ent.Comp.Pack, out var messagePack))
return;
var message = Loc.GetString(_random.Pick(messagePack.Values));
_chat.TrySendInGameICMessage(ent.Owner, message, InGameICChatType.Speak, true);
if (useDelay != null)
_useDelay.TryResetDelay((ent.Owner, useDelay));
}
}

View File

@@ -1,42 +0,0 @@
using Content.Server.Chat;
using Content.Shared.Dataset;
using Content.Shared.Interaction.Events;
using Content.Shared.Timing;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Server.Chat.Systems;
/// <summary>
/// Handles the speech on activating an entity
/// </summary>
public sealed partial class SpeakOnUIClosedSystem : EntitySystem
{
[Dependency] private readonly UseDelaySystem _useDelay = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly ChatSystem _chat = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SpeakOnUseComponent, UseInHandEvent>(OnUseInHand);
}
public void OnUseInHand(EntityUid uid, SpeakOnUseComponent? component, UseInHandEvent args)
{
if (!Resolve(uid, ref component))
return;
// Yes it won't work without UseDelayComponent, but we don't want any kind of spam
if (!TryComp(uid, out UseDelayComponent? useDelay) || _useDelay.IsDelayed((uid, useDelay)))
return;
if (!_prototypeManager.TryIndex(component.Pack, out var messagePack))
return;
var message = Loc.GetString(_random.Pick(messagePack.Values));
_chat.TrySendInGameICMessage(uid, message, InGameICChatType.Speak, true);
_useDelay.TryResetDelay((uid, useDelay));
}
}

View File

@@ -14,7 +14,7 @@ public sealed class DeleteChatMessageCommand : ToolshedCommand
[Dependency] private readonly IEntitySystemManager _manager = default!;
[CommandImplementation("id")]
public void DeleteChatMessage([CommandInvocationContext] IInvocationContext ctx, [CommandArgument] uint messageId)
public void DeleteChatMessage(IInvocationContext ctx, uint messageId)
{
if (!_manager.GetEntitySystem<ChatRepositorySystem>().Delete(messageId))
{

View File

@@ -14,7 +14,7 @@ public sealed class NukeChatMessagesCommand : ToolshedCommand
[Dependency] private readonly IEntitySystemManager _manager = default!;
[CommandImplementation("usernames")]
public void Command([CommandInvocationContext] IInvocationContext ctx, [CommandArgument] string usernamesCsv)
public void Command(IInvocationContext ctx, string usernamesCsv)
{
var usernames = usernamesCsv.Split(',');

View File

@@ -4,6 +4,7 @@ using System.Threading.Tasks;
using System.Runtime.InteropServices;
using Content.Corvax.Interfaces.Server;
using Content.Corvax.Interfaces.Shared;
using Content.Server.Administration.Managers;
using Content.Server.Chat.Managers;
using Content.Server.Database;
using Content.Server.GameTicking;
@@ -60,6 +61,7 @@ namespace Content.Server.Connection
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly IChatManager _chatManager = default!;
[Dependency] private readonly IAdminManager _adminManager = default!;
private ISharedSponsorsManager? _sponsorsMgr; // Corvax-Sponsors
private IServerVPNGuardManager? _vpnGuardMgr; // Corvax-VPNGuard
@@ -297,9 +299,16 @@ namespace Content.Server.Connection
ticker.PlayerGameStatuses.TryGetValue(userId, out var status) &&
status == PlayerGameStatus.JoinedGame;
var adminBypass = _cfg.GetCVar(CCVars.AdminBypassMaxPlayers) && adminData != null;
var softPlayerCount = _plyMgr.PlayerCount;
if (!_cfg.GetCVar(CCVars.AdminsCountForMaxPlayers))
{
softPlayerCount -= _adminManager.ActiveAdmins.Count();
}
// Corvax-Queue-Start
var isQueueEnabled = IoCManager.Instance!.TryResolveType<IServerJoinQueueManager>(out var mgr) && mgr.IsEnabled;
if ((_plyMgr.PlayerCount >= _cfg.GetCVar(CCVars.SoftMaxPlayers) && !adminBypass) && !wasInGame && !isQueueEnabled)
if ((softPlayerCount >= _cfg.GetCVar(CCVars.SoftMaxPlayers) && !adminBypass) && !wasInGame && !isQueueEnabled)
// Corvax-Queue-End
{
return (ConnectionDenyReason.Full, Loc.GetString("soft-player-cap-full"), null);
@@ -317,7 +326,7 @@ namespace Content.Server.Connection
foreach (var whitelist in _whitelists)
{
if (!IsValid(whitelist, _plyMgr.PlayerCount))
if (!IsValid(whitelist, softPlayerCount))
{
// Not valid for current player count.
continue;

View File

@@ -1,53 +1,58 @@
using Content.Shared.DeviceLinking;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.DeviceLinking.Components;
[RegisterComponent]
public sealed partial class SignalTimerComponent : Component
{
[DataField, ViewVariables(VVAccess.ReadWrite)]
[DataField]
public double Delay = 5;
/// <summary>
/// This shows the Label: text box in the UI.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
[DataField]
public bool CanEditLabel = true;
/// <summary>
/// The label, used for TextScreen visuals currently.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
[DataField]
public string Label = string.Empty;
/// <summary>
/// Default max width of a label (how many letters can this render?)
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
[DataField]
public int MaxLength = 5;
/// <summary>
/// The port that gets signaled when the timer triggers.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
[DataField]
public ProtoId<SourcePortPrototype> TriggerPort = "Timer";
/// <summary>
/// The port that gets signaled when the timer starts.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
[DataField]
public ProtoId<SourcePortPrototype> StartPort = "Start";
[DataField, ViewVariables(VVAccess.ReadWrite)]
[DataField]
public ProtoId<SinkPortPrototype> Trigger = "Trigger";
/// <summary>
/// If not null, this timer will play this sound when done.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
[DataField]
public SoundSpecifier? DoneSound;
}
/// <summary>
/// The maximum duration in seconds
/// When a larger number is in the input box, the display will start counting down from this one instead
/// </summary>
[DataField]
public Double MaxDuration = 3599; // 59m 59s
}

View File

@@ -44,7 +44,7 @@ public sealed class LogicGateSystem : EntitySystem
}
if (comp.StateB == SignalState.Momentary)
{
comp.StateB = SignalState.High;
comp.StateB = SignalState.Low;
}
// output most likely changed so update it
@@ -102,10 +102,12 @@ public sealed class LogicGateSystem : EntitySystem
if (args.Port == comp.InputPortA)
{
comp.StateA = state;
_appearance.SetData(uid, LogicGateVisuals.InputA, state == SignalState.High); //If A == High => Sets input A sprite to True
}
else if (args.Port == comp.InputPortB)
{
comp.StateB = state;
_appearance.SetData(uid, LogicGateVisuals.InputB, state == SignalState.High); //If B == High => Sets input B sprite to True
}
UpdateOutput(uid, comp);
@@ -143,6 +145,8 @@ public sealed class LogicGateSystem : EntitySystem
break;
}
_appearance.SetData(uid, LogicGateVisuals.Output, output);
// only send a payload if it actually changed
if (output != comp.LastOutput)
{

View File

@@ -152,7 +152,7 @@ public sealed class SignalTimerSystem : EntitySystem
if (!IsMessageValid(uid, args))
return;
component.Delay = args.Delay.TotalSeconds;
component.Delay = Math.Min(args.Delay.TotalSeconds, component.MaxDuration);
_appearanceSystem.SetData(uid, TextScreenVisuals.TargetTime, component.Delay);
}

View File

@@ -18,7 +18,7 @@ using JetBrains.Annotations;
using Robust.Server.Audio;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Player;
using Robust.Shared.Map.Events;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@@ -66,6 +66,42 @@ public sealed class NetworkConfiguratorSystem : SharedNetworkConfiguratorSystem
SubscribeLocalEvent<NetworkConfiguratorComponent, BoundUserInterfaceCheckRangeEvent>(OnUiRangeCheck);
SubscribeLocalEvent<DeviceListComponent, ComponentRemove>(OnComponentRemoved);
SubscribeLocalEvent<BeforeSaveEvent>(OnMapSave);
}
private void OnMapSave(BeforeSaveEvent ev)
{
var enumerator = AllEntityQuery<NetworkConfiguratorComponent>();
while (enumerator.MoveNext(out var uid, out var conf))
{
if (CompOrNull<TransformComponent>(conf.ActiveDeviceList)?.MapUid != ev.Map)
continue;
// The linked device list is (probably) being saved. Make sure that the configurator is also being saved
// (i.e., not in the hands of a mapper/ghost). In the future, map saving should raise a separate event
// containing a set of all entities that are about to be saved, which would make checking this much easier.
// This is a shitty bandaid, and will force close the UI during auto-saves.
// TODO Map serialization refactor
var xform = Transform(uid);
if (xform.MapUid == ev.Map && IsSaveable(uid))
continue;
_uiSystem.CloseUi(uid, NetworkConfiguratorUiKey.Configure);
DebugTools.AssertNull(conf.ActiveDeviceList);
}
bool IsSaveable(EntityUid uid)
{
while (uid.IsValid())
{
if (Prototype(uid)?.MapSavable == false)
return false;
uid = Transform(uid).ParentUid;
}
return true;
}
}
private void OnUiRangeCheck(Entity<NetworkConfiguratorComponent> ent, ref BoundUserInterfaceCheckRangeEvent args)
@@ -485,6 +521,9 @@ public sealed class NetworkConfiguratorSystem : SharedNetworkConfiguratorSystem
if (!TryComp(targetUid, out DeviceListComponent? list))
return;
if (TryComp(configurator.ActiveDeviceList, out DeviceListComponent? oldList))
oldList.Configurators.Remove(configuratorUid);
list.Configurators.Add(configuratorUid);
configurator.ActiveDeviceList = targetUid;
Dirty(configuratorUid, configurator);
@@ -758,7 +797,7 @@ public sealed class NetworkConfiguratorSystem : SharedNetworkConfiguratorSystem
{
if (query.TryGetComponent(device, out var comp))
{
component.Devices[addr] = device;
component.Devices.Add(addr, device);
comp.Configurators.Add(uid);
}
}

View File

@@ -135,12 +135,13 @@ namespace Content.Server.Disposal.Unit.EntitySystems
else
{
_xformSystem.AttachToGridOrMap(entity, xform);
var direction = holder.CurrentDirection == Direction.Invalid ? holder.PreviousDirection : holder.CurrentDirection;
if (holder.PreviousDirection != Direction.Invalid && _xformQuery.TryGetComponent(xform.ParentUid, out var parentXform))
if (direction != Direction.Invalid && _xformQuery.TryGetComponent(gridUid, out var gridXform))
{
var direction = holder.PreviousDirection.ToAngle();
direction += _xformSystem.GetWorldRotation(parentXform);
_throwing.TryThrow(entity, direction.ToWorldVec() * 3f, 10f);
var directionAngle = direction.ToAngle();
directionAngle += _xformSystem.GetWorldRotation(gridXform);
_throwing.TryThrow(entity, directionAngle.ToWorldVec() * 3f, 10f);
}
}
}

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