mirror of
https://github.com/space-syndicate/space-station-14.git
synced 2026-02-14 20:49:34 +01:00
83
Content.Benchmarks/HeatCapacityBenchmark.cs
Normal file
83
Content.Benchmarks/HeatCapacityBenchmark.cs
Normal file
@@ -0,0 +1,83 @@
|
||||
using System.Threading.Tasks;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using Content.IntegrationTests;
|
||||
using Content.IntegrationTests.Pair;
|
||||
using Content.Server.Atmos.EntitySystems;
|
||||
using Content.Shared.Atmos;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Analyzers;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Content.Benchmarks;
|
||||
|
||||
[Virtual]
|
||||
[GcServer(true)]
|
||||
[MemoryDiagnoser]
|
||||
public class HeatCapacityBenchmark
|
||||
{
|
||||
private TestPair _pair = default!;
|
||||
private IEntityManager _sEntMan = default!;
|
||||
private IEntityManager _cEntMan = default!;
|
||||
private Client.Atmos.EntitySystems.AtmosphereSystem _cAtmos = default!;
|
||||
private AtmosphereSystem _sAtmos = default!;
|
||||
private GasMixture _mix;
|
||||
|
||||
[GlobalSetup]
|
||||
public async Task SetupAsync()
|
||||
{
|
||||
ProgramShared.PathOffset = "../../../../";
|
||||
PoolManager.Startup();
|
||||
_pair = await PoolManager.GetServerClient();
|
||||
await _pair.Connect();
|
||||
_cEntMan = _pair.Client.ResolveDependency<IEntityManager>();
|
||||
_sEntMan = _pair.Server.ResolveDependency<IEntityManager>();
|
||||
_cAtmos = _cEntMan.System<Client.Atmos.EntitySystems.AtmosphereSystem>();
|
||||
_sAtmos = _sEntMan.System<AtmosphereSystem>();
|
||||
|
||||
const float volume = 2500f;
|
||||
const float temperature = 293.15f;
|
||||
|
||||
const float o2 = 12.3f;
|
||||
const float n2 = 45.6f;
|
||||
const float co2 = 0.42f;
|
||||
const float plasma = 0.05f;
|
||||
|
||||
_mix = new GasMixture(volume) { Temperature = temperature };
|
||||
|
||||
_mix.AdjustMoles(Gas.Oxygen, o2);
|
||||
_mix.AdjustMoles(Gas.Nitrogen, n2);
|
||||
_mix.AdjustMoles(Gas.CarbonDioxide, co2);
|
||||
_mix.AdjustMoles(Gas.Plasma, plasma);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public async Task ClientHeatCapacityBenchmark()
|
||||
{
|
||||
await _pair.Client.WaitPost(delegate
|
||||
{
|
||||
for (var i = 0; i < 10000; i++)
|
||||
{
|
||||
_cAtmos.GetHeatCapacity(_mix, applyScaling: true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public async Task ServerHeatCapacityBenchmark()
|
||||
{
|
||||
await _pair.Server.WaitPost(delegate
|
||||
{
|
||||
for (var i = 0; i < 10000; i++)
|
||||
{
|
||||
_sAtmos.GetHeatCapacity(_mix, applyScaling: true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[GlobalCleanup]
|
||||
public async Task CleanupAsync()
|
||||
{
|
||||
await _pair.DisposeAsync();
|
||||
PoolManager.Shutdown();
|
||||
}
|
||||
}
|
||||
35
Content.Client/Atmos/EntitySystems/AtmosphereSystem.Gases.cs
Normal file
35
Content.Client/Atmos/EntitySystems/AtmosphereSystem.Gases.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using Content.Shared.Atmos;
|
||||
|
||||
namespace Content.Client.Atmos.EntitySystems;
|
||||
|
||||
public sealed partial class AtmosphereSystem
|
||||
{
|
||||
/*
|
||||
Partial class for operations involving GasMixtures.
|
||||
|
||||
Any method that is overridden here is usually because the server-sided implementation contains
|
||||
code that would escape sandbox. As such these methods are overridden here with a safe
|
||||
implementation.
|
||||
*/
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override float GetHeatCapacityCalculation(float[] moles, bool space)
|
||||
{
|
||||
// Little hack to make space gas mixtures have heat capacity, therefore allowing them to cool down rooms.
|
||||
if (space && MathHelper.CloseTo(NumericsHelpers.HorizontalAdd(moles), 0f))
|
||||
{
|
||||
return Atmospherics.SpaceHeatCapacity;
|
||||
}
|
||||
|
||||
// explicit stackalloc call is banned on client tragically.
|
||||
// the JIT does not stackalloc this during runtime,
|
||||
// though this isnt the hottest code path so it should be fine
|
||||
// the gc can eat a little as a treat
|
||||
var tmp = new float[moles.Length];
|
||||
NumericsHelpers.Multiply(moles, GasSpecificHeats, tmp);
|
||||
// Adjust heat capacity by speedup, because this is primarily what
|
||||
// determines how quickly gases heat up/cool.
|
||||
return MathF.Max(NumericsHelpers.HorizontalAdd(tmp), Atmospherics.MinimumHeatCapacity);
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Client.Atmos.EntitySystems;
|
||||
|
||||
public sealed class AtmosphereSystem : SharedAtmosphereSystem
|
||||
public sealed partial class AtmosphereSystem : SharedAtmosphereSystem
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
using Content.Client.BarSign.Ui;
|
||||
using Content.Shared.BarSign;
|
||||
using Content.Shared.Power;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client.BarSign;
|
||||
|
||||
public sealed class BarSignSystem : VisualizerSystem<BarSignComponent>
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly UserInterfaceSystem _ui = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<BarSignComponent, AfterAutoHandleStateEvent>(OnAfterAutoHandleState);
|
||||
}
|
||||
|
||||
private void OnAfterAutoHandleState(EntityUid uid, BarSignComponent component, ref AfterAutoHandleStateEvent args)
|
||||
{
|
||||
if (_ui.TryGetOpenUi<BarSignBoundUserInterface>(uid, BarSignUiKey.Key, out var bui))
|
||||
bui.Update(component.Current);
|
||||
|
||||
UpdateAppearance(uid, component);
|
||||
}
|
||||
|
||||
protected override void OnAppearanceChange(EntityUid uid, BarSignComponent component, ref AppearanceChangeEvent args)
|
||||
{
|
||||
UpdateAppearance(uid, component, args.Component, args.Sprite);
|
||||
}
|
||||
|
||||
private void UpdateAppearance(EntityUid id, BarSignComponent sign, AppearanceComponent? appearance = null, SpriteComponent? sprite = null)
|
||||
{
|
||||
if (!Resolve(id, ref appearance, ref sprite))
|
||||
return;
|
||||
|
||||
AppearanceSystem.TryGetData<bool>(id, PowerDeviceVisuals.Powered, out var powered, appearance);
|
||||
|
||||
if (powered
|
||||
&& sign.Current != null
|
||||
&& _prototypeManager.Resolve(sign.Current, out var proto))
|
||||
{
|
||||
SpriteSystem.LayerSetSprite((id, sprite), 0, proto.Icon);
|
||||
sprite.LayerSetShader(0, "unshaded");
|
||||
}
|
||||
else
|
||||
{
|
||||
SpriteSystem.LayerSetRsiState((id, sprite), 0, "empty");
|
||||
sprite.LayerSetShader(0, null, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
30
Content.Client/BarSign/BarSignVisualizerSystem.cs
Normal file
30
Content.Client/BarSign/BarSignVisualizerSystem.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using Content.Shared.BarSign;
|
||||
using Content.Shared.Power;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client.BarSign;
|
||||
|
||||
public sealed class BarSignVisualizerSystem : VisualizerSystem<BarSignComponent>
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
|
||||
protected override void OnAppearanceChange(EntityUid uid, BarSignComponent component, ref AppearanceChangeEvent args)
|
||||
{
|
||||
AppearanceSystem.TryGetData<bool>(uid, PowerDeviceVisuals.Powered, out var powered, args.Component);
|
||||
AppearanceSystem.TryGetData<string>(uid, BarSignVisuals.BarSignPrototype, out var currentSign, args.Component);
|
||||
|
||||
if (powered
|
||||
&& currentSign != null
|
||||
&& _prototypeManager.Resolve<BarSignPrototype>(currentSign, out var proto))
|
||||
{
|
||||
SpriteSystem.LayerSetSprite((uid, args.Sprite), 0, proto.Icon);
|
||||
args.Sprite?.LayerSetShader(0, "unshaded");
|
||||
}
|
||||
else
|
||||
{
|
||||
SpriteSystem.LayerSetRsiState((uid, args.Sprite), 0, "empty");
|
||||
args.Sprite?.LayerSetShader(0, null, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,32 +19,27 @@ public sealed class BarSignBoundUserInterface(EntityUid owner, Enum uiKey) : Bou
|
||||
var sign = EntMan.GetComponentOrNull<BarSignComponent>(Owner)?.Current is { } current
|
||||
? _prototype.Index(current)
|
||||
: null;
|
||||
var allSigns = Shared.BarSign.BarSignSystem.GetAllBarSigns(_prototype)
|
||||
var allSigns = BarSignSystem.GetAllBarSigns(_prototype)
|
||||
.OrderBy(p => Loc.GetString(p.Name))
|
||||
.ToList();
|
||||
_menu = new(sign, allSigns);
|
||||
|
||||
_menu.OnSignSelected += id =>
|
||||
{
|
||||
SendMessage(new SetBarSignMessage(id));
|
||||
SendPredictedMessage(new SetBarSignMessage(id));
|
||||
};
|
||||
|
||||
_menu.OnClose += Close;
|
||||
_menu.OpenCentered();
|
||||
}
|
||||
|
||||
public void Update(ProtoId<BarSignPrototype>? sign)
|
||||
public override void Update()
|
||||
{
|
||||
if (_prototype.Resolve(sign, out var signPrototype))
|
||||
_menu?.UpdateState(signPrototype);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
if (!disposing)
|
||||
if (!EntMan.TryGetComponent<BarSignComponent>(Owner, out var signComp))
|
||||
return;
|
||||
_menu?.Dispose();
|
||||
|
||||
if (_prototype.Resolve(signComp.Current, out var signPrototype))
|
||||
_menu?.UpdateState(signPrototype);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,41 +2,31 @@ using Content.Shared.Chemistry;
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.FixedPoint;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.UserInterface;
|
||||
|
||||
namespace Content.Client.Chemistry.UI
|
||||
namespace Content.Client.Chemistry.UI;
|
||||
|
||||
[UsedImplicitly]
|
||||
public sealed class TransferAmountBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey)
|
||||
{
|
||||
[UsedImplicitly]
|
||||
public sealed class TransferAmountBoundUserInterface : BoundUserInterface
|
||||
[ViewVariables]
|
||||
private TransferAmountWindow? _window;
|
||||
|
||||
protected override void Open()
|
||||
{
|
||||
private IEntityManager _entManager;
|
||||
private EntityUid _owner;
|
||||
[ViewVariables]
|
||||
private TransferAmountWindow? _window;
|
||||
base.Open();
|
||||
_window = this.CreateWindow<TransferAmountWindow>();
|
||||
|
||||
public TransferAmountBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
|
||||
if (EntMan.TryGetComponent<SolutionTransferComponent>(Owner, out var comp))
|
||||
_window.SetBounds(comp.MinimumTransferAmount.Int(), comp.MaximumTransferAmount.Int());
|
||||
|
||||
_window.ApplyButton.OnPressed += _ =>
|
||||
{
|
||||
_owner = owner;
|
||||
_entManager = IoCManager.Resolve<IEntityManager>();
|
||||
}
|
||||
|
||||
protected override void Open()
|
||||
{
|
||||
base.Open();
|
||||
_window = this.CreateWindow<TransferAmountWindow>();
|
||||
|
||||
if (_entManager.TryGetComponent<SolutionTransferComponent>(_owner, out var comp))
|
||||
_window.SetBounds(comp.MinimumTransferAmount.Int(), comp.MaximumTransferAmount.Int());
|
||||
|
||||
_window.ApplyButton.OnPressed += _ =>
|
||||
if (int.TryParse(_window.AmountLineEdit.Text, out var i))
|
||||
{
|
||||
if (int.TryParse(_window.AmountLineEdit.Text, out var i))
|
||||
{
|
||||
SendMessage(new TransferAmountSetValueMessage(FixedPoint2.New(i)));
|
||||
_window.Close();
|
||||
}
|
||||
};
|
||||
}
|
||||
SendPredictedMessage(new TransferAmountSetValueMessage(FixedPoint2.New(i)));
|
||||
_window.Close();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,34 +3,33 @@ using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
namespace Content.Client.Chemistry.UI
|
||||
namespace Content.Client.Chemistry.UI;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class TransferAmountWindow : DefaultWindow
|
||||
{
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class TransferAmountWindow : DefaultWindow
|
||||
private int _max = Int32.MaxValue;
|
||||
private int _min = 1;
|
||||
|
||||
public TransferAmountWindow()
|
||||
{
|
||||
private int _max = Int32.MaxValue;
|
||||
private int _min = 1;
|
||||
RobustXamlLoader.Load(this);
|
||||
AmountLineEdit.OnTextChanged += OnValueChanged;
|
||||
}
|
||||
|
||||
public TransferAmountWindow()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
AmountLineEdit.OnTextChanged += OnValueChanged;
|
||||
}
|
||||
public void SetBounds(int min, int max)
|
||||
{
|
||||
_min = min;
|
||||
_max = max;
|
||||
MinimumAmount.Text = Loc.GetString("comp-solution-transfer-set-amount-min", ("amount", _min));
|
||||
MaximumAmount.Text = Loc.GetString("comp-solution-transfer-set-amount-max", ("amount", _max));
|
||||
}
|
||||
|
||||
public void SetBounds(int min, int max)
|
||||
{
|
||||
_min = min;
|
||||
_max = max;
|
||||
MinimumAmount.Text = Loc.GetString("comp-solution-transfer-set-amount-min", ("amount", _min));
|
||||
MaximumAmount.Text = Loc.GetString("comp-solution-transfer-set-amount-max", ("amount", _max));
|
||||
}
|
||||
|
||||
private void OnValueChanged(LineEdit.LineEditEventArgs args)
|
||||
{
|
||||
if (!int.TryParse(AmountLineEdit.Text, out var amount) || amount > _max || amount < _min)
|
||||
ApplyButton.Disabled = true;
|
||||
else
|
||||
ApplyButton.Disabled = false;
|
||||
}
|
||||
private void OnValueChanged(LineEdit.LineEditEventArgs args)
|
||||
{
|
||||
if (!int.TryParse(AmountLineEdit.Text, out var amount) || amount > _max || amount < _min)
|
||||
ApplyButton.Disabled = true;
|
||||
else
|
||||
ApplyButton.Disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,8 @@ using Content.Shared.Weapons.Ranged.Components;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Client.Serialization;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.Graphics;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.CombatMode;
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
using System.Linq;
|
||||
using Content.Client.Materials;
|
||||
using Content.Client.Materials.UI;
|
||||
using Content.Client.Message;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Shared.Construction.Components;
|
||||
using Content.Shared.Containers.ItemSlots;
|
||||
using Content.Shared.Materials;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Timing;
|
||||
@@ -61,57 +59,48 @@ public sealed partial class FlatpackCreatorMenu : FancyWindow
|
||||
!_itemSlots.TryGetSlot(_owner, flatpacker.SlotId, out var itemSlot))
|
||||
return;
|
||||
|
||||
var flatpackerEnt = (_owner, flatpacker);
|
||||
|
||||
if (flatpacker.Packing)
|
||||
{
|
||||
PackButton.Disabled = true;
|
||||
}
|
||||
else if (_currentBoard != null)
|
||||
{
|
||||
Dictionary<string, int> cost;
|
||||
if (_entityManager.TryGetComponent<MachineBoardComponent>(_currentBoard, out var machineBoardComp))
|
||||
cost = _flatpack.GetFlatpackCreationCost((_owner, flatpacker), (_currentBoard.Value, machineBoardComp));
|
||||
else
|
||||
cost = _flatpack.GetFlatpackCreationCost((_owner, flatpacker), null);
|
||||
|
||||
PackButton.Disabled = !_materialStorage.CanChangeMaterialAmount(_owner, cost);
|
||||
PackButton.Disabled = !_flatpack.TryGetFlatpackCreationCost(flatpackerEnt, _currentBoard.Value, out var curCost)
|
||||
|| !_materialStorage.CanChangeMaterialAmount(_owner, curCost);
|
||||
}
|
||||
|
||||
if (_currentBoard == itemSlot.Item)
|
||||
return;
|
||||
|
||||
_currentBoard = itemSlot.Item;
|
||||
CostHeaderLabel.Visible = _currentBoard != null;
|
||||
CostHeaderLabel.Visible = false;
|
||||
InsertLabel.Visible = _currentBoard == null;
|
||||
|
||||
if (_currentBoard is not null)
|
||||
if (_currentBoard is null)
|
||||
{
|
||||
string? prototype = null;
|
||||
Dictionary<string, int>? cost = null;
|
||||
MachineSprite.SetPrototype(NoBoardEffectId);
|
||||
CostLabel.SetMessage(Loc.GetString("flatpacker-ui-no-board-label"));
|
||||
MachineNameLabel.SetMessage(string.Empty);
|
||||
PackButton.Disabled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (_entityManager.TryGetComponent<MachineBoardComponent>(_currentBoard, out var newMachineBoardComp))
|
||||
{
|
||||
prototype = newMachineBoardComp.Prototype;
|
||||
cost = _flatpack.GetFlatpackCreationCost((_owner, flatpacker), (_currentBoard.Value, newMachineBoardComp));
|
||||
}
|
||||
else if (_entityManager.TryGetComponent<ComputerBoardComponent>(_currentBoard, out var computerBoard))
|
||||
{
|
||||
prototype = computerBoard.Prototype;
|
||||
cost = _flatpack.GetFlatpackCreationCost((_owner, flatpacker), null);
|
||||
}
|
||||
|
||||
if (prototype is not null && cost is not null)
|
||||
{
|
||||
var proto = _prototypeManager.Index<EntityPrototype>(prototype);
|
||||
MachineSprite.SetPrototype(prototype);
|
||||
MachineNameLabel.SetMessage(proto.Name);
|
||||
CostLabel.SetMarkup(GetCostString(cost));
|
||||
}
|
||||
if (_flatpack.TryGetFlatpackResultPrototype(_currentBoard.Value, out var prototype) &&
|
||||
_flatpack.TryGetFlatpackCreationCost(flatpackerEnt, _currentBoard.Value, out var cost))
|
||||
{
|
||||
var proto = _prototypeManager.Index<EntityPrototype>(prototype);
|
||||
MachineSprite.SetPrototype(prototype);
|
||||
MachineNameLabel.SetMessage(proto.Name);
|
||||
CostLabel.SetMarkup(GetCostString(cost));
|
||||
CostHeaderLabel.Visible = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
MachineSprite.SetPrototype(NoBoardEffectId);
|
||||
CostLabel.SetMessage(Loc.GetString("flatpacker-ui-no-board-label"));
|
||||
MachineNameLabel.SetMessage(" ");
|
||||
CostLabel.SetMarkup(Loc.GetString("flatpacker-ui-board-invalid-label"));
|
||||
MachineNameLabel.SetMessage(string.Empty);
|
||||
PackButton.Disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
53
Content.Client/HealthAnalyzer/UI/HealthAnalyzerControl.xaml
Normal file
53
Content.Client/HealthAnalyzer/UI/HealthAnalyzerControl.xaml
Normal file
@@ -0,0 +1,53 @@
|
||||
<BoxContainer
|
||||
xmlns="https://spacestation14.io"
|
||||
VerticalExpand="True"
|
||||
Orientation="Vertical">
|
||||
<Label
|
||||
Name="NoPatientDataText"
|
||||
Text="{Loc health-analyzer-window-no-patient-data-text}" />
|
||||
|
||||
<BoxContainer
|
||||
Name="PatientDataContainer"
|
||||
Margin="0 0 0 5"
|
||||
Orientation="Vertical">
|
||||
<BoxContainer Orientation="Horizontal" Margin="0 0 0 5">
|
||||
<SpriteView OverrideDirection="South" Scale="2 2" Name="SpriteView" Access="Public" SetSize="64 64" />
|
||||
<TextureRect Name="NoDataTex" Access="Public" SetSize="64 64" Visible="false" Stretch="KeepAspectCentered" TexturePath="/Textures/Interface/Misc/health_analyzer_out_of_range.png"/>
|
||||
<BoxContainer Margin="5 0 0 0" Orientation="Vertical" VerticalAlignment="Top">
|
||||
<RichTextLabel Name="NameLabel" SetWidth="150" />
|
||||
<Label Name="SpeciesLabel" VerticalAlignment="Top" StyleClasses="LabelSubText" />
|
||||
</BoxContainer>
|
||||
<Label Margin="0 0 5 0" HorizontalExpand="True" HorizontalAlignment="Right" VerticalExpand="True"
|
||||
VerticalAlignment="Top" Name="ScanModeLabel"
|
||||
Text="{Loc 'health-analyzer-window-entity-unknown-text'}" />
|
||||
</BoxContainer>
|
||||
|
||||
<PanelContainer StyleClasses="LowDivider" />
|
||||
|
||||
<GridContainer Margin="0 5 0 0" Columns="2">
|
||||
<Label Text="{Loc 'health-analyzer-window-entity-status-text'}" />
|
||||
<Label Name="StatusLabel" />
|
||||
<Label Text="{Loc 'health-analyzer-window-entity-temperature-text'}" />
|
||||
<Label Name="TemperatureLabel" />
|
||||
<Label Text="{Loc 'health-analyzer-window-entity-blood-level-text'}" />
|
||||
<Label Name="BloodLabel" />
|
||||
<Label Text="{Loc 'health-analyzer-window-entity-damage-total-text'}" />
|
||||
<Label Name="DamageLabel" />
|
||||
</GridContainer>
|
||||
</BoxContainer>
|
||||
|
||||
<PanelContainer Name="AlertsDivider" Visible="False" StyleClasses="LowDivider" />
|
||||
|
||||
<BoxContainer Name="AlertsContainer" Visible="False" Margin="0 5" Orientation="Vertical" HorizontalAlignment="Center">
|
||||
|
||||
</BoxContainer>
|
||||
|
||||
<PanelContainer StyleClasses="LowDivider" />
|
||||
|
||||
<BoxContainer
|
||||
Name="GroupsContainer"
|
||||
Margin="0 5 0 5"
|
||||
Orientation="Vertical">
|
||||
</BoxContainer>
|
||||
|
||||
</BoxContainer>
|
||||
241
Content.Client/HealthAnalyzer/UI/HealthAnalyzerControl.xaml.cs
Normal file
241
Content.Client/HealthAnalyzer/UI/HealthAnalyzerControl.xaml.cs
Normal file
@@ -0,0 +1,241 @@
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Damage.Components;
|
||||
using Content.Shared.Damage.Prototypes;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.Humanoid.Prototypes;
|
||||
using Content.Shared.IdentityManagement;
|
||||
using Content.Shared.MedicalScanner;
|
||||
using Content.Shared.Mobs;
|
||||
using Content.Shared.Mobs.Components;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
namespace Content.Client.HealthAnalyzer.UI;
|
||||
|
||||
// Health analyzer UI is split from its window because it's used by both the
|
||||
// health analyzer item and the cryo pod UI.
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class HealthAnalyzerControl : BoxContainer
|
||||
{
|
||||
private readonly IEntityManager _entityManager;
|
||||
private readonly SpriteSystem _spriteSystem;
|
||||
private readonly IPrototypeManager _prototypes;
|
||||
private readonly IResourceCache _cache;
|
||||
|
||||
public HealthAnalyzerControl()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
var dependencies = IoCManager.Instance!;
|
||||
_entityManager = dependencies.Resolve<IEntityManager>();
|
||||
_spriteSystem = _entityManager.System<SpriteSystem>();
|
||||
_prototypes = dependencies.Resolve<IPrototypeManager>();
|
||||
_cache = dependencies.Resolve<IResourceCache>();
|
||||
}
|
||||
|
||||
public void Populate(HealthAnalyzerUiState state)
|
||||
{
|
||||
var target = _entityManager.GetEntity(state.TargetEntity);
|
||||
|
||||
if (target == null
|
||||
|| !_entityManager.TryGetComponent<DamageableComponent>(target, out var damageable))
|
||||
{
|
||||
NoPatientDataText.Visible = true;
|
||||
return;
|
||||
}
|
||||
|
||||
NoPatientDataText.Visible = false;
|
||||
|
||||
// Scan Mode
|
||||
|
||||
ScanModeLabel.Text = state.ScanMode.HasValue
|
||||
? state.ScanMode.Value
|
||||
? Loc.GetString("health-analyzer-window-scan-mode-active")
|
||||
: Loc.GetString("health-analyzer-window-scan-mode-inactive")
|
||||
: Loc.GetString("health-analyzer-window-entity-unknown-text");
|
||||
|
||||
ScanModeLabel.FontColorOverride = state.ScanMode.HasValue && state.ScanMode.Value ? Color.Green : Color.Red;
|
||||
|
||||
// Patient Information
|
||||
|
||||
SpriteView.SetEntity(target.Value);
|
||||
SpriteView.Visible = state.ScanMode.HasValue && state.ScanMode.Value;
|
||||
NoDataTex.Visible = !SpriteView.Visible;
|
||||
|
||||
var name = new FormattedMessage();
|
||||
name.PushColor(Color.White);
|
||||
name.AddText(_entityManager.HasComponent<MetaDataComponent>(target.Value)
|
||||
? Identity.Name(target.Value, _entityManager)
|
||||
: Loc.GetString("health-analyzer-window-entity-unknown-text"));
|
||||
NameLabel.SetMessage(name);
|
||||
|
||||
SpeciesLabel.Text =
|
||||
_entityManager.TryGetComponent<HumanoidAppearanceComponent>(target.Value,
|
||||
out var humanoidAppearanceComponent)
|
||||
? Loc.GetString(_prototypes.Index<SpeciesPrototype>(humanoidAppearanceComponent.Species).Name)
|
||||
: Loc.GetString("health-analyzer-window-entity-unknown-species-text");
|
||||
|
||||
// Basic Diagnostic
|
||||
|
||||
TemperatureLabel.Text = !float.IsNaN(state.Temperature)
|
||||
? $"{state.Temperature - Atmospherics.T0C:F1} °C ({state.Temperature:F1} K)"
|
||||
: Loc.GetString("health-analyzer-window-entity-unknown-value-text");
|
||||
|
||||
BloodLabel.Text = !float.IsNaN(state.BloodLevel)
|
||||
? $"{state.BloodLevel * 100:F1} %"
|
||||
: Loc.GetString("health-analyzer-window-entity-unknown-value-text");
|
||||
|
||||
StatusLabel.Text =
|
||||
_entityManager.TryGetComponent<MobStateComponent>(target.Value, out var mobStateComponent)
|
||||
? GetStatus(mobStateComponent.CurrentState)
|
||||
: Loc.GetString("health-analyzer-window-entity-unknown-text");
|
||||
|
||||
// Total Damage
|
||||
|
||||
DamageLabel.Text = damageable.TotalDamage.ToString();
|
||||
|
||||
// Alerts
|
||||
|
||||
var showAlerts = state.Unrevivable == true || state.Bleeding == true;
|
||||
|
||||
AlertsDivider.Visible = showAlerts;
|
||||
AlertsContainer.Visible = showAlerts;
|
||||
|
||||
if (showAlerts)
|
||||
AlertsContainer.RemoveAllChildren();
|
||||
|
||||
if (state.Unrevivable == true)
|
||||
AlertsContainer.AddChild(new RichTextLabel
|
||||
{
|
||||
Text = Loc.GetString("health-analyzer-window-entity-unrevivable-text"),
|
||||
Margin = new Thickness(0, 4),
|
||||
MaxWidth = 300
|
||||
});
|
||||
|
||||
if (state.Bleeding == true)
|
||||
AlertsContainer.AddChild(new RichTextLabel
|
||||
{
|
||||
Text = Loc.GetString("health-analyzer-window-entity-bleeding-text"),
|
||||
Margin = new Thickness(0, 4),
|
||||
MaxWidth = 300
|
||||
});
|
||||
|
||||
// Damage Groups
|
||||
|
||||
var damageSortedGroups =
|
||||
damageable.DamagePerGroup.OrderByDescending(damage => damage.Value)
|
||||
.ToDictionary(x => x.Key, x => x.Value);
|
||||
|
||||
IReadOnlyDictionary<string, FixedPoint2> damagePerType = damageable.Damage.DamageDict;
|
||||
|
||||
DrawDiagnosticGroups(damageSortedGroups, damagePerType);
|
||||
}
|
||||
|
||||
private static string GetStatus(MobState mobState)
|
||||
{
|
||||
return mobState switch
|
||||
{
|
||||
MobState.Alive => Loc.GetString("health-analyzer-window-entity-alive-text"),
|
||||
MobState.Critical => Loc.GetString("health-analyzer-window-entity-critical-text"),
|
||||
MobState.Dead => Loc.GetString("health-analyzer-window-entity-dead-text"),
|
||||
_ => Loc.GetString("health-analyzer-window-entity-unknown-text"),
|
||||
};
|
||||
}
|
||||
|
||||
private void DrawDiagnosticGroups(
|
||||
Dictionary<string, FixedPoint2> groups,
|
||||
IReadOnlyDictionary<string, FixedPoint2> damageDict)
|
||||
{
|
||||
GroupsContainer.RemoveAllChildren();
|
||||
|
||||
foreach (var (damageGroupId, damageAmount) in groups)
|
||||
{
|
||||
if (damageAmount == 0)
|
||||
continue;
|
||||
|
||||
var groupTitleText = $"{Loc.GetString(
|
||||
"health-analyzer-window-damage-group-text",
|
||||
("damageGroup", _prototypes.Index<DamageGroupPrototype>(damageGroupId).LocalizedName),
|
||||
("amount", damageAmount)
|
||||
)}";
|
||||
|
||||
var groupContainer = new BoxContainer
|
||||
{
|
||||
Align = AlignMode.Begin,
|
||||
Orientation = LayoutOrientation.Vertical,
|
||||
};
|
||||
|
||||
groupContainer.AddChild(CreateDiagnosticGroupTitle(groupTitleText, damageGroupId));
|
||||
|
||||
GroupsContainer.AddChild(groupContainer);
|
||||
|
||||
// Show the damage for each type in that group.
|
||||
var group = _prototypes.Index<DamageGroupPrototype>(damageGroupId);
|
||||
|
||||
foreach (var type in group.DamageTypes)
|
||||
{
|
||||
if (!damageDict.TryGetValue(type, out var typeAmount) || typeAmount <= 0)
|
||||
continue;
|
||||
|
||||
var damageString = Loc.GetString(
|
||||
"health-analyzer-window-damage-type-text",
|
||||
("damageType", _prototypes.Index<DamageTypePrototype>(type).LocalizedName),
|
||||
("amount", typeAmount)
|
||||
);
|
||||
|
||||
groupContainer.AddChild(CreateDiagnosticItemLabel(damageString.Insert(0, " · ")));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Texture GetTexture(string texture)
|
||||
{
|
||||
var rsiPath = new ResPath("/Textures/Objects/Devices/health_analyzer.rsi");
|
||||
var rsiSprite = new SpriteSpecifier.Rsi(rsiPath, texture);
|
||||
|
||||
var rsi = _cache.GetResource<RSIResource>(rsiSprite.RsiPath).RSI;
|
||||
if (!rsi.TryGetState(rsiSprite.RsiState, out var state))
|
||||
{
|
||||
rsiSprite = new SpriteSpecifier.Rsi(rsiPath, "unknown");
|
||||
}
|
||||
|
||||
return _spriteSystem.Frame0(rsiSprite);
|
||||
}
|
||||
|
||||
private static Label CreateDiagnosticItemLabel(string text)
|
||||
{
|
||||
return new Label
|
||||
{
|
||||
Text = text,
|
||||
};
|
||||
}
|
||||
|
||||
private BoxContainer CreateDiagnosticGroupTitle(string text, string id)
|
||||
{
|
||||
var rootContainer = new BoxContainer
|
||||
{
|
||||
Margin = new Thickness(0, 6, 0, 0),
|
||||
VerticalAlignment = VAlignment.Bottom,
|
||||
Orientation = LayoutOrientation.Horizontal,
|
||||
};
|
||||
|
||||
rootContainer.AddChild(new TextureRect
|
||||
{
|
||||
SetSize = new Vector2(30, 30),
|
||||
Texture = GetTexture(id.ToLower())
|
||||
});
|
||||
|
||||
rootContainer.AddChild(CreateDiagnosticItemLabel(text));
|
||||
|
||||
return rootContainer;
|
||||
}
|
||||
}
|
||||
@@ -1,64 +1,15 @@
|
||||
<controls:FancyWindow
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
xmlns:ui="clr-namespace:Content.Client.HealthAnalyzer.UI"
|
||||
MaxHeight="525"
|
||||
MinWidth="300">
|
||||
<ScrollContainer
|
||||
Margin="5 5 5 5"
|
||||
ReturnMeasure="True"
|
||||
VerticalExpand="True">
|
||||
<BoxContainer
|
||||
Name="RootContainer"
|
||||
VerticalExpand="True"
|
||||
Orientation="Vertical">
|
||||
<Label
|
||||
Name="NoPatientDataText"
|
||||
Text="{Loc health-analyzer-window-no-patient-data-text}" />
|
||||
|
||||
<BoxContainer
|
||||
Name="PatientDataContainer"
|
||||
Margin="0 0 0 5"
|
||||
Orientation="Vertical">
|
||||
<BoxContainer Orientation="Horizontal" Margin="0 0 0 5">
|
||||
<SpriteView OverrideDirection="South" Scale="2 2" Name="SpriteView" Access="Public" SetSize="64 64" />
|
||||
<TextureRect Name="NoDataTex" Access="Public" SetSize="64 64" Visible="false" Stretch="KeepAspectCentered" TexturePath="/Textures/Interface/Misc/health_analyzer_out_of_range.png"/>
|
||||
<BoxContainer Margin="5 0 0 0" Orientation="Vertical" VerticalAlignment="Top">
|
||||
<RichTextLabel Name="NameLabel" SetWidth="150" />
|
||||
<Label Name="SpeciesLabel" VerticalAlignment="Top" StyleClasses="LabelSubText" />
|
||||
</BoxContainer>
|
||||
<Label Margin="0 0 5 0" HorizontalExpand="True" HorizontalAlignment="Right" VerticalExpand="True"
|
||||
VerticalAlignment="Top" Name="ScanModeLabel"
|
||||
Text="{Loc 'health-analyzer-window-entity-unknown-text'}" />
|
||||
</BoxContainer>
|
||||
|
||||
<PanelContainer StyleClasses="LowDivider" />
|
||||
|
||||
<GridContainer Margin="0 5 0 0" Columns="2">
|
||||
<Label Text="{Loc 'health-analyzer-window-entity-status-text'}" />
|
||||
<Label Name="StatusLabel" />
|
||||
<Label Text="{Loc 'health-analyzer-window-entity-temperature-text'}" />
|
||||
<Label Name="TemperatureLabel" />
|
||||
<Label Text="{Loc 'health-analyzer-window-entity-blood-level-text'}" />
|
||||
<Label Name="BloodLabel" />
|
||||
<Label Text="{Loc 'health-analyzer-window-entity-damage-total-text'}" />
|
||||
<Label Name="DamageLabel" />
|
||||
</GridContainer>
|
||||
</BoxContainer>
|
||||
|
||||
<PanelContainer Name="AlertsDivider" Visible="False" StyleClasses="LowDivider" />
|
||||
|
||||
<BoxContainer Name="AlertsContainer" Visible="False" Margin="0 5" Orientation="Vertical" HorizontalAlignment="Center">
|
||||
|
||||
</BoxContainer>
|
||||
|
||||
<PanelContainer StyleClasses="LowDivider" />
|
||||
|
||||
<BoxContainer
|
||||
Name="GroupsContainer"
|
||||
Margin="0 5 0 5"
|
||||
Orientation="Vertical">
|
||||
</BoxContainer>
|
||||
|
||||
</BoxContainer>
|
||||
<ui:HealthAnalyzerControl
|
||||
Name="HealthAnalyzer"/>
|
||||
</ScrollContainer>
|
||||
</controls:FancyWindow>
|
||||
|
||||
@@ -1,241 +1,20 @@
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Shared.Damage.Components;
|
||||
using Content.Shared.Damage.Prototypes;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.Humanoid.Prototypes;
|
||||
using Content.Shared.IdentityManagement;
|
||||
using Content.Shared.MedicalScanner;
|
||||
using Content.Shared.Mobs;
|
||||
using Content.Shared.Mobs.Components;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.HealthAnalyzer.UI
|
||||
namespace Content.Client.HealthAnalyzer.UI;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class HealthAnalyzerWindow : FancyWindow
|
||||
{
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class HealthAnalyzerWindow : FancyWindow
|
||||
public HealthAnalyzerWindow()
|
||||
{
|
||||
private readonly IEntityManager _entityManager;
|
||||
private readonly SpriteSystem _spriteSystem;
|
||||
private readonly IPrototypeManager _prototypes;
|
||||
private readonly IResourceCache _cache;
|
||||
RobustXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
public HealthAnalyzerWindow()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
var dependencies = IoCManager.Instance!;
|
||||
_entityManager = dependencies.Resolve<IEntityManager>();
|
||||
_spriteSystem = _entityManager.System<SpriteSystem>();
|
||||
_prototypes = dependencies.Resolve<IPrototypeManager>();
|
||||
_cache = dependencies.Resolve<IResourceCache>();
|
||||
}
|
||||
|
||||
public void Populate(HealthAnalyzerScannedUserMessage msg)
|
||||
{
|
||||
var target = _entityManager.GetEntity(msg.TargetEntity);
|
||||
|
||||
if (target == null
|
||||
|| !_entityManager.TryGetComponent<DamageableComponent>(target, out var damageable))
|
||||
{
|
||||
NoPatientDataText.Visible = true;
|
||||
return;
|
||||
}
|
||||
|
||||
NoPatientDataText.Visible = false;
|
||||
|
||||
// Scan Mode
|
||||
|
||||
ScanModeLabel.Text = msg.ScanMode.HasValue
|
||||
? msg.ScanMode.Value
|
||||
? Loc.GetString("health-analyzer-window-scan-mode-active")
|
||||
: Loc.GetString("health-analyzer-window-scan-mode-inactive")
|
||||
: Loc.GetString("health-analyzer-window-entity-unknown-text");
|
||||
|
||||
ScanModeLabel.FontColorOverride = msg.ScanMode.HasValue && msg.ScanMode.Value ? Color.Green : Color.Red;
|
||||
|
||||
// Patient Information
|
||||
|
||||
SpriteView.SetEntity(target.Value);
|
||||
SpriteView.Visible = msg.ScanMode.HasValue && msg.ScanMode.Value;
|
||||
NoDataTex.Visible = !SpriteView.Visible;
|
||||
|
||||
var name = new FormattedMessage();
|
||||
name.PushColor(Color.White);
|
||||
name.AddText(_entityManager.HasComponent<MetaDataComponent>(target.Value)
|
||||
? Identity.Name(target.Value, _entityManager)
|
||||
: Loc.GetString("health-analyzer-window-entity-unknown-text"));
|
||||
NameLabel.SetMessage(name);
|
||||
|
||||
SpeciesLabel.Text =
|
||||
_entityManager.TryGetComponent<HumanoidAppearanceComponent>(target.Value,
|
||||
out var humanoidAppearanceComponent)
|
||||
? Loc.GetString(_prototypes.Index<SpeciesPrototype>(humanoidAppearanceComponent.Species).Name)
|
||||
: Loc.GetString("health-analyzer-window-entity-unknown-species-text");
|
||||
|
||||
// Basic Diagnostic
|
||||
|
||||
TemperatureLabel.Text = !float.IsNaN(msg.Temperature)
|
||||
? $"{msg.Temperature - Atmospherics.T0C:F1} °C ({msg.Temperature:F1} K)"
|
||||
: Loc.GetString("health-analyzer-window-entity-unknown-value-text");
|
||||
|
||||
BloodLabel.Text = !float.IsNaN(msg.BloodLevel)
|
||||
? $"{msg.BloodLevel * 100:F1} %"
|
||||
: Loc.GetString("health-analyzer-window-entity-unknown-value-text");
|
||||
|
||||
StatusLabel.Text =
|
||||
_entityManager.TryGetComponent<MobStateComponent>(target.Value, out var mobStateComponent)
|
||||
? GetStatus(mobStateComponent.CurrentState)
|
||||
: Loc.GetString("health-analyzer-window-entity-unknown-text");
|
||||
|
||||
// Total Damage
|
||||
|
||||
DamageLabel.Text = damageable.TotalDamage.ToString();
|
||||
|
||||
// Alerts
|
||||
|
||||
var showAlerts = msg.Unrevivable == true || msg.Bleeding == true;
|
||||
|
||||
AlertsDivider.Visible = showAlerts;
|
||||
AlertsContainer.Visible = showAlerts;
|
||||
|
||||
if (showAlerts)
|
||||
AlertsContainer.RemoveAllChildren();
|
||||
|
||||
if (msg.Unrevivable == true)
|
||||
AlertsContainer.AddChild(new RichTextLabel
|
||||
{
|
||||
Text = Loc.GetString("health-analyzer-window-entity-unrevivable-text"),
|
||||
Margin = new Thickness(0, 4),
|
||||
MaxWidth = 300
|
||||
});
|
||||
|
||||
if (msg.Bleeding == true)
|
||||
AlertsContainer.AddChild(new RichTextLabel
|
||||
{
|
||||
Text = Loc.GetString("health-analyzer-window-entity-bleeding-text"),
|
||||
Margin = new Thickness(0, 4),
|
||||
MaxWidth = 300
|
||||
});
|
||||
|
||||
// Damage Groups
|
||||
|
||||
var damageSortedGroups =
|
||||
damageable.DamagePerGroup.OrderByDescending(damage => damage.Value)
|
||||
.ToDictionary(x => x.Key, x => x.Value);
|
||||
|
||||
IReadOnlyDictionary<string, FixedPoint2> damagePerType = damageable.Damage.DamageDict;
|
||||
|
||||
DrawDiagnosticGroups(damageSortedGroups, damagePerType);
|
||||
}
|
||||
|
||||
private static string GetStatus(MobState mobState)
|
||||
{
|
||||
return mobState switch
|
||||
{
|
||||
MobState.Alive => Loc.GetString("health-analyzer-window-entity-alive-text"),
|
||||
MobState.Critical => Loc.GetString("health-analyzer-window-entity-critical-text"),
|
||||
MobState.Dead => Loc.GetString("health-analyzer-window-entity-dead-text"),
|
||||
_ => Loc.GetString("health-analyzer-window-entity-unknown-text"),
|
||||
};
|
||||
}
|
||||
|
||||
private void DrawDiagnosticGroups(
|
||||
Dictionary<string, FixedPoint2> groups,
|
||||
IReadOnlyDictionary<string, FixedPoint2> damageDict)
|
||||
{
|
||||
GroupsContainer.RemoveAllChildren();
|
||||
|
||||
foreach (var (damageGroupId, damageAmount) in groups)
|
||||
{
|
||||
if (damageAmount == 0)
|
||||
continue;
|
||||
|
||||
var groupTitleText = $"{Loc.GetString(
|
||||
"health-analyzer-window-damage-group-text",
|
||||
("damageGroup", _prototypes.Index<DamageGroupPrototype>(damageGroupId).LocalizedName),
|
||||
("amount", damageAmount)
|
||||
)}";
|
||||
|
||||
var groupContainer = new BoxContainer
|
||||
{
|
||||
Align = BoxContainer.AlignMode.Begin,
|
||||
Orientation = BoxContainer.LayoutOrientation.Vertical,
|
||||
};
|
||||
|
||||
groupContainer.AddChild(CreateDiagnosticGroupTitle(groupTitleText, damageGroupId));
|
||||
|
||||
GroupsContainer.AddChild(groupContainer);
|
||||
|
||||
// Show the damage for each type in that group.
|
||||
var group = _prototypes.Index<DamageGroupPrototype>(damageGroupId);
|
||||
|
||||
foreach (var type in group.DamageTypes)
|
||||
{
|
||||
if (!damageDict.TryGetValue(type, out var typeAmount) || typeAmount <= 0)
|
||||
continue;
|
||||
|
||||
var damageString = Loc.GetString(
|
||||
"health-analyzer-window-damage-type-text",
|
||||
("damageType", _prototypes.Index<DamageTypePrototype>(type).LocalizedName),
|
||||
("amount", typeAmount)
|
||||
);
|
||||
|
||||
groupContainer.AddChild(CreateDiagnosticItemLabel(damageString.Insert(0, " · ")));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Texture GetTexture(string texture)
|
||||
{
|
||||
var rsiPath = new ResPath("/Textures/Objects/Devices/health_analyzer.rsi");
|
||||
var rsiSprite = new SpriteSpecifier.Rsi(rsiPath, texture);
|
||||
|
||||
var rsi = _cache.GetResource<RSIResource>(rsiSprite.RsiPath).RSI;
|
||||
if (!rsi.TryGetState(rsiSprite.RsiState, out var state))
|
||||
{
|
||||
rsiSprite = new SpriteSpecifier.Rsi(rsiPath, "unknown");
|
||||
}
|
||||
|
||||
return _spriteSystem.Frame0(rsiSprite);
|
||||
}
|
||||
|
||||
private static Label CreateDiagnosticItemLabel(string text)
|
||||
{
|
||||
return new Label
|
||||
{
|
||||
Text = text,
|
||||
};
|
||||
}
|
||||
|
||||
private BoxContainer CreateDiagnosticGroupTitle(string text, string id)
|
||||
{
|
||||
var rootContainer = new BoxContainer
|
||||
{
|
||||
Margin = new Thickness(0, 6, 0, 0),
|
||||
VerticalAlignment = VAlignment.Bottom,
|
||||
Orientation = BoxContainer.LayoutOrientation.Horizontal,
|
||||
};
|
||||
|
||||
rootContainer.AddChild(new TextureRect
|
||||
{
|
||||
SetSize = new Vector2(30, 30),
|
||||
Texture = GetTexture(id.ToLower())
|
||||
});
|
||||
|
||||
rootContainer.AddChild(CreateDiagnosticItemLabel(text));
|
||||
|
||||
return rootContainer;
|
||||
}
|
||||
public void Populate(HealthAnalyzerScannedUserMessage msg)
|
||||
{
|
||||
HealthAnalyzer.Populate(msg.State);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,4 +5,3 @@ namespace Content.Client.Kitchen.EntitySystems;
|
||||
|
||||
[UsedImplicitly]
|
||||
public sealed class ReagentGrinderSystem : SharedReagentGrinderSystem;
|
||||
|
||||
|
||||
285
Content.Client/Medical/Cryogenics/BeakerBarChart.cs
Normal file
285
Content.Client/Medical/Cryogenics/BeakerBarChart.cs
Normal file
@@ -0,0 +1,285 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Numerics;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
// ReSharper disable CompareOfFloatsByEqualityOperator
|
||||
|
||||
namespace Content.Client.Medical.Cryogenics;
|
||||
|
||||
|
||||
public sealed class BeakerBarChart : Control
|
||||
{
|
||||
private sealed class Entry
|
||||
{
|
||||
public float WidthFraction; // This entry's width as a fraction of the chart's total width (between 0 and 1)
|
||||
public float TargetAmount;
|
||||
public string Uid; // This UID is used to track entries between frames, for animation.
|
||||
public string? Tooltip;
|
||||
public Color Color;
|
||||
public Label Label;
|
||||
|
||||
public Entry(string uid, Label label)
|
||||
{
|
||||
Uid = uid;
|
||||
Label = label;
|
||||
}
|
||||
}
|
||||
|
||||
public float Capacity = 50;
|
||||
|
||||
public Color NotchColor = new(1, 1, 1, 0.25f);
|
||||
public Color BackgroundColor = new(0.1f, 0.1f, 0.1f);
|
||||
|
||||
public int MediumNotchInterval = 5;
|
||||
public int BigNotchInterval = 10;
|
||||
|
||||
// When we have a very large beaker (i.e. bluespace beaker) we might need to increase the distance between notches.
|
||||
// The distance between notches is increased by ScaleMultiplier when the distance between notches is less than
|
||||
// MinSmallNotchScreenDistance in UI units.
|
||||
public int MinSmallNotchScreenDistance = 2;
|
||||
public int ScaleMultiplier = 10;
|
||||
|
||||
public float SmallNotchHeight = 0.1f;
|
||||
public float MediumNotchHeight = 0.25f;
|
||||
public float BigNotchHeight = 1f;
|
||||
|
||||
// We don't animate new entries until this control has been drawn at least once.
|
||||
private bool _hasBeenDrawn = false;
|
||||
|
||||
// This is used to keep the segments of the chart in the same order as the SetEntry calls.
|
||||
// For example: In update 1 we might get cryox, alox, bic (in that order), and in update 2 we get alox, cryox, bic.
|
||||
// To keep the order of the entries the same as the order of the SetEntry calls, we let the old cryox entry
|
||||
// disappear and create a new cryox entry behind the alox entry.
|
||||
private int _nextUpdateableEntry = 0;
|
||||
|
||||
private readonly List<Entry> _entries = new();
|
||||
|
||||
|
||||
public BeakerBarChart()
|
||||
{
|
||||
MouseFilter = MouseFilterMode.Pass;
|
||||
TooltipSupplier = SupplyTooltip;
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
foreach (var entry in _entries)
|
||||
{
|
||||
entry.TargetAmount = 0;
|
||||
}
|
||||
|
||||
_nextUpdateableEntry = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Either adds a new entry to the chart if the UID doesn't appear yet, or updates the amount of an existing entry.
|
||||
/// </summary>
|
||||
public void SetEntry(
|
||||
string uid,
|
||||
string label,
|
||||
float amount,
|
||||
Color color,
|
||||
Color? textColor = null,
|
||||
string? tooltip = null)
|
||||
{
|
||||
// If we can find an old entry we're allowed to update, update that one.
|
||||
if (TryFindUpdateableEntry(uid, out var index))
|
||||
{
|
||||
_entries[index].TargetAmount = amount;
|
||||
_entries[index].Tooltip = tooltip;
|
||||
_entries[index].Label.Text = label;
|
||||
_nextUpdateableEntry = index + 1;
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise create a new entry.
|
||||
if (amount <= 0)
|
||||
return;
|
||||
|
||||
// If no text color is provided, use either white or black depending on how dark the background is.
|
||||
textColor ??= (color.R + color.G + color.B < 1.5f ? Color.White : Color.Black);
|
||||
|
||||
var childLabel = new Label
|
||||
{
|
||||
Text = label,
|
||||
ClipText = true,
|
||||
FontColorOverride = textColor,
|
||||
Margin = new Thickness(4, 0, 0, 0)
|
||||
};
|
||||
AddChild(childLabel);
|
||||
|
||||
_entries.Insert(
|
||||
_nextUpdateableEntry,
|
||||
new Entry(uid, childLabel)
|
||||
{
|
||||
WidthFraction = (_hasBeenDrawn ? 0 : amount / Capacity),
|
||||
TargetAmount = amount,
|
||||
Tooltip = tooltip,
|
||||
Color = color
|
||||
}
|
||||
);
|
||||
|
||||
_nextUpdateableEntry += 1;
|
||||
}
|
||||
|
||||
private bool TryFindUpdateableEntry(string uid, out int index)
|
||||
{
|
||||
for (int i = _nextUpdateableEntry; i < _entries.Count; i++)
|
||||
{
|
||||
if (_entries[i].Uid == uid)
|
||||
{
|
||||
index = i;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
index = -1;
|
||||
return false;
|
||||
}
|
||||
|
||||
private IEnumerable<(Entry, float xMin, float xMax)> EntryRanges(float? pixelWidth = null)
|
||||
{
|
||||
float chartWidth = pixelWidth ?? PixelWidth;
|
||||
var xStart = 0f;
|
||||
|
||||
foreach (var entry in _entries)
|
||||
{
|
||||
var entryWidth = entry.WidthFraction * chartWidth;
|
||||
var xEnd = MathF.Min(xStart + entryWidth, chartWidth);
|
||||
|
||||
yield return (entry, xStart, xEnd);
|
||||
|
||||
xStart = xEnd;
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryFindEntry(float x, [NotNullWhen(true)] out Entry? entry)
|
||||
{
|
||||
foreach (var (currentEntry, xMin, xMax) in EntryRanges())
|
||||
{
|
||||
if (xMin <= x && x < xMax)
|
||||
{
|
||||
entry = currentEntry;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
entry = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
// Tween the amounts to their target amounts.
|
||||
const float tweenInverseHalfLife = 8; // Half life of tween is 1/n
|
||||
var hasChanged = false;
|
||||
|
||||
foreach (var entry in _entries)
|
||||
{
|
||||
var targetWidthFraction = entry.TargetAmount / Capacity;
|
||||
|
||||
if (entry.WidthFraction == targetWidthFraction)
|
||||
continue;
|
||||
|
||||
// Tween with lerp abuse interpolation
|
||||
entry.WidthFraction = MathHelper.Lerp(
|
||||
entry.WidthFraction,
|
||||
targetWidthFraction,
|
||||
MathHelper.Clamp01(tweenInverseHalfLife * args.DeltaSeconds)
|
||||
);
|
||||
hasChanged = true;
|
||||
|
||||
if (MathF.Abs(entry.WidthFraction - targetWidthFraction) < 0.0001f)
|
||||
entry.WidthFraction = targetWidthFraction;
|
||||
}
|
||||
|
||||
if (!hasChanged)
|
||||
return;
|
||||
|
||||
InvalidateArrange();
|
||||
|
||||
// Remove old entries whose animations have finished.
|
||||
foreach (var entry in _entries)
|
||||
{
|
||||
if (entry.WidthFraction == 0 && entry.TargetAmount == 0)
|
||||
RemoveChild(entry.Label);
|
||||
}
|
||||
|
||||
_entries.RemoveAll(entry => entry.WidthFraction == 0 && entry.TargetAmount == 0);
|
||||
}
|
||||
|
||||
protected override void MouseMove(GUIMouseMoveEventArgs args)
|
||||
{
|
||||
HideTooltip();
|
||||
}
|
||||
|
||||
protected override void Draw(DrawingHandleScreen handle)
|
||||
{
|
||||
handle.DrawRect(PixelSizeBox, BackgroundColor);
|
||||
|
||||
// Draw the entry backgrounds
|
||||
foreach (var (entry, xMin, xMax) in EntryRanges())
|
||||
{
|
||||
if (xMin != xMax)
|
||||
handle.DrawRect(new(xMin, 0, xMax, PixelHeight), entry.Color);
|
||||
}
|
||||
|
||||
// Draw notches
|
||||
var unitWidth = PixelWidth / Capacity;
|
||||
var unitsPerNotch = 1;
|
||||
|
||||
while (unitWidth < MinSmallNotchScreenDistance)
|
||||
{
|
||||
// This is here for 1000u bluespace beakers. If the distance between small notches is so small that it would
|
||||
// be very ugly, we reduce the amount of notches by ScaleMultiplier (currently a factor of 10).
|
||||
// (I could use an analytical algorithm here, but it would be more difficult to read with pretty much no
|
||||
// performance benefit, since it loops zero times normally and one time for the bluespace beaker)
|
||||
unitWidth *= ScaleMultiplier;
|
||||
unitsPerNotch *= ScaleMultiplier;
|
||||
}
|
||||
|
||||
for (int i = 0; i <= Capacity / unitsPerNotch; i++)
|
||||
{
|
||||
var x = i * unitWidth;
|
||||
var height = (i % BigNotchInterval == 0 ? BigNotchHeight :
|
||||
i % MediumNotchInterval == 0 ? MediumNotchHeight :
|
||||
SmallNotchHeight) * PixelHeight;
|
||||
var start = new Vector2(x, PixelHeight);
|
||||
var end = new Vector2(x, PixelHeight - height);
|
||||
handle.DrawLine(start, end, NotchColor);
|
||||
}
|
||||
|
||||
_hasBeenDrawn = true;
|
||||
}
|
||||
|
||||
protected override Vector2 ArrangeOverride(Vector2 finalSize)
|
||||
{
|
||||
foreach (var (entry, xMin, xMax) in EntryRanges(finalSize.X))
|
||||
{
|
||||
entry.Label.Arrange(new((int)xMin, 0, (int)xMax, (int)finalSize.Y));
|
||||
}
|
||||
|
||||
return finalSize;
|
||||
}
|
||||
|
||||
private Control? SupplyTooltip(Control sender)
|
||||
{
|
||||
var globalMousePos = UserInterfaceManager.MousePositionScaled.Position;
|
||||
var mousePos = globalMousePos - GlobalPosition;
|
||||
|
||||
if (!TryFindEntry(mousePos.X, out var entry) || entry.Tooltip == null)
|
||||
return null;
|
||||
|
||||
var msg = new FormattedMessage();
|
||||
msg.AddText(entry.Tooltip);
|
||||
|
||||
var tooltip = new Tooltip();
|
||||
tooltip.SetMessage(msg);
|
||||
return tooltip;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Medical.Cryogenics;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.UserInterface;
|
||||
namespace Content.Client.Medical.Cryogenics;
|
||||
|
||||
[UsedImplicitly]
|
||||
public sealed class CryoPodBoundUserInterface : BoundUserInterface
|
||||
{
|
||||
private CryoPodWindow? _window;
|
||||
|
||||
public CryoPodBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void Open()
|
||||
{
|
||||
base.Open();
|
||||
_window = this.CreateWindowCenteredLeft<CryoPodWindow>();
|
||||
_window.Title = EntMan.GetComponent<MetaDataComponent>(Owner).EntityName;
|
||||
_window.OnEjectPatientPressed += EjectPatientPressed;
|
||||
_window.OnEjectBeakerPressed += EjectBeakerPressed;
|
||||
_window.OnInjectPressed += InjectPressed;
|
||||
}
|
||||
|
||||
private void EjectPatientPressed()
|
||||
{
|
||||
var isLocked =
|
||||
EntMan.TryGetComponent<CryoPodComponent>(Owner, out var cryoComp)
|
||||
&& cryoComp.Locked;
|
||||
|
||||
_window?.SetEjectErrorVisible(isLocked);
|
||||
SendMessage(new CryoPodSimpleUiMessage(CryoPodSimpleUiMessage.MessageType.EjectPatient));
|
||||
}
|
||||
|
||||
private void EjectBeakerPressed()
|
||||
{
|
||||
SendMessage(new CryoPodSimpleUiMessage(CryoPodSimpleUiMessage.MessageType.EjectBeaker));
|
||||
}
|
||||
|
||||
private void InjectPressed(FixedPoint2 transferAmount)
|
||||
{
|
||||
SendMessage(new CryoPodInjectUiMessage(transferAmount));
|
||||
}
|
||||
|
||||
protected override void ReceiveMessage(BoundUserInterfaceMessage message)
|
||||
{
|
||||
if (_window != null && message is CryoPodUserMessage cryoMsg)
|
||||
{
|
||||
_window.Populate(cryoMsg);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,6 @@ namespace Content.Client.Medical.Cryogenics;
|
||||
|
||||
public sealed class CryoPodSystem : SharedCryoPodSystem
|
||||
{
|
||||
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
|
||||
[Dependency] private readonly SpriteSystem _sprite = default!;
|
||||
|
||||
public override void Initialize()
|
||||
@@ -46,8 +45,8 @@ public sealed class CryoPodSystem : SharedCryoPodSystem
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_appearance.TryGetData<bool>(uid, CryoPodVisuals.ContainsEntity, out var isOpen, args.Component)
|
||||
|| !_appearance.TryGetData<bool>(uid, CryoPodVisuals.IsOn, out var isOn, args.Component))
|
||||
if (!Appearance.TryGetData<bool>(uid, CryoPodVisuals.ContainsEntity, out var isOpen, args.Component)
|
||||
|| !Appearance.TryGetData<bool>(uid, CryoPodVisuals.IsOn, out var isOn, args.Component))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -64,6 +63,11 @@ public sealed class CryoPodSystem : SharedCryoPodSystem
|
||||
_sprite.LayerSetVisible((uid, args.Sprite), CryoPodVisualLayers.Cover, true);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void UpdateUi(Entity<CryoPodComponent> cryoPod)
|
||||
{
|
||||
// Atmos and health scanner aren't predicted currently...
|
||||
}
|
||||
}
|
||||
|
||||
public enum CryoPodVisualLayers : byte
|
||||
|
||||
232
Content.Client/Medical/Cryogenics/CryoPodWindow.xaml
Normal file
232
Content.Client/Medical/Cryogenics/CryoPodWindow.xaml
Normal file
@@ -0,0 +1,232 @@
|
||||
<controls:FancyWindow xmlns="https://spacestation14.io"
|
||||
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
|
||||
xmlns:health="clr-namespace:Content.Client.HealthAnalyzer.UI"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
xmlns:cryogenics="clr-namespace:Content.Client.Medical.Cryogenics"
|
||||
MinSize="250 300"
|
||||
Resizable="False">
|
||||
|
||||
<Label Name="LoadingPlaceHolder"
|
||||
Text="{Loc 'cryo-pod-window-loading'}"
|
||||
Align="Center"
|
||||
HorizontalExpand="True"
|
||||
VerticalExpand="True"/>
|
||||
|
||||
<BoxContainer Name="Sections"
|
||||
Orientation="Horizontal"
|
||||
Visible="False"
|
||||
Margin="10"
|
||||
SeparationOverride="16">
|
||||
<BoxContainer Name="CryoSection"
|
||||
VerticalExpand="True"
|
||||
Orientation="Vertical"
|
||||
MinWidth="250"
|
||||
MaxWidth="250">
|
||||
|
||||
<!-- Flavor text -->
|
||||
<BoxContainer Orientation="Horizontal"
|
||||
SeparationOverride="10"
|
||||
Margin="8 0 0 8">
|
||||
<TextureRect StyleClasses="NTLogoDark"
|
||||
VerticalExpand="True"
|
||||
Stretch="KeepAspectCentered"
|
||||
SetSize="32 32"/>
|
||||
<BoxContainer Orientation="Vertical"
|
||||
SeparationOverride="-4">
|
||||
<Label Text="{Loc 'cryo-pod-window-product-name'}"
|
||||
StyleClasses="FontLarge"/>
|
||||
<Label Text="{Loc 'cryo-pod-window-product-subtitle'}"
|
||||
StyleClasses="LabelSubText"/>
|
||||
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Atmos info -->
|
||||
<BoxContainer Orientation="Horizontal"
|
||||
SeparationOverride="20"
|
||||
Margin="0 0 0 4">
|
||||
<!-- Pressure -->
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<Label Text="{Loc 'gas-analyzer-window-pressure-text'}"
|
||||
StyleClasses="LabelSubText"/>
|
||||
<Label Name="Pressure"/>
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Temperature -->
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<Label Text="{Loc 'gas-analyzer-window-temperature-text'}"
|
||||
StyleClasses="LabelSubText"/>
|
||||
<Label Name="Temperature"/>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Gas mix -->
|
||||
<Control Margin="0 0 0 22">
|
||||
<controls:SplitBar Name="GasMixChart"
|
||||
MinHeight="8"
|
||||
MaxHeight="8"/>
|
||||
</Control>
|
||||
|
||||
<!-- Warnings & status -->
|
||||
<BoxContainer Orientation="Vertical"
|
||||
HorizontalExpand="True"
|
||||
VerticalExpand="True"
|
||||
Align="Center"
|
||||
Margin="0 0 0 14"
|
||||
SeparationOverride="20">
|
||||
|
||||
<!-- Ejection error (if the pod is locked) -->
|
||||
<PanelContainer Name="EjectError"
|
||||
Visible="False"
|
||||
HorizontalExpand="True">
|
||||
<PanelContainer.PanelOverride>
|
||||
<gfx:StyleBoxFlat BorderThickness="1" BorderColor="orange"/>
|
||||
</PanelContainer.PanelOverride>
|
||||
|
||||
<BoxContainer Orientation="Vertical"
|
||||
Margin="6">
|
||||
<Label Text="{Loc 'cryo-pod-window-error-header'}"
|
||||
FontColorOverride="orange"
|
||||
Align="Center"/>
|
||||
<RichTextLabel Text="{Loc 'cryo-pod-window-eject-error'}"/>
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
|
||||
<!-- Pressure warning -->
|
||||
<PanelContainer Name="LowPressureWarning"
|
||||
Visible="False"
|
||||
HorizontalExpand="True">
|
||||
<PanelContainer.PanelOverride>
|
||||
<gfx:StyleBoxFlat BorderThickness="1" BorderColor="orange"/>
|
||||
</PanelContainer.PanelOverride>
|
||||
|
||||
<BoxContainer Orientation="Vertical"
|
||||
Margin="6">
|
||||
<Label Text="{Loc 'cryo-pod-window-warning-header'}"
|
||||
FontColorOverride="orange"
|
||||
Align="Center"/>
|
||||
<RichTextLabel Text="{Loc 'cryo-pod-window-low-pressure-warning'}"/>
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
|
||||
<!-- Temperature warning -->
|
||||
<PanelContainer Name="HighTemperatureWarning"
|
||||
Visible="False"
|
||||
HorizontalExpand="True">
|
||||
<PanelContainer.PanelOverride>
|
||||
<gfx:StyleBoxFlat BorderThickness="1" BorderColor="orange"/>
|
||||
</PanelContainer.PanelOverride>
|
||||
|
||||
<BoxContainer Orientation="Vertical"
|
||||
Margin="6">
|
||||
<Label Text="{Loc 'cryo-pod-window-warning-header'}"
|
||||
FontColorOverride="orange"
|
||||
Align="Center"/>
|
||||
<!-- Note: This placeholder text should never be visible. -->
|
||||
<RichTextLabel Name="HighTemperatureWarningText"
|
||||
Text="Temperature too high."/>
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
|
||||
<!-- Status checklist -->
|
||||
<BoxContainer Orientation="Vertical">
|
||||
|
||||
<BoxContainer Orientation="Horizontal"
|
||||
SeparationOverride="8">
|
||||
<Label Text="{Loc 'cryo-pod-window-status'}"/>
|
||||
<Label Name="StatusLabel"
|
||||
Text="{Loc 'cryo-pod-window-status-not-ready'}"
|
||||
FontColorOverride="Orange"/>
|
||||
</BoxContainer>
|
||||
|
||||
<GridContainer Columns="2"
|
||||
HSeparationOverride="0"
|
||||
VSeparationOverride="6"
|
||||
Margin="6 3 0 0">
|
||||
<Label Text="⋄"
|
||||
StyleClasses="LabelSubText"/>
|
||||
<Label Name="PressureCheck"
|
||||
Text="{Loc 'cryo-pod-window-checklist-pressure'}"
|
||||
StyleClasses="LabelSubText"/>
|
||||
<Label Text="⋄"
|
||||
StyleClasses="LabelSubText"/>
|
||||
<Label Name="ChemicalsCheck"
|
||||
Text="{Loc 'cryo-pod-window-checklist-chemicals'}"
|
||||
StyleClasses="LabelSubText"
|
||||
FontColorOverride="Orange"/>
|
||||
<Label Text="⋄"
|
||||
StyleClasses="LabelSubText"/>
|
||||
<Label Name="TemperatureCheck"
|
||||
Text="{Loc 'cryo-pod-window-checklist-temperature'}"
|
||||
StyleClasses="LabelSubText"/>
|
||||
</GridContainer>
|
||||
|
||||
</BoxContainer>
|
||||
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Reagents -->
|
||||
<Control HorizontalExpand="True"
|
||||
MinHeight="30">
|
||||
<Label Name="NoBeakerText"
|
||||
Text="{Loc 'cryo-pod-window-chems-no-beaker'}"
|
||||
FontColorOverride="Gray"
|
||||
VerticalExpand="True"
|
||||
VAlign="Center"/>
|
||||
<cryogenics:BeakerBarChart Name="ChemicalsChart"
|
||||
HorizontalExpand="True"
|
||||
VerticalExpand="True"/>
|
||||
</Control>
|
||||
|
||||
<!-- Buttons -->
|
||||
<BoxContainer Orientation="Vertical"
|
||||
Margin="-2 2 -2 0">
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Button Name="Inject1"
|
||||
Text="{Loc 'cryo-pod-window-inject-1u'}"
|
||||
Disabled="True"
|
||||
HorizontalExpand="True"
|
||||
StyleClasses="OpenBoth"/>
|
||||
<Button Name="Inject5"
|
||||
Text="{Loc 'cryo-pod-window-inject-5u'}"
|
||||
Disabled="True"
|
||||
HorizontalExpand="True"
|
||||
StyleClasses="OpenBoth"/>
|
||||
<Button Name="Inject10"
|
||||
Text="{Loc 'cryo-pod-window-inject-10u'}"
|
||||
Disabled="True"
|
||||
HorizontalExpand="True"
|
||||
StyleClasses="OpenBoth"/>
|
||||
<Button Name="Inject20"
|
||||
Text="{Loc 'cryo-pod-window-inject-20u'}"
|
||||
Disabled="True"
|
||||
HorizontalExpand="True"
|
||||
StyleClasses="OpenBoth"/>
|
||||
<Button Name="EjectBeakerButton"
|
||||
Text="{Loc 'cryo-pod-window-eject-beaker'}"
|
||||
Disabled="True"
|
||||
StyleClasses="OpenBoth"/>
|
||||
</BoxContainer>
|
||||
<Button Name="EjectPatientButton"
|
||||
Text="{Loc 'cryo-pod-window-eject-patient'}"
|
||||
Disabled="True"
|
||||
HorizontalExpand="True"
|
||||
StyleClasses="OpenRight"/>
|
||||
</BoxContainer>
|
||||
|
||||
</BoxContainer>
|
||||
<BoxContainer Name="HealthSection"
|
||||
VerticalExpand="True"
|
||||
Orientation="Vertical">
|
||||
|
||||
<health:HealthAnalyzerControl Name="HealthAnalyzer"/>
|
||||
|
||||
<!-- This label is used to deal with a stray hline at the end of the health analyzer UI -->
|
||||
<Label Name="NoDamageText"
|
||||
Text="{Loc 'cryo-pod-window-health-no-damage'}"
|
||||
FontColorOverride="DeepSkyBlue"/>
|
||||
<Control VerticalExpand="True"/>
|
||||
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</controls:FancyWindow>
|
||||
260
Content.Client/Medical/Cryogenics/CryoPodWindow.xaml.cs
Normal file
260
Content.Client/Medical/Cryogenics/CryoPodWindow.xaml.cs
Normal file
@@ -0,0 +1,260 @@
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Chemistry.Reagent;
|
||||
using Content.Shared.Damage.Components;
|
||||
using Content.Shared.EntityConditions.Conditions;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Medical.Cryogenics;
|
||||
using Content.Shared.Temperature;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Prototypes;
|
||||
namespace Content.Client.Medical.Cryogenics;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class CryoPodWindow : FancyWindow
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
|
||||
public event Action? OnEjectPatientPressed;
|
||||
public event Action? OnEjectBeakerPressed;
|
||||
public event Action<FixedPoint2>? OnInjectPressed;
|
||||
|
||||
public CryoPodWindow()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
RobustXamlLoader.Load(this);
|
||||
EjectPatientButton.OnPressed += _ => OnEjectPatientPressed?.Invoke();
|
||||
EjectBeakerButton.OnPressed += _ => OnEjectBeakerPressed?.Invoke();
|
||||
Inject1.OnPressed += _ => OnInjectPressed?.Invoke(1);
|
||||
Inject5.OnPressed += _ => OnInjectPressed?.Invoke(5);
|
||||
Inject10.OnPressed += _ => OnInjectPressed?.Invoke(10);
|
||||
Inject20.OnPressed += _ => OnInjectPressed?.Invoke(20);
|
||||
}
|
||||
|
||||
public void Populate(CryoPodUserMessage msg)
|
||||
{
|
||||
// Loading screen
|
||||
if (LoadingPlaceHolder.Visible)
|
||||
{
|
||||
LoadingPlaceHolder.Visible = false;
|
||||
Sections.Visible = true;
|
||||
}
|
||||
|
||||
// Atmosphere
|
||||
var hasCorrectPressure = (msg.GasMix.Pressure > Atmospherics.WarningLowPressure);
|
||||
var hasGas = (msg.GasMix.Pressure > Atmospherics.GasMinMoles);
|
||||
var showsPressureWarning = !hasCorrectPressure;
|
||||
LowPressureWarning.Visible = showsPressureWarning;
|
||||
Pressure.Text = Loc.GetString("gas-analyzer-window-pressure-val-text",
|
||||
("pressure", $"{msg.GasMix.Pressure:0.00}"));
|
||||
Temperature.Text = Loc.GetString("generic-not-available-shorthand");
|
||||
|
||||
if (hasGas)
|
||||
{
|
||||
var celsius = TemperatureHelpers.KelvinToCelsius(msg.GasMix.Temperature);
|
||||
Temperature.Text = Loc.GetString("gas-analyzer-window-temperature-val-text",
|
||||
("tempK", $"{msg.GasMix.Temperature:0.0}"),
|
||||
("tempC", $"{celsius:0.0}"));
|
||||
}
|
||||
|
||||
// Gas mix segmented bar chart
|
||||
GasMixChart.Clear();
|
||||
GasMixChart.Visible = hasGas;
|
||||
|
||||
if (msg.GasMix.Gases != null)
|
||||
{
|
||||
var totalGasAmount = msg.GasMix.Gases.Sum(gas => gas.Amount);
|
||||
|
||||
foreach (var gas in msg.GasMix.Gases)
|
||||
{
|
||||
var color = Color.FromHex($"#{gas.Color}", Color.White);
|
||||
var percent = gas.Amount / totalGasAmount * 100;
|
||||
var localizedName = Loc.GetString(gas.Name);
|
||||
var tooltip = Loc.GetString("gas-analyzer-window-molarity-percentage-text",
|
||||
("gasName", localizedName),
|
||||
("amount", $"{gas.Amount:0.##}"),
|
||||
("percentage", $"{percent:0.#}"));
|
||||
GasMixChart.AddEntry(gas.Amount, color, tooltip: tooltip);
|
||||
}
|
||||
}
|
||||
|
||||
// Health analyzer
|
||||
var maybePatient = _entityManager.GetEntity(msg.Health.TargetEntity);
|
||||
var hasPatient = msg.Health.TargetEntity.HasValue;
|
||||
var hasDamage = (hasPatient
|
||||
&& _entityManager.TryGetComponent(maybePatient, out DamageableComponent? damageable)
|
||||
&& damageable.TotalDamage > 0);
|
||||
|
||||
NoDamageText.Visible = (hasPatient && !hasDamage);
|
||||
HealthSection.Visible = hasPatient;
|
||||
EjectPatientButton.Disabled = !hasPatient;
|
||||
|
||||
if (hasPatient)
|
||||
HealthAnalyzer.Populate(msg.Health);
|
||||
|
||||
// Reagents
|
||||
float? lowestTempRequirement = null;
|
||||
ReagentId? lowestTempReagent = null;
|
||||
var totalBeakerCapacity = msg.BeakerCapacity ?? 0;
|
||||
var availableQuantity = new FixedPoint2();
|
||||
var injectingQuantity =
|
||||
msg.Injecting?.Aggregate(new FixedPoint2(), (sum, r) => sum + r.Quantity)
|
||||
?? new FixedPoint2(); // Either the sum of the reagent quantities in `msg.Injecting` or zero.
|
||||
var hasBeaker = (msg.Beaker != null);
|
||||
|
||||
ChemicalsChart.Clear();
|
||||
ChemicalsChart.Capacity = (totalBeakerCapacity < 1 ? 50 : (int)totalBeakerCapacity);
|
||||
|
||||
var chartMaxChemsQuantity = ChemicalsChart.Capacity - injectingQuantity; // Ensure space for injection buffer
|
||||
|
||||
if (hasBeaker)
|
||||
{
|
||||
foreach (var (reagent, quantity) in msg.Beaker!)
|
||||
{
|
||||
availableQuantity += quantity;
|
||||
|
||||
// Make sure we don't add too many chemicals to the chart, so that there's still enough space to
|
||||
// visualize the injection buffer.
|
||||
var chemsQuantityOvershoot = FixedPoint2.Max(0, availableQuantity - chartMaxChemsQuantity);
|
||||
var chartQuantity = FixedPoint2.Max(0, quantity - chemsQuantityOvershoot);
|
||||
|
||||
var reagentProto = _prototypeManager.Index<ReagentPrototype>(reagent.Prototype);
|
||||
ChemicalsChart.SetEntry(
|
||||
reagent.Prototype,
|
||||
reagentProto.LocalizedName,
|
||||
(float)chartQuantity,
|
||||
reagentProto.SubstanceColor,
|
||||
tooltip: $"{quantity}u {reagentProto.LocalizedName}"
|
||||
);
|
||||
|
||||
var temp = TryFindMaxTemperatureRequirement(reagent);
|
||||
if (lowestTempRequirement == null
|
||||
|| temp < lowestTempRequirement)
|
||||
{
|
||||
lowestTempRequirement = temp;
|
||||
lowestTempReagent = reagent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (injectingQuantity != 0)
|
||||
{
|
||||
var injectingText = (injectingQuantity > 1 ? $"{injectingQuantity}u" : "");
|
||||
ChemicalsChart.SetEntry(
|
||||
"injecting",
|
||||
injectingText,
|
||||
(float)injectingQuantity,
|
||||
Color.MediumSpringGreen,
|
||||
tooltip: Loc.GetString("cryo-pod-window-chems-injecting-tooltip",
|
||||
("quantity", injectingQuantity))
|
||||
);
|
||||
}
|
||||
|
||||
var isBeakerEmpty = (injectingQuantity + availableQuantity == 0);
|
||||
var isChemicalsChartVisible = (hasBeaker || injectingQuantity != 0);
|
||||
NoBeakerText.Visible = !isChemicalsChartVisible;
|
||||
ChemicalsChart.Visible = isChemicalsChartVisible;
|
||||
Inject1.Disabled = (!hasPatient || availableQuantity < 0.1f);
|
||||
Inject5.Disabled = (!hasPatient || availableQuantity <= 1);
|
||||
Inject10.Disabled = (!hasPatient || availableQuantity <= 5);
|
||||
Inject20.Disabled = (!hasPatient || availableQuantity <= 10);
|
||||
EjectBeakerButton.Disabled = !hasBeaker;
|
||||
|
||||
// Temperature warning
|
||||
var hasCorrectTemperature = (lowestTempRequirement == null || lowestTempRequirement > msg.GasMix.Temperature);
|
||||
var showsTemperatureWarning = (!showsPressureWarning && !hasCorrectTemperature);
|
||||
|
||||
HighTemperatureWarning.Visible = showsTemperatureWarning;
|
||||
|
||||
if (showsTemperatureWarning)
|
||||
{
|
||||
var reagentName = _prototypeManager.Index<ReagentPrototype>(lowestTempReagent!.Value.Prototype)
|
||||
.LocalizedName;
|
||||
HighTemperatureWarningText.Text = Loc.GetString("cryo-pod-window-high-temperature-warning",
|
||||
("reagent", reagentName),
|
||||
("temperature", lowestTempRequirement!));
|
||||
}
|
||||
|
||||
// Status checklist
|
||||
const float fallbackTemperatureRequirement = 213;
|
||||
var hasTemperatureCheck = (hasGas && hasCorrectTemperature
|
||||
&& (lowestTempRequirement != null || msg.GasMix.Temperature < fallbackTemperatureRequirement));
|
||||
var hasChemicals = (hasBeaker && !isBeakerEmpty);
|
||||
|
||||
UpdateChecklistItem(PressureCheck, Loc.GetString("cryo-pod-window-checklist-pressure"), hasCorrectPressure);
|
||||
UpdateChecklistItem(ChemicalsCheck, Loc.GetString("cryo-pod-window-checklist-chemicals"), hasChemicals);
|
||||
UpdateChecklistItem(TemperatureCheck, Loc.GetString("cryo-pod-window-checklist-temperature"), hasTemperatureCheck);
|
||||
|
||||
var isReady = (hasCorrectPressure && hasChemicals && hasTemperatureCheck);
|
||||
var isCooling = (lowestTempRequirement != null && hasPatient
|
||||
&& msg.Health.Temperature > lowestTempRequirement);
|
||||
var isInjecting = (injectingQuantity > 0);
|
||||
StatusLabel.Text = (!isReady ? Loc.GetString("cryo-pod-window-status-not-ready") :
|
||||
isCooling ? Loc.GetString("cryo-pod-window-status-cooling") :
|
||||
isInjecting ? Loc.GetString("cryo-pod-window-status-injecting") :
|
||||
hasPatient ? Loc.GetString("cryo-pod-window-status-ready-to-inject") :
|
||||
Loc.GetString("cryo-pod-window-status-ready-for-patient"));
|
||||
StatusLabel.FontColorOverride = (isReady ? Color.DeepSkyBlue : Color.Orange);
|
||||
}
|
||||
|
||||
private void UpdateChecklistItem(Label label, string text, bool isOkay)
|
||||
{
|
||||
label.Text = (isOkay ? text : Loc.GetString("cryo-pod-window-checklist-fail", ("item", text)));
|
||||
label.FontColorOverride = (isOkay ? null : Color.Orange);
|
||||
}
|
||||
|
||||
private float? TryFindMaxTemperatureRequirement(ReagentId reagent)
|
||||
{
|
||||
var reagentProto = _prototypeManager.Index<ReagentPrototype>(reagent.Prototype);
|
||||
if (reagentProto.Metabolisms == null)
|
||||
return null;
|
||||
|
||||
float? result = null;
|
||||
|
||||
foreach (var (_, metabolism) in reagentProto.Metabolisms)
|
||||
{
|
||||
foreach (var effect in metabolism.Effects)
|
||||
{
|
||||
if (effect.Conditions == null)
|
||||
continue;
|
||||
|
||||
foreach (var condition in effect.Conditions)
|
||||
{
|
||||
// If there are multiple temperature conditions in the same reagent (which could hypothetically
|
||||
// happen, although it currently doesn't), we return the lowest max temperature.
|
||||
if (condition is TemperatureCondition tempCondition
|
||||
&& float.IsFinite(tempCondition.Max)
|
||||
&& (result == null || tempCondition.Max < result))
|
||||
{
|
||||
result = tempCondition.Max;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public void SetEjectErrorVisible(bool isVisible)
|
||||
{
|
||||
EjectError.Visible = isVisible;
|
||||
}
|
||||
|
||||
protected override Vector2 MeasureOverride(Vector2 availableSize)
|
||||
{
|
||||
const float antiJiggleSlackSpace = 80;
|
||||
var oldSize = DesiredSize;
|
||||
var newSize = base.MeasureOverride(availableSize);
|
||||
|
||||
// Reduce how often the height of the window jiggles
|
||||
if (newSize.Y < oldSize.Y && newSize.Y + antiJiggleSlackSpace > oldSize.Y)
|
||||
newSize.Y = oldSize.Y;
|
||||
|
||||
return newSize;
|
||||
}
|
||||
}
|
||||
5
Content.Client/Medical/DefibrillatorSystem.cs
Normal file
5
Content.Client/Medical/DefibrillatorSystem.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
using Content.Shared.Medical;
|
||||
|
||||
namespace Content.Client.Medical;
|
||||
|
||||
public sealed class DefibrillatorSystem : SharedDefibrillatorSystem;
|
||||
@@ -41,9 +41,9 @@ public sealed class NetworkConfiguratorLinkOverlay : Overlay
|
||||
if (!Colors.TryGetValue(uid, out var color))
|
||||
{
|
||||
color = new Color(
|
||||
_random.Next(0, 255),
|
||||
_random.Next(0, 255),
|
||||
_random.Next(0, 255));
|
||||
_random.NextByte(0, 255),
|
||||
_random.NextByte(0, 255),
|
||||
_random.NextByte(0, 255));
|
||||
Colors.Add(uid, color);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
using Content.Shared.Inventory.Events;
|
||||
using Content.Shared.Overlays;
|
||||
using Robust.Client.Graphics;
|
||||
using System.Linq;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client.Overlays;
|
||||
@@ -35,6 +33,9 @@ public sealed class ShowHealthBarsSystem : EquipmentHudSystem<ShowHealthBarsComp
|
||||
{
|
||||
base.UpdateInternal(component);
|
||||
|
||||
_overlay.DamageContainers.Clear();
|
||||
_overlay.StatusIcon = null;
|
||||
|
||||
foreach (var comp in component.Components)
|
||||
{
|
||||
foreach (var damageContainerId in comp.DamageContainers)
|
||||
|
||||
@@ -5,7 +5,6 @@ using Content.Shared.Overlays;
|
||||
using Content.Shared.StatusIcon;
|
||||
using Content.Shared.StatusIcon.Components;
|
||||
using Robust.Shared.Prototypes;
|
||||
using System.Linq;
|
||||
using Content.Shared.Damage.Components;
|
||||
|
||||
namespace Content.Client.Overlays;
|
||||
@@ -32,9 +31,13 @@ public sealed class ShowHealthIconsSystem : EquipmentHudSystem<ShowHealthIconsCo
|
||||
{
|
||||
base.UpdateInternal(component);
|
||||
|
||||
foreach (var damageContainerId in component.Components.SelectMany(x => x.DamageContainers))
|
||||
DamageContainers.Clear();
|
||||
foreach (var comp in component.Components)
|
||||
{
|
||||
DamageContainers.Add(damageContainerId);
|
||||
foreach (var damageContainerId in comp.DamageContainers)
|
||||
{
|
||||
DamageContainers.Add(damageContainerId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,11 @@ public sealed class StationMapBoundUserInterface : BoundUserInterface
|
||||
base.Open();
|
||||
EntityUid? gridUid = null;
|
||||
|
||||
if (EntMan.TryGetComponent<TransformComponent>(Owner, out var xform))
|
||||
if (EntMan.TryGetComponent<StationMapComponent>(Owner, out var comp) && comp.TargetGrid != null)
|
||||
{
|
||||
gridUid = comp.TargetGrid;
|
||||
}
|
||||
else if (EntMan.TryGetComponent<TransformComponent>(Owner, out var xform))
|
||||
{
|
||||
gridUid = xform.GridUid;
|
||||
}
|
||||
@@ -30,8 +34,8 @@ public sealed class StationMapBoundUserInterface : BoundUserInterface
|
||||
{
|
||||
stationName = gridMetaData.EntityName;
|
||||
}
|
||||
|
||||
if (EntMan.TryGetComponent<StationMapComponent>(Owner, out var comp) && comp.ShowLocation)
|
||||
|
||||
if (comp != null && comp.ShowLocation)
|
||||
_window.Set(stationName, gridUid, Owner);
|
||||
else
|
||||
_window.Set(stationName, gridUid, null);
|
||||
|
||||
@@ -3,5 +3,6 @@ namespace Content.Client.Power;
|
||||
/// Remains in use by portable scrubbers and lathes.
|
||||
public enum PowerDeviceVisualLayers : byte
|
||||
{
|
||||
Powered
|
||||
Powered,
|
||||
Charging
|
||||
}
|
||||
|
||||
@@ -32,8 +32,7 @@ public sealed class SSDIndicatorSystem : EntitySystem
|
||||
_cfg.GetCVar(CCVars.ICShowSSDIndicator) &&
|
||||
!_mobState.IsDead(uid) &&
|
||||
!HasComp<ActiveNPCComponent>(uid) &&
|
||||
TryComp<MindContainerComponent>(uid, out var mindContainer) &&
|
||||
mindContainer.ShowExamineInfo)
|
||||
HasComp<MindExaminableComponent>(uid))
|
||||
{
|
||||
args.StatusIcons.Add(_prototype.Index(component.Icon));
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ public sealed class GenpopLockerBoundUserInterface(EntityUid owner, Enum uiKey)
|
||||
|
||||
_menu.OnConfigurationComplete += (name, time, crime) =>
|
||||
{
|
||||
SendMessage(new GenpopLockerIdConfiguredMessage(name, time, crime));
|
||||
SendPredictedMessage(new GenpopLockerIdConfiguredMessage(name, time, crime));
|
||||
Close();
|
||||
};
|
||||
|
||||
|
||||
@@ -153,7 +153,7 @@ public sealed partial class MapScreen : BoxContainer
|
||||
break;
|
||||
}
|
||||
|
||||
if (IsFTLBlocked())
|
||||
if (IsPingBlocked())
|
||||
{
|
||||
MapRebuildButton.Disabled = true;
|
||||
ClearMapObjects();
|
||||
@@ -408,9 +408,21 @@ public sealed partial class MapScreen : BoxContainer
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if we shouldn't be able to select the Scan for Objects button.
|
||||
/// </summary>
|
||||
private bool IsPingBlocked()
|
||||
{
|
||||
return _state switch
|
||||
{
|
||||
FTLState.Available or FTLState.Cooldown => false,
|
||||
_ => true,
|
||||
};
|
||||
}
|
||||
|
||||
private void OnMapObjectPress(IMapObject mapObject)
|
||||
{
|
||||
if (IsFTLBlocked())
|
||||
if (IsPingBlocked())
|
||||
return;
|
||||
|
||||
var coordinates = _shuttles.GetMapCoordinates(mapObject);
|
||||
@@ -506,7 +518,7 @@ public sealed partial class MapScreen : BoxContainer
|
||||
BumpMapDequeue();
|
||||
}
|
||||
|
||||
if (!IsFTLBlocked() && _nextPing < curTime)
|
||||
if (!IsPingBlocked() && _nextPing < curTime)
|
||||
{
|
||||
MapRebuildButton.Disabled = false;
|
||||
}
|
||||
|
||||
@@ -19,12 +19,13 @@ public sealed class SmartFridgeBoundUserInterface : BoundUserInterface
|
||||
|
||||
_menu = this.CreateWindow<SmartFridgeMenu>();
|
||||
_menu.OnItemSelected += OnItemSelected;
|
||||
_menu.OnRemoveButtonPressed += data => SendPredictedMessage(new SmartFridgeRemoveEntryMessage(data.Entry));
|
||||
Refresh();
|
||||
}
|
||||
|
||||
public void Refresh()
|
||||
{
|
||||
if (_menu is not {} menu || !EntMan.TryGetComponent(Owner, out SmartFridgeComponent? fridge))
|
||||
if (_menu is not { } menu || !EntMan.TryGetComponent(Owner, out SmartFridgeComponent? fridge))
|
||||
return;
|
||||
|
||||
menu.SetFlavorText(Loc.GetString(fridge.FlavorText));
|
||||
|
||||
@@ -13,4 +13,10 @@
|
||||
SizeFlagsStretchRatio="3"
|
||||
HorizontalExpand="True"
|
||||
ClipText="True"/>
|
||||
<TextureButton Name="RemoveButton"
|
||||
StyleClasses="CrossButtonRed"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0 0 10 0"
|
||||
Scale="0.75 0.75"
|
||||
Visible="True" />
|
||||
</BoxContainer>
|
||||
|
||||
@@ -8,11 +8,18 @@ namespace Content.Client.SmartFridge;
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class SmartFridgeItem : BoxContainer
|
||||
{
|
||||
public Action? RemoveButtonPressed;
|
||||
|
||||
public SmartFridgeItem(EntityUid uid, string text)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
EntityView.SetEntity(uid);
|
||||
NameLabel.Text = text;
|
||||
|
||||
RemoveButton.OnPressed += _ => RemoveButtonPressed?.Invoke();
|
||||
|
||||
if (uid.IsValid())
|
||||
RemoveButton.Visible = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ public sealed partial class SmartFridgeMenu : FancyWindow
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
|
||||
public event Action<GUIBoundKeyEventArgs, ListData>? OnItemSelected;
|
||||
public event Action<SmartFridgeListData>? OnRemoveButtonPressed;
|
||||
|
||||
private readonly StyleBoxFlat _styleBox = new() { BackgroundColor = new Color(70, 73, 102) };
|
||||
|
||||
@@ -48,8 +49,10 @@ public sealed partial class SmartFridgeMenu : FancyWindow
|
||||
return;
|
||||
|
||||
var label = Loc.GetString("smart-fridge-list-item", ("item", entry.Entry.Name), ("amount", entry.Amount));
|
||||
button.AddChild(new SmartFridgeItem(entry.Representative, label));
|
||||
var item = new SmartFridgeItem(entry.Representative, label);
|
||||
item.RemoveButtonPressed += () => OnRemoveButtonPressed?.Invoke(entry);
|
||||
|
||||
button.AddChild(item);
|
||||
button.ToolTip = label;
|
||||
button.StyleBoxOverride = _styleBox;
|
||||
}
|
||||
|
||||
18
Content.Client/SmartFridge/SmartFridgeSystem.cs
Normal file
18
Content.Client/SmartFridge/SmartFridgeSystem.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using Content.Shared.SmartFridge;
|
||||
|
||||
namespace Content.Client.SmartFridge;
|
||||
|
||||
public sealed class SmartFridgeSystem : SharedSmartFridgeSystem
|
||||
{
|
||||
[Dependency] private readonly SharedUserInterfaceSystem _uiSystem = default!;
|
||||
|
||||
protected override void UpdateUI(Entity<SmartFridgeComponent> ent)
|
||||
{
|
||||
base.UpdateUI(ent);
|
||||
|
||||
if (!_uiSystem.TryGetOpenUi<SmartFridgeBoundUserInterface>(ent.Owner, SmartFridgeUiKey.Key, out var bui))
|
||||
return;
|
||||
|
||||
bui.Refresh();
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
using Content.Shared.SmartFridge;
|
||||
using Robust.Shared.Analyzers;
|
||||
|
||||
namespace Content.Client.SmartFridge;
|
||||
|
||||
public sealed class SmartFridgeUISystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly SharedUserInterfaceSystem _uiSystem = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<SmartFridgeComponent, AfterAutoHandleStateEvent>(OnSmartFridgeAfterState);
|
||||
}
|
||||
|
||||
private void OnSmartFridgeAfterState(Entity<SmartFridgeComponent> ent, ref AfterAutoHandleStateEvent args)
|
||||
{
|
||||
if (!_uiSystem.TryGetOpenUi<SmartFridgeBoundUserInterface>(ent.Owner, SmartFridgeUiKey.Key, out var bui))
|
||||
return;
|
||||
|
||||
bui.Refresh();
|
||||
}
|
||||
}
|
||||
@@ -69,10 +69,10 @@ public sealed class LabelSheetlet : Sheetlet<PalettedStylesheet>
|
||||
.Class(StyleClass.LabelMonospaceText)
|
||||
.Prop(Label.StylePropertyFont, robotoMonoBold11),
|
||||
E<Label>()
|
||||
.Class(StyleClass.LabelMonospaceHeading)
|
||||
.Class(StyleClass.LabelMonospaceSubHeading)
|
||||
.Prop(Label.StylePropertyFont, robotoMonoBold12),
|
||||
E<Label>()
|
||||
.Class(StyleClass.LabelMonospaceSubHeading)
|
||||
.Class(StyleClass.LabelMonospaceHeading)
|
||||
.Prop(Label.StylePropertyFont, robotoMonoBold14),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -41,8 +41,8 @@ public static class StyleClass
|
||||
public const string LabelKeyText = "LabelKeyText";
|
||||
public const string LabelWeak = "LabelWeak"; // replaces `LabelSecondaryColor`
|
||||
public const string LabelMonospaceText = "ConsoleText";
|
||||
public const string LabelMonospaceHeading = "ConsoleText";
|
||||
public const string LabelMonospaceSubHeading = "ConsoleText";
|
||||
public const string LabelMonospaceHeading = "ConsoleHeading";
|
||||
public const string LabelMonospaceSubHeading = "ConsoleSubHeading";
|
||||
|
||||
public const string BackgroundPanel = "BackgroundPanel"; // replaces `AngleRect`
|
||||
public const string BackgroundPanelOpenLeft = "BackgroundPanelOpenLeft"; // replaces `BackgroundOpenLeft`
|
||||
|
||||
@@ -34,11 +34,17 @@ public sealed class SurveillanceCameraMonitorBoundUserInterface : BoundUserInter
|
||||
_window.SubnetRefresh += OnSubnetRefresh;
|
||||
_window.CameraSwitchTimer += OnCameraSwitchTimer;
|
||||
_window.CameraDisconnect += OnCameraDisconnect;
|
||||
|
||||
var xform = EntMan.GetComponent<TransformComponent>(Owner);
|
||||
var gridUid = xform.GridUid ?? xform.MapUid;
|
||||
|
||||
if (gridUid is not null)
|
||||
_window?.SetMap(gridUid.Value);
|
||||
}
|
||||
|
||||
private void OnCameraSelected(string address)
|
||||
private void OnCameraSelected(string address, string? subnet)
|
||||
{
|
||||
SendMessage(new SurveillanceCameraMonitorSwitchMessage(address));
|
||||
SendMessage(new SurveillanceCameraMonitorSwitchMessage(address, subnet));
|
||||
}
|
||||
|
||||
private void OnSubnetRequest(string subnet)
|
||||
|
||||
@@ -1,25 +1,71 @@
|
||||
<DefaultWindow xmlns="https://spacestation14.io"
|
||||
xmlns:viewport="clr-namespace:Content.Client.Viewport"
|
||||
xmlns:local="clr-namespace:Content.Client.SurveillanceCamera.UI"
|
||||
Title="{Loc 'surveillance-camera-monitor-ui-window'}">
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<BoxContainer Orientation="Vertical" MinWidth="350" VerticalExpand="True">
|
||||
<!-- lazy -->
|
||||
<OptionButton Name="SubnetSelector" />
|
||||
<Button Name="SubnetRefreshButton" Text="{Loc 'surveillance-camera-monitor-ui-refresh-subnets'}" />
|
||||
<ScrollContainer VerticalExpand="True">
|
||||
<ItemList Name="SubnetList" />
|
||||
</ScrollContainer>
|
||||
<Button Name="CameraRefreshButton" Text="{Loc 'surveillance-camera-monitor-ui-refresh-cameras'}" />
|
||||
<Button Name="CameraDisconnectButton" Text="{Loc 'surveillance-camera-monitor-ui-disconnect'}" />
|
||||
<Label Name="CameraStatus" />
|
||||
<BoxContainer>
|
||||
<!-- Panel with tabs -->
|
||||
<BoxContainer Orientation="Vertical" MinWidth="350">
|
||||
<TabContainer Name="ViewModeTabs" VerticalExpand="True">
|
||||
<!-- Camera list tab -->
|
||||
<BoxContainer Name="{Loc 'surveillance-camera-monitor-ui-tab-list'}" Orientation="Vertical">
|
||||
<OptionButton Name="SubnetSelector"/>
|
||||
<Button Name="SubnetRefreshButton" Text="{Loc 'surveillance-camera-monitor-ui-refresh-subnets'}"/>
|
||||
<ScrollContainer VerticalExpand="True">
|
||||
<ItemList Name="SubnetList"/>
|
||||
</ScrollContainer>
|
||||
<Button Name="CameraRefreshButton" Text="{Loc 'surveillance-camera-monitor-ui-refresh-cameras'}"/>
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Map view tab -->
|
||||
<BoxContainer Name="{Loc 'surveillance-camera-monitor-ui-tab-map'}" Orientation="Vertical" VerticalExpand="True">
|
||||
<local:SurveillanceCameraNavMapControl Name="CameraMap"
|
||||
VerticalExpand="True"
|
||||
HorizontalExpand="True"
|
||||
MinSize="350 350"/>
|
||||
|
||||
<!-- Map legend -->
|
||||
<BoxContainer Name="LegendContainer" Margin="0 10">
|
||||
<TextureRect Stretch="KeepAspectCentered"
|
||||
TexturePath="/Textures/Interface/NavMap/beveled_triangle.png"
|
||||
Modulate="#FF00FF"
|
||||
SetSize="20 20"
|
||||
Margin="10 0 5 0"/>
|
||||
<Label Text="{Loc 'surveillance-camera-monitor-ui-legend-active'}"/>
|
||||
|
||||
<TextureRect Stretch="KeepAspectCentered"
|
||||
TexturePath="/Textures/Interface/NavMap/beveled_triangle.png"
|
||||
SetSize="20 20"
|
||||
Modulate="#fbff19ff"
|
||||
Margin="10 0 5 0"/>
|
||||
<Label Text="{Loc 'surveillance-camera-monitor-ui-legend-selected'}"/>
|
||||
|
||||
<TextureRect Stretch="KeepAspectCentered"
|
||||
TexturePath="/Textures/Interface/NavMap/beveled_circle.png"
|
||||
SetSize="20 20"
|
||||
Modulate="#a09f9fff"
|
||||
Margin="10 0 5 0"/>
|
||||
<Label Text="{Loc 'surveillance-camera-monitor-ui-legend-inactive'}"/>
|
||||
|
||||
<TextureRect Stretch="KeepAspectCentered"
|
||||
TexturePath="/Textures/Interface/NavMap/beveled_square.png"
|
||||
SetSize="20 20"
|
||||
Modulate="#fa1f1fff"
|
||||
Margin="10 0 5 0"/>
|
||||
<Label Text="{Loc 'surveillance-camera-monitor-ui-legend-invalid'}"/>
|
||||
</BoxContainer>
|
||||
<Button Name="SubnetRefreshButtonMap" Text="{Loc 'surveillance-camera-monitor-ui-refresh-subnets'}"/>
|
||||
</BoxContainer>
|
||||
</TabContainer>
|
||||
<Button Name="CameraDisconnectButton" Text="{Loc 'surveillance-camera-monitor-ui-disconnect'}"/>
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Right panel with camera view -->
|
||||
<BoxContainer Orientation="Vertical" HorizontalExpand="True">
|
||||
<Label Name="CameraStatus"/>
|
||||
<Control VerticalExpand="True" Margin="5" Name="CameraViewBox">
|
||||
<viewport:ScalingViewport Name="CameraView" MinSize="500 500" MouseFilter="Ignore"/>
|
||||
<TextureRect MinSize="500 500" Name="CameraViewBackground" />
|
||||
</Control>
|
||||
</BoxContainer>
|
||||
<Control VerticalExpand="True" HorizontalExpand="True" Margin="5 5 5 5" Name="CameraViewBox">
|
||||
<viewport:ScalingViewport Name="CameraView"
|
||||
VerticalExpand="True"
|
||||
HorizontalExpand="True"
|
||||
MinSize="500 500"
|
||||
MouseFilter="Ignore" />
|
||||
<TextureRect VerticalExpand="True" HorizontalExpand="True" MinSize="500 500" Name="CameraViewBackground" />
|
||||
</Control>
|
||||
</BoxContainer>
|
||||
</DefaultWindow>
|
||||
|
||||
@@ -3,6 +3,7 @@ using Content.Client.Resources;
|
||||
using Content.Client.Viewport;
|
||||
using Content.Shared.DeviceNetwork;
|
||||
using Content.Shared.SurveillanceCamera;
|
||||
using Content.Shared.SurveillanceCamera.Components;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.ResourceManagement;
|
||||
@@ -21,8 +22,15 @@ public sealed partial class SurveillanceCameraMonitorWindow : DefaultWindow
|
||||
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly IResourceCache _resourceCache = default!;
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Triggered when a camera is selected.
|
||||
/// First parameter contains the camera's address.
|
||||
/// Second optional parameter contains a subnet - if possible, the monitor will switch to this subnet.
|
||||
/// </summary>
|
||||
public event Action<string, string?>? CameraSelected;
|
||||
|
||||
public event Action<string>? CameraSelected;
|
||||
public event Action<string>? SubnetOpened;
|
||||
public event Action? CameraRefresh;
|
||||
public event Action? SubnetRefresh;
|
||||
@@ -33,6 +41,7 @@ public sealed partial class SurveillanceCameraMonitorWindow : DefaultWindow
|
||||
private bool _isSwitching;
|
||||
private readonly FixedEye _defaultEye = new();
|
||||
private readonly Dictionary<string, int> _subnetMap = new();
|
||||
private EntityUid? _mapUid;
|
||||
|
||||
private string? SelectedSubnet
|
||||
{
|
||||
@@ -68,11 +77,15 @@ public sealed partial class SurveillanceCameraMonitorWindow : DefaultWindow
|
||||
SubnetSelector.OnItemSelected += args =>
|
||||
{
|
||||
// piss
|
||||
SubnetOpened!((string) args.Button.GetItemMetadata(args.Id)!);
|
||||
SubnetOpened?.Invoke((string) args.Button.GetItemMetadata(args.Id)!);
|
||||
};
|
||||
SubnetRefreshButton.OnPressed += _ => SubnetRefresh!();
|
||||
CameraRefreshButton.OnPressed += _ => CameraRefresh!();
|
||||
CameraDisconnectButton.OnPressed += _ => CameraDisconnect!();
|
||||
SubnetRefreshButton.OnPressed += _ => SubnetRefresh?.Invoke();
|
||||
SubnetRefreshButtonMap.OnPressed += _ => SubnetRefresh?.Invoke();
|
||||
CameraRefreshButton.OnPressed += _ => CameraRefresh?.Invoke();
|
||||
CameraDisconnectButton.OnPressed += _ => CameraDisconnect?.Invoke();
|
||||
|
||||
CameraMap.EnableCameraSelection = true;
|
||||
CameraMap.CameraSelected += OnCameraMapSelected;
|
||||
}
|
||||
|
||||
|
||||
@@ -80,6 +93,9 @@ public sealed partial class SurveillanceCameraMonitorWindow : DefaultWindow
|
||||
// pass it here so that the UI can change its view.
|
||||
public void UpdateState(IEye? eye, HashSet<string> subnets, string activeAddress, string activeSubnet, Dictionary<string, string> cameras)
|
||||
{
|
||||
CameraMap.SetActiveCameraAddress(activeAddress);
|
||||
CameraMap.SetAvailableSubnets(subnets);
|
||||
|
||||
_currentAddress = activeAddress;
|
||||
SetCameraView(eye);
|
||||
|
||||
@@ -189,6 +205,25 @@ public sealed partial class SurveillanceCameraMonitorWindow : DefaultWindow
|
||||
|
||||
private void OnSubnetListSelect(ItemList.ItemListSelectedEventArgs args)
|
||||
{
|
||||
CameraSelected!((string) SubnetList[args.ItemIndex].Metadata!);
|
||||
CameraSelected!((string) SubnetList[args.ItemIndex].Metadata!, null);
|
||||
}
|
||||
|
||||
public void SetMap(EntityUid mapUid)
|
||||
{
|
||||
CameraMap.MapUid = _mapUid = mapUid;
|
||||
}
|
||||
|
||||
private void OnCameraMapSelected(NetEntity netEntity)
|
||||
{
|
||||
if (_mapUid is null || !_entityManager.TryGetComponent<SurveillanceCameraMapComponent>(_mapUid.Value, out var mapComp))
|
||||
return;
|
||||
|
||||
if (!mapComp.Cameras.TryGetValue(netEntity, out var marker) || !marker.Active)
|
||||
return;
|
||||
|
||||
if (!string.IsNullOrEmpty(marker.Address))
|
||||
CameraSelected?.Invoke(marker.Address, marker.Subnet);
|
||||
else
|
||||
_entityManager.RaisePredictiveEvent(new RequestCameraMarkerUpdateMessage(netEntity));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Shared.Map;
|
||||
using Content.Client.Pinpointer.UI;
|
||||
using Content.Client.Resources;
|
||||
using Content.Shared.SurveillanceCamera.Components;
|
||||
|
||||
namespace Content.Client.SurveillanceCamera.UI;
|
||||
|
||||
public sealed class SurveillanceCameraNavMapControl : NavMapControl
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly IResourceCache _resourceCache = default!;
|
||||
|
||||
private static readonly Color CameraActiveColor = Color.FromHex("#FF00FF");
|
||||
private static readonly Color CameraInactiveColor = Color.FromHex("#a09f9fff");
|
||||
private static readonly Color CameraSelectedColor = Color.FromHex("#fbff19ff");
|
||||
private static readonly Color CameraInvalidColor = Color.FromHex("#fa1f1fff");
|
||||
|
||||
private readonly Texture _activeTexture;
|
||||
private readonly Texture _inactiveTexture;
|
||||
private readonly Texture _selectedTexture;
|
||||
private readonly Texture _invalidTexture;
|
||||
|
||||
private string _activeCameraAddress = string.Empty;
|
||||
private HashSet<string> _availableSubnets = new();
|
||||
private (Dictionary<NetEntity, CameraMarker> Cameras, string ActiveAddress, HashSet<string> AvailableSubnets) _lastState;
|
||||
|
||||
public bool EnableCameraSelection { get; set; }
|
||||
|
||||
public event Action<NetEntity>? CameraSelected;
|
||||
|
||||
|
||||
public SurveillanceCameraNavMapControl()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
_activeTexture = _resourceCache.GetTexture("/Textures/Interface/NavMap/beveled_triangle.png");
|
||||
_selectedTexture = _activeTexture;
|
||||
_inactiveTexture = _resourceCache.GetTexture("/Textures/Interface/NavMap/beveled_circle.png");
|
||||
_invalidTexture = _resourceCache.GetTexture("/Textures/Interface/NavMap/beveled_square.png");
|
||||
|
||||
TrackedEntitySelectedAction += entity =>
|
||||
{
|
||||
if (entity.HasValue)
|
||||
CameraSelected?.Invoke(entity.Value);
|
||||
};
|
||||
}
|
||||
|
||||
public void SetActiveCameraAddress(string address)
|
||||
{
|
||||
if (_activeCameraAddress == address)
|
||||
return;
|
||||
|
||||
_activeCameraAddress = address;
|
||||
ForceNavMapUpdate();
|
||||
}
|
||||
|
||||
public void SetAvailableSubnets(HashSet<string> subnets)
|
||||
{
|
||||
if (_availableSubnets.SetEquals(subnets))
|
||||
return;
|
||||
|
||||
_availableSubnets = subnets;
|
||||
ForceNavMapUpdate();
|
||||
}
|
||||
|
||||
protected override void UpdateNavMap()
|
||||
{
|
||||
base.UpdateNavMap();
|
||||
|
||||
if (MapUid is null || !_entityManager.TryGetComponent<SurveillanceCameraMapComponent>(MapUid, out var mapComp))
|
||||
return;
|
||||
|
||||
var currentState = (mapComp.Cameras, _activeCameraAddress, _availableSubnets);
|
||||
if (_lastState.Equals(currentState))
|
||||
return;
|
||||
|
||||
_lastState = currentState;
|
||||
UpdateCameraMarkers(mapComp);
|
||||
}
|
||||
|
||||
private void UpdateCameraMarkers(SurveillanceCameraMapComponent mapComp)
|
||||
{
|
||||
TrackedEntities.Clear();
|
||||
|
||||
if (MapUid is null)
|
||||
return;
|
||||
|
||||
foreach (var (netEntity, marker) in mapComp.Cameras)
|
||||
{
|
||||
if (!marker.Visible || !_availableSubnets.Contains(marker.Subnet))
|
||||
continue;
|
||||
|
||||
var coords = new EntityCoordinates(MapUid.Value, marker.Position);
|
||||
|
||||
Texture texture;
|
||||
Color color;
|
||||
|
||||
if (string.IsNullOrEmpty(marker.Address))
|
||||
{
|
||||
color = CameraInvalidColor;
|
||||
texture = _invalidTexture;
|
||||
}
|
||||
else if (marker.Address == _activeCameraAddress)
|
||||
{
|
||||
color = CameraSelectedColor;
|
||||
texture = _selectedTexture;
|
||||
}
|
||||
else if (marker.Active)
|
||||
{
|
||||
color = CameraActiveColor;
|
||||
texture = _activeTexture;
|
||||
}
|
||||
else
|
||||
{
|
||||
color = CameraInactiveColor;
|
||||
texture = _inactiveTexture;
|
||||
}
|
||||
|
||||
TrackedEntities[netEntity] = new NavMapBlip(
|
||||
coords,
|
||||
texture,
|
||||
color,
|
||||
false,
|
||||
EnableCameraSelection
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ public static class BoundKeyHelper
|
||||
public static string ShortKeyName(BoundKeyFunction keyFunction)
|
||||
{
|
||||
// need to use shortened key names so they fit in the buttons.
|
||||
return TryGetShortKeyName(keyFunction, out var name) ? Loc.GetString(name) : " ";
|
||||
return TryGetShortKeyName(keyFunction, out var name) ? name : " ";
|
||||
}
|
||||
|
||||
public static bool IsBound(BoundKeyFunction keyFunction)
|
||||
|
||||
@@ -198,8 +198,8 @@ public sealed class ActionButton : Control, IEntityControl
|
||||
if (!_entities.TryGetComponent(Action, out MetaDataComponent? metadata))
|
||||
return null;
|
||||
|
||||
var name = FormattedMessage.FromMarkupPermissive(Loc.GetString(metadata.EntityName));
|
||||
var desc = FormattedMessage.FromMarkupPermissive(Loc.GetString(metadata.EntityDescription));
|
||||
var name = FormattedMessage.FromMarkupPermissive(metadata.EntityName);
|
||||
var desc = FormattedMessage.FromMarkupPermissive(metadata.EntityDescription);
|
||||
|
||||
if (_player.LocalEntity is null)
|
||||
return null;
|
||||
|
||||
@@ -43,6 +43,9 @@ public sealed partial class MeleeWeaponSystem
|
||||
return;
|
||||
}
|
||||
|
||||
var length = 1f;
|
||||
var offset = 1f;
|
||||
|
||||
var spriteRotation = Angle.Zero;
|
||||
if (arcComponent.Animation != WeaponArcAnimation.None
|
||||
&& TryComp(weapon, out MeleeWeaponComponent? meleeWeaponComponent))
|
||||
@@ -55,9 +58,11 @@ public sealed partial class MeleeWeaponSystem
|
||||
|
||||
if (meleeWeaponComponent.SwingLeft)
|
||||
angle *= -1;
|
||||
|
||||
length = (1 / meleeWeaponComponent.AttackRate) * 0.6f;
|
||||
offset = meleeWeaponComponent.AnimationOffset;
|
||||
}
|
||||
_sprite.SetRotation((animationUid, sprite), localPos.ToWorldAngle());
|
||||
var distance = Math.Clamp(localPos.Length() / 2f, 0.2f, 1f);
|
||||
|
||||
var xform = _xformQuery.GetComponent(animationUid);
|
||||
TrackUserComponent track;
|
||||
@@ -67,16 +72,16 @@ public sealed partial class MeleeWeaponSystem
|
||||
case WeaponArcAnimation.Slash:
|
||||
track = EnsureComp<TrackUserComponent>(animationUid);
|
||||
track.User = user;
|
||||
_animation.Play(animationUid, GetSlashAnimation(sprite, angle, spriteRotation), SlashAnimationKey);
|
||||
_animation.Play(animationUid, GetSlashAnimation((animationUid, sprite), angle, spriteRotation, length, offset), SlashAnimationKey);
|
||||
if (arcComponent.Fadeout)
|
||||
_animation.Play(animationUid, GetFadeAnimation(sprite, 0.065f, 0.065f + 0.05f), FadeAnimationKey);
|
||||
_animation.Play(animationUid, GetFadeAnimation(sprite, length * 0.5f, length + 0.15f), FadeAnimationKey);
|
||||
break;
|
||||
case WeaponArcAnimation.Thrust:
|
||||
track = EnsureComp<TrackUserComponent>(animationUid);
|
||||
track.User = user;
|
||||
_animation.Play(animationUid, GetThrustAnimation((animationUid, sprite), distance, spriteRotation), ThrustAnimationKey);
|
||||
_animation.Play(animationUid, GetThrustAnimation((animationUid, sprite), offset, spriteRotation, length), ThrustAnimationKey);
|
||||
if (arcComponent.Fadeout)
|
||||
_animation.Play(animationUid, GetFadeAnimation(sprite, 0.05f, 0.15f), FadeAnimationKey);
|
||||
_animation.Play(animationUid, GetFadeAnimation(sprite, length * 0.5f, length + 0.15f), FadeAnimationKey);
|
||||
break;
|
||||
case WeaponArcAnimation.None:
|
||||
var (mapPos, mapRot) = TransformSystem.GetWorldPositionRotation(userXform);
|
||||
@@ -89,21 +94,22 @@ public sealed partial class MeleeWeaponSystem
|
||||
}
|
||||
}
|
||||
|
||||
private Animation GetSlashAnimation(SpriteComponent sprite, Angle arc, Angle spriteRotation)
|
||||
private Animation GetSlashAnimation(Entity<SpriteComponent> sprite, Angle arc, Angle spriteRotation, float length, float offset)
|
||||
{
|
||||
const float slashStart = 0.03f;
|
||||
const float slashEnd = 0.065f;
|
||||
const float length = slashEnd + 0.05f;
|
||||
var startRotation = sprite.Rotation + arc / 2;
|
||||
var endRotation = sprite.Rotation - arc / 2;
|
||||
var startRotationOffset = startRotation.RotateVec(new Vector2(0f, -1f));
|
||||
var endRotationOffset = endRotation.RotateVec(new Vector2(0f, -1f));
|
||||
var startRotation = sprite.Comp.Rotation + (arc * 0.5f);
|
||||
var endRotation = sprite.Comp.Rotation - (arc * 0.5f);
|
||||
|
||||
var startRotationOffset = startRotation.RotateVec(new Vector2(0f, -offset * 0.9f));
|
||||
var minRotationOffset = sprite.Comp.Rotation.RotateVec(new Vector2(0f, -offset * 1.1f));
|
||||
var endRotationOffset = endRotation.RotateVec(new Vector2(0f, -offset * 0.9f));
|
||||
|
||||
startRotation += spriteRotation;
|
||||
endRotation += spriteRotation;
|
||||
sprite.Comp.NoRotation = true;
|
||||
|
||||
return new Animation()
|
||||
{
|
||||
Length = TimeSpan.FromSeconds(length),
|
||||
Length = TimeSpan.FromSeconds(length + 0.05f),
|
||||
AnimationTracks =
|
||||
{
|
||||
new AnimationTrackComponentProperty()
|
||||
@@ -112,10 +118,12 @@ public sealed partial class MeleeWeaponSystem
|
||||
Property = nameof(SpriteComponent.Rotation),
|
||||
KeyFrames =
|
||||
{
|
||||
new AnimationTrackProperty.KeyFrame(startRotation, 0f),
|
||||
new AnimationTrackProperty.KeyFrame(startRotation, slashStart),
|
||||
new AnimationTrackProperty.KeyFrame(endRotation, slashEnd)
|
||||
}
|
||||
new AnimationTrackProperty.KeyFrame(Angle.Lerp(startRotation,endRotation,0.0f), length * 0.0f),
|
||||
new AnimationTrackProperty.KeyFrame(Angle.Lerp(startRotation,endRotation,0.5f), length * 0.10f),
|
||||
new AnimationTrackProperty.KeyFrame(Angle.Lerp(startRotation,endRotation,1.0f), length * 0.15f),
|
||||
new AnimationTrackProperty.KeyFrame(Angle.Lerp(startRotation,endRotation,0.9f), length * 0.20f),
|
||||
new AnimationTrackProperty.KeyFrame(Angle.Lerp(startRotation,endRotation,0.80f), length * 0.6f, Easings.OutQuart)
|
||||
},
|
||||
},
|
||||
new AnimationTrackComponentProperty()
|
||||
{
|
||||
@@ -123,21 +131,21 @@ public sealed partial class MeleeWeaponSystem
|
||||
Property = nameof(SpriteComponent.Offset),
|
||||
KeyFrames =
|
||||
{
|
||||
new AnimationTrackProperty.KeyFrame(startRotationOffset, 0f),
|
||||
new AnimationTrackProperty.KeyFrame(startRotationOffset, slashStart),
|
||||
new AnimationTrackProperty.KeyFrame(endRotationOffset, slashEnd)
|
||||
new AnimationTrackProperty.KeyFrame(Vector2.Lerp(startRotationOffset,endRotationOffset,0.0f), length * 0.0f),
|
||||
new AnimationTrackProperty.KeyFrame(minRotationOffset, length * 0.10f),
|
||||
new AnimationTrackProperty.KeyFrame(Vector2.Lerp(startRotationOffset,endRotationOffset,1.0f), length * 0.15f),
|
||||
new AnimationTrackProperty.KeyFrame(Vector2.Lerp(startRotationOffset,endRotationOffset,0.80f), length * 0.6f, Easings.OutQuart)
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private Animation GetThrustAnimation(Entity<SpriteComponent> sprite, float distance, Angle spriteRotation)
|
||||
private Animation GetThrustAnimation(Entity<SpriteComponent> sprite, float offset, Angle spriteRotation, float length)
|
||||
{
|
||||
const float thrustEnd = 0.05f;
|
||||
const float length = 0.15f;
|
||||
var startOffset = sprite.Comp.Rotation.RotateVec(new Vector2(0f, -distance / 5f));
|
||||
var endOffset = sprite.Comp.Rotation.RotateVec(new Vector2(0f, -distance));
|
||||
var startOffset = sprite.Comp.Rotation.RotateVec(new Vector2(0f, 0f));
|
||||
var endOffset = sprite.Comp.Rotation.RotateVec(new Vector2(0f, -offset * 1.2f));
|
||||
|
||||
_sprite.SetRotation(sprite.AsNullable(), sprite.Comp.Rotation + spriteRotation);
|
||||
|
||||
return new Animation()
|
||||
@@ -151,9 +159,11 @@ public sealed partial class MeleeWeaponSystem
|
||||
Property = nameof(SpriteComponent.Offset),
|
||||
KeyFrames =
|
||||
{
|
||||
new AnimationTrackProperty.KeyFrame(startOffset, 0f),
|
||||
new AnimationTrackProperty.KeyFrame(endOffset, thrustEnd),
|
||||
new AnimationTrackProperty.KeyFrame(endOffset, length),
|
||||
new AnimationTrackProperty.KeyFrame(Vector2.Lerp(startOffset, endOffset, 0f), length * 0f),
|
||||
new AnimationTrackProperty.KeyFrame(Vector2.Lerp(startOffset, endOffset, 0.65f), length * 0.10f),
|
||||
new AnimationTrackProperty.KeyFrame(Vector2.Lerp(startOffset, endOffset, 1f), length * 0.20f),
|
||||
new AnimationTrackProperty.KeyFrame(Vector2.Lerp(startOffset, endOffset, 0.9f), length * 0.30f),
|
||||
new AnimationTrackProperty.KeyFrame(Vector2.Lerp(startOffset, endOffset, 0.7f), length * 0.60f, Easings.OutQuart)
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -200,11 +210,12 @@ public sealed partial class MeleeWeaponSystem
|
||||
InterpolationMode = AnimationInterpolationMode.Linear,
|
||||
KeyFrames =
|
||||
{
|
||||
new AnimationTrackProperty.KeyFrame(direction.Normalized() * 0.15f, 0f),
|
||||
new AnimationTrackProperty.KeyFrame(Vector2.Zero, length)
|
||||
}
|
||||
}
|
||||
}
|
||||
new AnimationTrackProperty.KeyFrame(Vector2.Zero, 0f),
|
||||
new AnimationTrackProperty.KeyFrame(direction.Normalized() * 0.15f, length*0.4f),
|
||||
new AnimationTrackProperty.KeyFrame(Vector2.Zero, length*0.6f),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -11,17 +11,20 @@ public sealed partial class MagazineVisualsComponent : Component
|
||||
/// <summary>
|
||||
/// What RsiState we use.
|
||||
/// </summary>
|
||||
[DataField("magState")] public string? MagState;
|
||||
[DataField]
|
||||
public string? MagState;
|
||||
|
||||
/// <summary>
|
||||
/// How many steps there are
|
||||
/// </summary>
|
||||
[DataField("steps")] public int MagSteps;
|
||||
[DataField("steps")]
|
||||
public int MagSteps;
|
||||
|
||||
/// <summary>
|
||||
/// Should we hide when the count is 0
|
||||
/// </summary>
|
||||
[DataField("zeroVisible")] public bool ZeroVisible;
|
||||
[DataField]
|
||||
public bool ZeroVisible;
|
||||
}
|
||||
|
||||
public enum GunVisualLayers : byte
|
||||
|
||||
@@ -8,9 +8,10 @@ public sealed partial class SpentAmmoVisualsComponent : Component
|
||||
/// <summary>
|
||||
/// Should we do "{_state}-spent" or just "spent"
|
||||
/// </summary>
|
||||
[DataField("suffix")] public bool Suffix = true;
|
||||
[DataField]
|
||||
public bool Suffix = true;
|
||||
|
||||
[DataField("state")]
|
||||
[DataField]
|
||||
public string State = "base";
|
||||
}
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ public sealed class GunSpreadOverlay : Overlay
|
||||
if (mapPos.MapId == MapId.Nullspace)
|
||||
return;
|
||||
|
||||
if (!_guns.TryGetGun(player.Value, out var gunUid, out var gun))
|
||||
if (!_guns.TryGetGun(player.Value, out var gun))
|
||||
return;
|
||||
|
||||
var mouseScreenPos = _input.MouseScreenPosition;
|
||||
@@ -58,12 +58,12 @@ public sealed class GunSpreadOverlay : Overlay
|
||||
return;
|
||||
|
||||
// (☞゚ヮ゚)☞
|
||||
var maxSpread = gun.MaxAngleModified;
|
||||
var minSpread = gun.MinAngleModified;
|
||||
var timeSinceLastFire = (_timing.CurTime - gun.NextFire).TotalSeconds;
|
||||
var currentAngle = new Angle(MathHelper.Clamp(gun.CurrentAngle.Theta - gun.AngleDecayModified.Theta * timeSinceLastFire,
|
||||
gun.MinAngleModified.Theta, gun.MaxAngleModified.Theta));
|
||||
var direction = (mousePos.Position - mapPos.Position);
|
||||
var maxSpread = gun.Comp.MaxAngleModified;
|
||||
var minSpread = gun.Comp.MinAngleModified;
|
||||
var timeSinceLastFire = (_timing.CurTime - gun.Comp.NextFire).TotalSeconds;
|
||||
var currentAngle = new Angle(MathHelper.Clamp(gun.Comp.CurrentAngle.Theta - gun.Comp.AngleDecayModified.Theta * timeSinceLastFire,
|
||||
gun.Comp.MinAngleModified.Theta, gun.Comp.MaxAngleModified.Theta));
|
||||
var direction = mousePos.Position - mapPos.Position;
|
||||
|
||||
worldHandle.DrawLine(mapPos.Position, mousePos.Position + direction, Color.Orange);
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ public abstract class BaseBulletRenderer : Control
|
||||
{
|
||||
var countPerRow = Math.Min(Capacity, CountPerRow(availableSize.X));
|
||||
|
||||
var rows = Math.Min((int) MathF.Ceiling(Capacity / (float) countPerRow), Rows);
|
||||
var rows = Math.Min((int)MathF.Ceiling(Capacity / (float)countPerRow), Rows);
|
||||
|
||||
var height = _params.ItemHeight * rows + (_params.VerticalSeparation * rows - 1);
|
||||
var width = RowWidth(countPerRow);
|
||||
@@ -110,7 +110,7 @@ public abstract class BaseBulletRenderer : Control
|
||||
|
||||
private int CountPerRow(float width)
|
||||
{
|
||||
return (int) ((width - _params.ItemWidth + _params.ItemSeparation) / _params.ItemSeparation);
|
||||
return (int)((width - _params.ItemWidth + _params.ItemSeparation) / _params.ItemSeparation);
|
||||
}
|
||||
|
||||
private int RowWidth(int count)
|
||||
|
||||
@@ -2,10 +2,8 @@ using Content.Shared.Projectiles;
|
||||
using Content.Shared.Weapons.Ranged.Components;
|
||||
using Content.Shared.Weapons.Ranged.Systems;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
using Robust.Shared.Physics.Events;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Client.Weapons.Ranged.Systems;
|
||||
@@ -22,26 +20,26 @@ public sealed class FlyBySoundSystem : SharedFlyBySoundSystem
|
||||
SubscribeLocalEvent<FlyBySoundComponent, StartCollideEvent>(OnCollide);
|
||||
}
|
||||
|
||||
private void OnCollide(EntityUid uid, FlyBySoundComponent component, ref StartCollideEvent args)
|
||||
private void OnCollide(Entity<FlyBySoundComponent> ent, ref StartCollideEvent args)
|
||||
{
|
||||
var attachedEnt = _player.LocalEntity;
|
||||
|
||||
// If it's not our ent or we shot it.
|
||||
if (attachedEnt == null ||
|
||||
args.OtherEntity != attachedEnt ||
|
||||
TryComp<ProjectileComponent>(uid, out var projectile) &&
|
||||
TryComp<ProjectileComponent>(ent, out var projectile) &&
|
||||
projectile.Shooter == attachedEnt)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.OurFixtureId != FlyByFixture ||
|
||||
!_random.Prob(component.Prob))
|
||||
!_random.Prob(ent.Comp.Prob))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Play attached to our entity because the projectile may immediately delete or the likes.
|
||||
_audio.PlayPredicted(component.Sound, attachedEnt.Value, attachedEnt.Value);
|
||||
_audio.PlayPredicted(ent.Comp.Sound, attachedEnt.Value, attachedEnt.Value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,12 +14,12 @@ namespace Content.Client.Weapons.Ranged.Systems;
|
||||
|
||||
public sealed partial class GunSystem
|
||||
{
|
||||
private void OnAmmoCounterCollect(EntityUid uid, AmmoCounterComponent component, ItemStatusCollectMessage args)
|
||||
private void OnAmmoCounterCollect(Entity<AmmoCounterComponent> ent, ref ItemStatusCollectMessage args)
|
||||
{
|
||||
RefreshControl(uid, component);
|
||||
RefreshControl(ent);
|
||||
|
||||
if (component.Control != null)
|
||||
args.Controls.Add(component.Control);
|
||||
if (ent.Comp.Control != null)
|
||||
args.Controls.Add(ent.Comp.Control);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -27,35 +27,32 @@ public sealed partial class GunSystem
|
||||
/// </summary>
|
||||
/// <param name="uid"></param>
|
||||
/// <param name="component"></param>
|
||||
private void RefreshControl(EntityUid uid, AmmoCounterComponent? component = null)
|
||||
private void RefreshControl(Entity<AmmoCounterComponent> ent)
|
||||
{
|
||||
if (!Resolve(uid, ref component, false))
|
||||
return;
|
||||
|
||||
component.Control?.Dispose();
|
||||
component.Control = null;
|
||||
ent.Comp.Control?.Dispose();
|
||||
ent.Comp.Control = null;
|
||||
|
||||
var ev = new AmmoCounterControlEvent();
|
||||
RaiseLocalEvent(uid, ev, false);
|
||||
RaiseLocalEvent(ent, ev, false);
|
||||
|
||||
// Fallback to default if none specified
|
||||
ev.Control ??= new DefaultStatusControl();
|
||||
|
||||
component.Control = ev.Control;
|
||||
UpdateAmmoCount(uid, component);
|
||||
ent.Comp.Control = ev.Control;
|
||||
UpdateAmmoCount(ent);
|
||||
}
|
||||
|
||||
private void UpdateAmmoCount(EntityUid uid, AmmoCounterComponent component)
|
||||
private void UpdateAmmoCount(Entity<AmmoCounterComponent> ent)
|
||||
{
|
||||
if (component.Control == null)
|
||||
if (ent.Comp.Control == null)
|
||||
return;
|
||||
|
||||
var ev = new UpdateAmmoCounterEvent()
|
||||
{
|
||||
Control = component.Control
|
||||
Control = ent.Comp.Control
|
||||
};
|
||||
|
||||
RaiseLocalEvent(uid, ev, false);
|
||||
RaiseLocalEvent(ent, ev, false);
|
||||
}
|
||||
|
||||
protected override void UpdateAmmoCount(EntityUid uid, bool prediction = true)
|
||||
@@ -68,7 +65,7 @@ public sealed partial class GunSystem
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateAmmoCount(uid, clientComp);
|
||||
UpdateAmmoCount((uid, clientComp));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -12,41 +12,41 @@ public sealed partial class GunSystem
|
||||
SubscribeLocalEvent<BallisticAmmoProviderComponent, UpdateAmmoCounterEvent>(OnBallisticAmmoCount);
|
||||
}
|
||||
|
||||
private void OnBallisticAmmoCount(EntityUid uid, BallisticAmmoProviderComponent component, UpdateAmmoCounterEvent args)
|
||||
private void OnBallisticAmmoCount(Entity<BallisticAmmoProviderComponent> ent, ref UpdateAmmoCounterEvent args)
|
||||
{
|
||||
if (args.Control is DefaultStatusControl control)
|
||||
{
|
||||
control.Update(GetBallisticShots(component), component.Capacity);
|
||||
control.Update(GetBallisticShots(ent.Comp), ent.Comp.Capacity);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Cycle(EntityUid uid, BallisticAmmoProviderComponent component, MapCoordinates coordinates)
|
||||
protected override void Cycle(Entity<BallisticAmmoProviderComponent> ent, MapCoordinates coordinates)
|
||||
{
|
||||
if (!Timing.IsFirstTimePredicted)
|
||||
return;
|
||||
|
||||
EntityUid? ent = null;
|
||||
EntityUid? ammoEnt = null;
|
||||
|
||||
// TODO: Combine with TakeAmmo
|
||||
if (component.Entities.Count > 0)
|
||||
if (ent.Comp.Entities.Count > 0)
|
||||
{
|
||||
var existing = component.Entities[^1];
|
||||
component.Entities.RemoveAt(component.Entities.Count - 1);
|
||||
var existing = ent.Comp.Entities[^1];
|
||||
ent.Comp.Entities.RemoveAt(ent.Comp.Entities.Count - 1);
|
||||
|
||||
Containers.Remove(existing, component.Container);
|
||||
Containers.Remove(existing, ent.Comp.Container);
|
||||
EnsureShootable(existing);
|
||||
}
|
||||
else if (component.UnspawnedCount > 0)
|
||||
else if (ent.Comp.UnspawnedCount > 0)
|
||||
{
|
||||
component.UnspawnedCount--;
|
||||
ent = Spawn(component.Proto, coordinates);
|
||||
EnsureShootable(ent.Value);
|
||||
ent.Comp.UnspawnedCount--;
|
||||
ammoEnt = Spawn(ent.Comp.Proto, coordinates);
|
||||
EnsureShootable(ammoEnt.Value);
|
||||
}
|
||||
|
||||
if (ent != null && IsClientSide(ent.Value))
|
||||
Del(ent.Value);
|
||||
if (ammoEnt != null && IsClientSide(ammoEnt.Value))
|
||||
Del(ammoEnt.Value);
|
||||
|
||||
var cycledEvent = new GunCycledEvent();
|
||||
RaiseLocalEvent(uid, ref cycledEvent);
|
||||
RaiseLocalEvent(ent, ref cycledEvent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,11 +10,11 @@ public partial class GunSystem
|
||||
SubscribeLocalEvent<BasicEntityAmmoProviderComponent, UpdateAmmoCounterEvent>(OnBasicEntityAmmoCount);
|
||||
}
|
||||
|
||||
private void OnBasicEntityAmmoCount(EntityUid uid, BasicEntityAmmoProviderComponent component, UpdateAmmoCounterEvent args)
|
||||
private void OnBasicEntityAmmoCount(Entity<BasicEntityAmmoProviderComponent> ent, ref UpdateAmmoCounterEvent args)
|
||||
{
|
||||
if (args.Control is DefaultStatusControl control && component.Count != null && component.Capacity != null)
|
||||
if (args.Control is DefaultStatusControl control && ent.Comp.Count != null && ent.Comp.Capacity != null)
|
||||
{
|
||||
control.Update(component.Count.Value, component.Capacity.Value);
|
||||
control.Update(ent.Comp.Count.Value, ent.Comp.Capacity.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,11 +18,11 @@ public sealed partial class GunSystem
|
||||
SubscribeLocalEvent<ChamberMagazineAmmoProviderComponent, AppearanceChangeEvent>(OnChamberMagazineAppearance);
|
||||
}
|
||||
|
||||
private void OnChamberMagazineAppearance(EntityUid uid, ChamberMagazineAmmoProviderComponent component, ref AppearanceChangeEvent args)
|
||||
private void OnChamberMagazineAppearance(Entity<ChamberMagazineAmmoProviderComponent> ent, ref AppearanceChangeEvent args)
|
||||
{
|
||||
if (args.Sprite == null ||
|
||||
!_sprite.LayerMapTryGet((uid, args.Sprite), GunVisualLayers.Base, out var boltLayer, false) ||
|
||||
!Appearance.TryGetData(uid, AmmoVisuals.BoltClosed, out bool boltClosed))
|
||||
!_sprite.LayerMapTryGet((ent, args.Sprite), GunVisualLayers.Base, out var boltLayer, false) ||
|
||||
!Appearance.TryGetData(ent, AmmoVisuals.BoltClosed, out bool boltClosed))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -30,11 +30,11 @@ public sealed partial class GunSystem
|
||||
// Maybe re-using base layer for this will bite me someday but screw you future sloth.
|
||||
if (boltClosed)
|
||||
{
|
||||
_sprite.LayerSetRsiState((uid, args.Sprite), boltLayer, "base");
|
||||
_sprite.LayerSetRsiState((ent, args.Sprite), boltLayer, "base");
|
||||
}
|
||||
else
|
||||
{
|
||||
_sprite.LayerSetRsiState((uid, args.Sprite), boltLayer, "bolt-open");
|
||||
_sprite.LayerSetRsiState((ent, args.Sprite), boltLayer, "bolt-open");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,17 +55,17 @@ public sealed partial class GunSystem
|
||||
// to avoid 6-7 additional entity spawns.
|
||||
}
|
||||
|
||||
private void OnChamberMagazineCounter(EntityUid uid, ChamberMagazineAmmoProviderComponent component, AmmoCounterControlEvent args)
|
||||
private void OnChamberMagazineCounter(Entity<ChamberMagazineAmmoProviderComponent> ent, ref AmmoCounterControlEvent args)
|
||||
{
|
||||
args.Control = new ChamberMagazineStatusControl();
|
||||
}
|
||||
|
||||
private void OnChamberMagazineAmmoUpdate(EntityUid uid, ChamberMagazineAmmoProviderComponent component, UpdateAmmoCounterEvent args)
|
||||
private void OnChamberMagazineAmmoUpdate(Entity<ChamberMagazineAmmoProviderComponent> ent, ref UpdateAmmoCounterEvent args)
|
||||
{
|
||||
if (args.Control is not ChamberMagazineStatusControl control) return;
|
||||
|
||||
var chambered = GetChamberEntity(uid);
|
||||
var magEntity = GetMagazineEntity(uid);
|
||||
var chambered = GetChamberEntity(ent);
|
||||
var magEntity = GetMagazineEntity(ent);
|
||||
var ammoCountEv = new GetAmmoCountEvent();
|
||||
|
||||
if (magEntity != null)
|
||||
|
||||
@@ -11,11 +11,11 @@ public sealed partial class GunSystem
|
||||
SubscribeLocalEvent<MagazineAmmoProviderComponent, AmmoCounterControlEvent>(OnMagazineControl);
|
||||
}
|
||||
|
||||
private void OnMagazineAmmoUpdate(EntityUid uid, MagazineAmmoProviderComponent component, UpdateAmmoCounterEvent args)
|
||||
private void OnMagazineAmmoUpdate(Entity<MagazineAmmoProviderComponent> ent, ref UpdateAmmoCounterEvent args)
|
||||
{
|
||||
var ent = GetMagazineEntity(uid);
|
||||
var magEnt = GetMagazineEntity(ent);
|
||||
|
||||
if (ent == null)
|
||||
if (magEnt == null)
|
||||
{
|
||||
if (args.Control is DefaultStatusControl control)
|
||||
{
|
||||
@@ -25,14 +25,14 @@ public sealed partial class GunSystem
|
||||
return;
|
||||
}
|
||||
|
||||
RaiseLocalEvent(ent.Value, args, false);
|
||||
RaiseLocalEvent(magEnt.Value, args, false);
|
||||
}
|
||||
|
||||
private void OnMagazineControl(EntityUid uid, MagazineAmmoProviderComponent component, AmmoCounterControlEvent args)
|
||||
private void OnMagazineControl(Entity<MagazineAmmoProviderComponent> ent, ref AmmoCounterControlEvent args)
|
||||
{
|
||||
var ent = GetMagazineEntity(uid);
|
||||
if (ent == null)
|
||||
var magEnt = GetMagazineEntity(ent);
|
||||
if (magEnt == null)
|
||||
return;
|
||||
RaiseLocalEvent(ent.Value, args, false);
|
||||
RaiseLocalEvent(magEnt.Value, args, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,24 +13,24 @@ public sealed partial class GunSystem
|
||||
SubscribeLocalEvent<MagazineVisualsComponent, AppearanceChangeEvent>(OnMagazineVisualsChange);
|
||||
}
|
||||
|
||||
private void OnMagazineVisualsInit(EntityUid uid, MagazineVisualsComponent component, ComponentInit args)
|
||||
private void OnMagazineVisualsInit(Entity<MagazineVisualsComponent> ent, ref ComponentInit args)
|
||||
{
|
||||
if (!TryComp<SpriteComponent>(uid, out var sprite)) return;
|
||||
if (!TryComp<SpriteComponent>(ent, out var sprite)) return;
|
||||
|
||||
if (_sprite.LayerMapTryGet((uid, sprite), GunVisualLayers.Mag, out _, false))
|
||||
if (_sprite.LayerMapTryGet((ent, sprite), GunVisualLayers.Mag, out _, false))
|
||||
{
|
||||
_sprite.LayerSetRsiState((uid, sprite), GunVisualLayers.Mag, $"{component.MagState}-{component.MagSteps - 1}");
|
||||
_sprite.LayerSetVisible((uid, sprite), GunVisualLayers.Mag, false);
|
||||
_sprite.LayerSetRsiState((ent, sprite), GunVisualLayers.Mag, $"{ent.Comp.MagState}-{ent.Comp.MagSteps - 1}");
|
||||
_sprite.LayerSetVisible((ent, sprite), GunVisualLayers.Mag, false);
|
||||
}
|
||||
|
||||
if (_sprite.LayerMapTryGet((uid, sprite), GunVisualLayers.MagUnshaded, out _, false))
|
||||
if (_sprite.LayerMapTryGet((ent, sprite), GunVisualLayers.MagUnshaded, out _, false))
|
||||
{
|
||||
_sprite.LayerSetRsiState((uid, sprite), GunVisualLayers.MagUnshaded, $"{component.MagState}-unshaded-{component.MagSteps - 1}");
|
||||
_sprite.LayerSetVisible((uid, sprite), GunVisualLayers.MagUnshaded, false);
|
||||
_sprite.LayerSetRsiState((ent, sprite), GunVisualLayers.MagUnshaded, $"{ent.Comp.MagState}-unshaded-{ent.Comp.MagSteps - 1}");
|
||||
_sprite.LayerSetVisible((ent, sprite), GunVisualLayers.MagUnshaded, false);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnMagazineVisualsChange(EntityUid uid, MagazineVisualsComponent component, ref AppearanceChangeEvent args)
|
||||
private void OnMagazineVisualsChange(Entity<MagazineVisualsComponent> ent, ref AppearanceChangeEvent args)
|
||||
{
|
||||
// tl;dr
|
||||
// 1.If no mag then hide it OR
|
||||
@@ -45,53 +45,53 @@ public sealed partial class GunSystem
|
||||
{
|
||||
if (!args.AppearanceData.TryGetValue(AmmoVisuals.AmmoMax, out var capacity))
|
||||
{
|
||||
capacity = component.MagSteps;
|
||||
capacity = ent.Comp.MagSteps;
|
||||
}
|
||||
|
||||
if (!args.AppearanceData.TryGetValue(AmmoVisuals.AmmoCount, out var current))
|
||||
{
|
||||
current = component.MagSteps;
|
||||
current = ent.Comp.MagSteps;
|
||||
}
|
||||
|
||||
var step = ContentHelpers.RoundToLevels((int)current, (int)capacity, component.MagSteps);
|
||||
var step = ContentHelpers.RoundToLevels((int)current, (int)capacity, ent.Comp.MagSteps);
|
||||
|
||||
if (step == 0 && !component.ZeroVisible)
|
||||
if (step == 0 && !ent.Comp.ZeroVisible)
|
||||
{
|
||||
if (_sprite.LayerMapTryGet((uid, sprite), GunVisualLayers.Mag, out _, false))
|
||||
if (_sprite.LayerMapTryGet((ent, sprite), GunVisualLayers.Mag, out _, false))
|
||||
{
|
||||
_sprite.LayerSetVisible((uid, sprite), GunVisualLayers.Mag, false);
|
||||
_sprite.LayerSetVisible((ent, sprite), GunVisualLayers.Mag, false);
|
||||
}
|
||||
|
||||
if (_sprite.LayerMapTryGet((uid, sprite), GunVisualLayers.MagUnshaded, out _, false))
|
||||
if (_sprite.LayerMapTryGet((ent, sprite), GunVisualLayers.MagUnshaded, out _, false))
|
||||
{
|
||||
_sprite.LayerSetVisible((uid, sprite), GunVisualLayers.MagUnshaded, false);
|
||||
_sprite.LayerSetVisible((ent, sprite), GunVisualLayers.MagUnshaded, false);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (_sprite.LayerMapTryGet((uid, sprite), GunVisualLayers.Mag, out _, false))
|
||||
if (_sprite.LayerMapTryGet((ent, sprite), GunVisualLayers.Mag, out _, false))
|
||||
{
|
||||
_sprite.LayerSetVisible((uid, sprite), GunVisualLayers.Mag, true);
|
||||
_sprite.LayerSetRsiState((uid, sprite), GunVisualLayers.Mag, $"{component.MagState}-{step}");
|
||||
_sprite.LayerSetVisible((ent, sprite), GunVisualLayers.Mag, true);
|
||||
_sprite.LayerSetRsiState((ent, sprite), GunVisualLayers.Mag, $"{ent.Comp.MagState}-{step}");
|
||||
}
|
||||
|
||||
if (_sprite.LayerMapTryGet((uid, sprite), GunVisualLayers.MagUnshaded, out _, false))
|
||||
if (_sprite.LayerMapTryGet((ent, sprite), GunVisualLayers.MagUnshaded, out _, false))
|
||||
{
|
||||
_sprite.LayerSetVisible((uid, sprite), GunVisualLayers.MagUnshaded, true);
|
||||
_sprite.LayerSetRsiState((uid, sprite), GunVisualLayers.MagUnshaded, $"{component.MagState}-unshaded-{step}");
|
||||
_sprite.LayerSetVisible((ent, sprite), GunVisualLayers.MagUnshaded, true);
|
||||
_sprite.LayerSetRsiState((ent, sprite), GunVisualLayers.MagUnshaded, $"{ent.Comp.MagState}-unshaded-{step}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_sprite.LayerMapTryGet((uid, sprite), GunVisualLayers.Mag, out _, false))
|
||||
if (_sprite.LayerMapTryGet((ent, sprite), GunVisualLayers.Mag, out _, false))
|
||||
{
|
||||
_sprite.LayerSetVisible((uid, sprite), GunVisualLayers.Mag, false);
|
||||
_sprite.LayerSetVisible((ent, sprite), GunVisualLayers.Mag, false);
|
||||
}
|
||||
|
||||
if (_sprite.LayerMapTryGet((uid, sprite), GunVisualLayers.MagUnshaded, out _, false))
|
||||
if (_sprite.LayerMapTryGet((ent, sprite), GunVisualLayers.MagUnshaded, out _, false))
|
||||
{
|
||||
_sprite.LayerSetVisible((uid, sprite), GunVisualLayers.MagUnshaded, false);
|
||||
_sprite.LayerSetVisible((ent, sprite), GunVisualLayers.MagUnshaded, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,25 +14,25 @@ public sealed partial class GunSystem
|
||||
SubscribeLocalEvent<RevolverAmmoProviderComponent, EntRemovedFromContainerMessage>(OnRevolverEntRemove);
|
||||
}
|
||||
|
||||
private void OnRevolverEntRemove(EntityUid uid, RevolverAmmoProviderComponent component, EntRemovedFromContainerMessage args)
|
||||
private void OnRevolverEntRemove(Entity<RevolverAmmoProviderComponent> ent, ref EntRemovedFromContainerMessage args)
|
||||
{
|
||||
if (args.Container.ID != RevolverContainer)
|
||||
return;
|
||||
|
||||
// See ChamberMagazineAmmoProvider
|
||||
// <See ChamberMagazineAmmoProvider>
|
||||
if (!IsClientSide(args.Entity))
|
||||
return;
|
||||
|
||||
QueueDel(args.Entity);
|
||||
}
|
||||
|
||||
private void OnRevolverAmmoUpdate(EntityUid uid, RevolverAmmoProviderComponent component, UpdateAmmoCounterEvent args)
|
||||
private void OnRevolverAmmoUpdate(Entity<RevolverAmmoProviderComponent> ent, ref UpdateAmmoCounterEvent args)
|
||||
{
|
||||
if (args.Control is not RevolverStatusControl control) return;
|
||||
control.Update(component.CurrentIndex, component.Chambers);
|
||||
control.Update(ent.Comp.CurrentIndex, ent.Comp.Chambers);
|
||||
}
|
||||
|
||||
private void OnRevolverCounter(EntityUid uid, RevolverAmmoProviderComponent component, AmmoCounterControlEvent args)
|
||||
private void OnRevolverCounter(Entity<RevolverAmmoProviderComponent> ent, ref AmmoCounterControlEvent args)
|
||||
{
|
||||
args.Control = new RevolverStatusControl();
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ public sealed partial class GunSystem
|
||||
SubscribeLocalEvent<SpentAmmoVisualsComponent, AppearanceChangeEvent>(OnSpentAmmoAppearance);
|
||||
}
|
||||
|
||||
private void OnSpentAmmoAppearance(EntityUid uid, SpentAmmoVisualsComponent component, ref AppearanceChangeEvent args)
|
||||
private void OnSpentAmmoAppearance(Entity<SpentAmmoVisualsComponent> ent, ref AppearanceChangeEvent args)
|
||||
{
|
||||
var sprite = args.Sprite;
|
||||
if (sprite == null) return;
|
||||
@@ -21,15 +21,15 @@ public sealed partial class GunSystem
|
||||
return;
|
||||
}
|
||||
|
||||
var spent = (bool) varSpent;
|
||||
var spent = (bool)varSpent;
|
||||
string state;
|
||||
|
||||
if (spent)
|
||||
state = component.Suffix ? $"{component.State}-spent" : "spent";
|
||||
state = ent.Comp.Suffix ? $"{ent.Comp.State}-spent" : "spent";
|
||||
else
|
||||
state = component.State;
|
||||
state = ent.Comp.State;
|
||||
|
||||
_sprite.LayerSetRsiState((uid, sprite), AmmoVisualLayers.Base, state);
|
||||
_sprite.RemoveLayer((uid, sprite), AmmoVisualLayers.Tip, false);
|
||||
_sprite.LayerSetRsiState((ent, sprite), AmmoVisualLayers.Base, state);
|
||||
_sprite.RemoveLayer((ent, sprite), AmmoVisualLayers.Tip, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,13 +31,13 @@ namespace Content.Client.Weapons.Ranged.Systems;
|
||||
|
||||
public sealed partial class GunSystem : SharedGunSystem
|
||||
{
|
||||
[Dependency] private readonly AnimationPlayerSystem _animPlayer = default!;
|
||||
[Dependency] private readonly IEyeManager _eyeManager = default!;
|
||||
[Dependency] private readonly IInputManager _inputManager = default!;
|
||||
[Dependency] private readonly InputSystem _inputSystem = default!;
|
||||
[Dependency] private readonly IOverlayManager _overlayManager = default!;
|
||||
[Dependency] private readonly IPlayerManager _player = default!;
|
||||
[Dependency] private readonly IStateManager _state = default!;
|
||||
[Dependency] private readonly AnimationPlayerSystem _animPlayer = default!;
|
||||
[Dependency] private readonly InputSystem _inputSystem = default!;
|
||||
[Dependency] private readonly SharedCameraRecoilSystem _recoil = default!;
|
||||
[Dependency] private readonly SharedMapSystem _maps = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _xform = default!;
|
||||
@@ -167,29 +167,29 @@ public sealed partial class GunSystem : SharedGunSystem
|
||||
|
||||
var entity = entityNull.Value;
|
||||
|
||||
if (!TryGetGun(entity, out var gunUid, out var gun))
|
||||
if (!TryGetGun(entity, out var gun))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var useKey = gun.UseKey ? EngineKeyFunctions.Use : EngineKeyFunctions.UseSecondary;
|
||||
var useKey = gun.Comp.UseKey ? EngineKeyFunctions.Use : EngineKeyFunctions.UseSecondary;
|
||||
|
||||
if (_inputSystem.CmdStates.GetState(useKey) != BoundKeyState.Down && !gun.BurstActivated)
|
||||
if (_inputSystem.CmdStates.GetState(useKey) != BoundKeyState.Down && !gun.Comp.BurstActivated)
|
||||
{
|
||||
if (gun.ShotCounter != 0)
|
||||
RaisePredictiveEvent(new RequestStopShootEvent { Gun = GetNetEntity(gunUid) });
|
||||
if (gun.Comp.ShotCounter != 0)
|
||||
RaisePredictiveEvent(new RequestStopShootEvent { Gun = GetNetEntity(gun) });
|
||||
return;
|
||||
}
|
||||
|
||||
if (gun.NextFire > Timing.CurTime)
|
||||
if (gun.Comp.NextFire > Timing.CurTime)
|
||||
return;
|
||||
|
||||
var mousePos = _eyeManager.PixelToMap(_inputManager.MouseScreenPosition);
|
||||
|
||||
if (mousePos.MapId == MapId.Nullspace)
|
||||
{
|
||||
if (gun.ShotCounter != 0)
|
||||
RaisePredictiveEvent(new RequestStopShootEvent { Gun = GetNetEntity(gunUid) });
|
||||
if (gun.Comp.ShotCounter != 0)
|
||||
RaisePredictiveEvent(new RequestStopShootEvent { Gun = GetNetEntity(gun) });
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -207,11 +207,11 @@ public sealed partial class GunSystem : SharedGunSystem
|
||||
{
|
||||
Target = target,
|
||||
Coordinates = GetNetCoordinates(coordinates),
|
||||
Gun = GetNetEntity(gunUid),
|
||||
Gun = GetNetEntity(gun),
|
||||
});
|
||||
}
|
||||
|
||||
public override void Shoot(EntityUid gunUid, GunComponent gun, List<(EntityUid? Entity, IShootable Shootable)> ammo,
|
||||
public override void Shoot(Entity<GunComponent> gun, List<(EntityUid? Entity, IShootable Shootable)> ammo,
|
||||
EntityCoordinates fromCoordinates, EntityCoordinates toCoordinates, out bool userImpulse, EntityUid? user = null, bool throwItems = false)
|
||||
{
|
||||
userImpulse = true;
|
||||
@@ -226,7 +226,7 @@ public sealed partial class GunSystem : SharedGunSystem
|
||||
{
|
||||
if (throwItems)
|
||||
{
|
||||
Recoil(user, direction, gun.CameraRecoilScalarModified);
|
||||
Recoil(user, direction, gun.Comp.CameraRecoilScalarModified);
|
||||
if (IsClientSide(ent!.Value))
|
||||
Del(ent.Value);
|
||||
else
|
||||
@@ -241,9 +241,9 @@ public sealed partial class GunSystem : SharedGunSystem
|
||||
if (!cartridge.Spent)
|
||||
{
|
||||
SetCartridgeSpent(ent!.Value, cartridge, true);
|
||||
MuzzleFlash(gunUid, cartridge, worldAngle, user);
|
||||
Audio.PlayPredicted(gun.SoundGunshotModified, gunUid, user);
|
||||
Recoil(user, direction, gun.CameraRecoilScalarModified);
|
||||
MuzzleFlash(gun, cartridge, worldAngle, user);
|
||||
Audio.PlayPredicted(gun.Comp.SoundGunshotModified, gun, user);
|
||||
Recoil(user, direction, gun.Comp.CameraRecoilScalarModified);
|
||||
// TODO: Can't predict entity deletions.
|
||||
//if (cartridge.DeleteOnSpawn)
|
||||
// Del(cartridge.Owner);
|
||||
@@ -251,7 +251,7 @@ public sealed partial class GunSystem : SharedGunSystem
|
||||
else
|
||||
{
|
||||
userImpulse = false;
|
||||
Audio.PlayPredicted(gun.SoundEmpty, gunUid, user);
|
||||
Audio.PlayPredicted(gun.Comp.SoundEmpty, gun, user);
|
||||
}
|
||||
|
||||
if (IsClientSide(ent!.Value))
|
||||
@@ -259,17 +259,17 @@ public sealed partial class GunSystem : SharedGunSystem
|
||||
|
||||
break;
|
||||
case AmmoComponent newAmmo:
|
||||
MuzzleFlash(gunUid, newAmmo, worldAngle, user);
|
||||
Audio.PlayPredicted(gun.SoundGunshotModified, gunUid, user);
|
||||
Recoil(user, direction, gun.CameraRecoilScalarModified);
|
||||
MuzzleFlash(gun, newAmmo, worldAngle, user);
|
||||
Audio.PlayPredicted(gun.Comp.SoundGunshotModified, gun, user);
|
||||
Recoil(user, direction, gun.Comp.CameraRecoilScalarModified);
|
||||
if (IsClientSide(ent!.Value))
|
||||
Del(ent.Value);
|
||||
else
|
||||
RemoveShootable(ent.Value);
|
||||
break;
|
||||
case HitscanAmmoComponent:
|
||||
Audio.PlayPredicted(gun.SoundGunshotModified, gunUid, user);
|
||||
Recoil(user, direction, gun.CameraRecoilScalarModified);
|
||||
Audio.PlayPredicted(gun.Comp.SoundGunshotModified, gun, user);
|
||||
Recoil(user, direction, gun.Comp.CameraRecoilScalarModified);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -407,5 +407,5 @@ public sealed partial class GunSystem : SharedGunSystem
|
||||
}
|
||||
|
||||
// TODO: Move RangedDamageSoundComponent to shared so this can be predicted.
|
||||
public override void PlayImpactSound(EntityUid otherEntity, DamageSpecifier? modifiedDamage, SoundSpecifier? weaponSound, bool forceWeaponSound) {}
|
||||
public override void PlayImpactSound(EntityUid otherEntity, DamageSpecifier? modifiedDamage, SoundSpecifier? weaponSound, bool forceWeaponSound) { }
|
||||
}
|
||||
|
||||
585
Content.IntegrationTests/Tests/Atmos/AirtightTest.cs
Normal file
585
Content.IntegrationTests/Tests/Atmos/AirtightTest.cs
Normal file
@@ -0,0 +1,585 @@
|
||||
using System.Numerics;
|
||||
using Content.Server.Atmos.Components;
|
||||
using Content.Server.Atmos.EntitySystems;
|
||||
using Content.Shared.Atmos;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.Atmos;
|
||||
|
||||
/// <summary>
|
||||
/// Mega-testclass for testing <see cref="AirtightSystem"/> and <see cref="AirtightComponent"/>.
|
||||
/// </summary>
|
||||
[TestOf(typeof(AirtightSystem))]
|
||||
[TestOf(typeof(AtmosphereSystem))]
|
||||
public sealed class AirtightTest : AtmosTest
|
||||
{
|
||||
// Load the same DeltaPressure test because it's quite a useful testmap for testing airtightness.
|
||||
protected override ResPath? TestMapPath => new("Maps/Test/Atmospherics/DeltaPressure/deltapressuretest.yml");
|
||||
|
||||
private readonly EntProtoId _wallProto = new("WallSolid");
|
||||
|
||||
private EntityUid _targetWall = EntityUid.Invalid;
|
||||
private EntityUid _targetRotationEnt = EntityUid.Invalid;
|
||||
|
||||
#region Prototypes
|
||||
|
||||
[TestPrototypes]
|
||||
private const string Prototypes = @"
|
||||
- type: entity
|
||||
id: AirtightDirectionalRotationTest
|
||||
parent: WindowDirectional
|
||||
components:
|
||||
- type: Airtight
|
||||
airBlockedDirection: North
|
||||
fixAirBlockedDirectionInitialize: true
|
||||
noAirWhenFullyAirBlocked: false
|
||||
";
|
||||
|
||||
#endregion
|
||||
|
||||
#region Component and Helper Assertions
|
||||
|
||||
/*
|
||||
Tests for asserting that proper ComponentInit and other events properly work.
|
||||
*/
|
||||
|
||||
[Test]
|
||||
public async Task Component_InitDataCorrect()
|
||||
{
|
||||
// Ensure grid/atmos is initialized.
|
||||
SAtmos.RunProcessingFull(ProcessEnt, MapData.Grid.Owner, SAtmos.AtmosTickRate);
|
||||
|
||||
await Server.WaitPost(delegate
|
||||
{
|
||||
var coords = new EntityCoordinates(RelevantAtmos.Owner, Vector2.Zero);
|
||||
_targetWall = SEntMan.SpawnAtPosition(_wallProto, coords);
|
||||
});
|
||||
|
||||
SEntMan.TryGetComponent<AirtightComponent>(_targetWall, out var airtightComp);
|
||||
Assert.That(airtightComp, Is.Not.Null, "Expected spawned wall entity to have AirtightComponent.");
|
||||
|
||||
// The data on the component itself should reflect full blockage.
|
||||
// It should also hold the proper last position.
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(airtightComp.AirBlockedDirection, Is.EqualTo(AtmosDirection.All));
|
||||
Assert.That(airtightComp.LastPosition, Is.EqualTo((RelevantAtmos.Owner, Vector2i.Zero)));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCase(AtmosDirection.North)]
|
||||
[TestCase(AtmosDirection.South)]
|
||||
[TestCase(AtmosDirection.East)]
|
||||
[TestCase(AtmosDirection.West)]
|
||||
public async Task MultiTile_Component_InitDataCorrect(AtmosDirection direction)
|
||||
{
|
||||
// Ensure grid/atmos is initialized.
|
||||
SAtmos.RunProcessingFull(ProcessEnt, MapData.Grid.Owner, SAtmos.AtmosTickRate);
|
||||
|
||||
var offsetVec = Vector2i.Zero.Offset(direction);
|
||||
await Server.WaitPost(delegate
|
||||
{
|
||||
var coords = new EntityCoordinates(RelevantAtmos.Owner, offsetVec);
|
||||
_targetWall = SEntMan.SpawnAtPosition(_wallProto, coords);
|
||||
});
|
||||
|
||||
SEntMan.TryGetComponent<AirtightComponent>(_targetWall, out var airtightComp);
|
||||
Assert.That(airtightComp, Is.Not.Null, "Expected spawned wall entity to have AirtightComponent.");
|
||||
|
||||
// The data on the component itself should reflect full blockage.
|
||||
// It should also hold the proper last position.
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(airtightComp.AirBlockedDirection, Is.EqualTo(AtmosDirection.All));
|
||||
Assert.That(airtightComp.LastPosition, Is.EqualTo((RelevantAtmos.Owner, offsetVec)));
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Single Tile Assertion
|
||||
|
||||
/*
|
||||
Tests for asserting single tile airtightness state on both reconstructed and cached data.
|
||||
These tests just spawn a wall in the center and make sure that both reconstructed and cached
|
||||
airtight data reflect the expected states both immediately after the action and after an atmos tick.
|
||||
*/
|
||||
|
||||
/// <summary>
|
||||
/// Tests that the reconstructed airtight map reflects properly when an airtight entity is spawned.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task Spawn_ReconstructedUpdatesImmediately()
|
||||
{
|
||||
// Ensure grid/atmos is initialized.
|
||||
SAtmos.RunProcessingFull(ProcessEnt, MapData.Grid.Owner, SAtmos.AtmosTickRate);
|
||||
|
||||
// Before an entity is spawned, the tile in question should be completely unblocked.
|
||||
// This should be reflected in a reconstruction.
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(
|
||||
SAtmos.IsTileAirBlocked(ProcessEnt.Owner, Vector2i.Zero, mapGridComp: ProcessEnt.Comp3),
|
||||
Is.False,
|
||||
"Expected no airtightness for reconstructed AirtightData before spawning an airtight entity.");
|
||||
}
|
||||
|
||||
// We cannot use the Spawn InteractionTest helper because it runs ticks,
|
||||
// which invalidate testing for cached data (ticks would update the cache).
|
||||
await Server.WaitPost(delegate
|
||||
{
|
||||
var coords = new EntityCoordinates(RelevantAtmos.Owner, Vector2.Zero);
|
||||
_targetWall = SEntMan.SpawnAtPosition(_wallProto, coords);
|
||||
});
|
||||
|
||||
// Now, immediately after spawn, the reconstructed data should reflect airtightness.
|
||||
Assert.That(
|
||||
SAtmos.IsTileAirBlocked(ProcessEnt.Owner, Vector2i.Zero, mapGridComp: ProcessEnt.Comp3),
|
||||
Is.True,
|
||||
"Expected airtightness for reconstructed AirtightData immediately after spawn.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that the AirtightData cache updates properly when an airtight entity is spawned.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task Spawn_CacheUpdatesOnAtmosTick()
|
||||
{
|
||||
// Ensure grid/atmos is initialized.
|
||||
SAtmos.RunProcessingFull(ProcessEnt, MapData.Grid.Owner, SAtmos.AtmosTickRate);
|
||||
|
||||
// Space should be blank before spawn.
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(
|
||||
SAtmos.IsTileAirBlockedCached(RelevantAtmos, Vector2i.Zero),
|
||||
Is.False,
|
||||
"Expected cached AirtightData to be unblocked before spawning an airtight entity.");
|
||||
|
||||
var tile = RelevantAtmos.Comp.Tiles[Vector2i.Zero];
|
||||
Assert.That(tile.AdjacentBits,
|
||||
Is.EqualTo(AtmosDirection.All),
|
||||
"Expected tile to be completely unblocked before spawning an airtight entity.");
|
||||
|
||||
Assert.That(tile.AirtightData.BlockedDirections,
|
||||
Is.EqualTo(AtmosDirection.Invalid),
|
||||
"Expected AirtightData to reflect non-airtight state before spawning an airtight entity.");
|
||||
|
||||
for (var i = 0; i < Atmospherics.Directions; i++)
|
||||
{
|
||||
var direction = (AtmosDirection)(1 << i);
|
||||
var curTile = tile.AdjacentTiles[i];
|
||||
Assert.That(curTile, Is.Not.Null, $"Center tile does not hold expected reference to adjacent tile in direction {direction}.");
|
||||
}
|
||||
}
|
||||
|
||||
await Server.WaitPost(delegate
|
||||
{
|
||||
var coords = new EntityCoordinates(RelevantAtmos.Owner, Vector2.Zero);
|
||||
_targetWall = SEntMan.SpawnAtPosition(_wallProto, coords);
|
||||
});
|
||||
|
||||
// Now, immediately after spawn, the reconstructed data should reflect airtightness,
|
||||
// but the cached data should still be stale.
|
||||
// This goes the same for the references, which haven't been updated, as well as the AirtightData.
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(
|
||||
SAtmos.IsTileAirBlockedCached(RelevantAtmos, Vector2i.Zero),
|
||||
Is.False,
|
||||
"Expected cached AirtightData to remain stale immediately after spawn before atmos tick.");
|
||||
|
||||
var tile = RelevantAtmos.Comp.Tiles[Vector2i.Zero];
|
||||
Assert.That(tile.AdjacentBits,
|
||||
Is.EqualTo(AtmosDirection.All),
|
||||
"Expected tile to still show non-airtight state before an atmos tick.");
|
||||
|
||||
Assert.That(tile.AirtightData.BlockedDirections,
|
||||
Is.EqualTo(AtmosDirection.Invalid),
|
||||
"Expected AirtightData to reflect non-airtight state after spawn before an atmos tick.");
|
||||
|
||||
for (var i = 0; i < Atmospherics.Directions; i++)
|
||||
{
|
||||
var direction = (AtmosDirection)(1 << i);
|
||||
var curTile = tile.AdjacentTiles[i];
|
||||
Assert.That(curTile, Is.Not.Null, $"Center tile does not hold expected reference to adjacent tile in direction {direction}.");
|
||||
}
|
||||
}
|
||||
|
||||
// Tick to update cache.
|
||||
SAtmos.RunProcessingFull(ProcessEnt, MapData.Grid.Owner, SAtmos.AtmosTickRate);
|
||||
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(
|
||||
SAtmos.IsTileAirBlocked(ProcessEnt.Owner, Vector2i.Zero, mapGridComp: ProcessEnt.Comp3),
|
||||
Is.True,
|
||||
"Expected airtightness for reconstructed AirtightData after atmos tick.");
|
||||
|
||||
Assert.That(
|
||||
SAtmos.IsTileAirBlockedCached(RelevantAtmos, Vector2i.Zero),
|
||||
Is.True,
|
||||
"Expected cached AirtightData to reflect airtightness after atmos tick.");
|
||||
|
||||
var tile = RelevantAtmos.Comp.Tiles[Vector2i.Zero];
|
||||
Assert.That(tile.AdjacentBits,
|
||||
Is.EqualTo(AtmosDirection.Invalid),
|
||||
"Expected tile to reflect airtight state after atmos tick.");
|
||||
|
||||
Assert.That(tile.AirtightData.BlockedDirections,
|
||||
Is.EqualTo(AtmosDirection.All),
|
||||
"Expected AirtightData to reflect airtight state after spawn before an atmos tick.");
|
||||
|
||||
for (var i = 0; i < Atmospherics.Directions; i++)
|
||||
{
|
||||
var direction = (AtmosDirection)(1 << i);
|
||||
var curTile = tile.AdjacentTiles[i];
|
||||
Assert.That(curTile, Is.Null, $"Center tile holds unexpected reference to adjacent tile in direction {direction}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that an airtight reconstruction reflects properly after an entity is deleted.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task Delete_ReconstructedUpdatesImmediately()
|
||||
{
|
||||
// Ensure grid/atmos is initialized.
|
||||
SAtmos.RunProcessingFull(ProcessEnt, MapData.Grid.Owner, SAtmos.AtmosTickRate);
|
||||
|
||||
await Server.WaitPost(delegate
|
||||
{
|
||||
var coords = new EntityCoordinates(RelevantAtmos.Owner, Vector2.Zero);
|
||||
_targetWall = SEntMan.SpawnAtPosition(_wallProto, coords);
|
||||
});
|
||||
|
||||
SAtmos.RunProcessingFull(ProcessEnt, MapData.Grid.Owner, SAtmos.AtmosTickRate);
|
||||
|
||||
Assert.That(
|
||||
SAtmos.IsTileAirBlocked(ProcessEnt.Owner, Vector2i.Zero, mapGridComp: ProcessEnt.Comp3),
|
||||
Is.True,
|
||||
"Expected airtightness for reconstructed AirtightData before deletion.");
|
||||
|
||||
await Server.WaitPost(delegate
|
||||
{
|
||||
SEntMan.DeleteEntity(_targetWall);
|
||||
});
|
||||
|
||||
Assert.That(
|
||||
SAtmos.IsTileAirBlocked(ProcessEnt.Owner, Vector2i.Zero, mapGridComp: ProcessEnt.Comp3),
|
||||
Is.False,
|
||||
"Expected no airtightness for reconstructed AirtightData immediately after deletion.");
|
||||
|
||||
SAtmos.RunProcessingFull(ProcessEnt, MapData.Grid.Owner, SAtmos.AtmosTickRate);
|
||||
|
||||
Assert.That(
|
||||
SAtmos.IsTileAirBlocked(ProcessEnt.Owner, Vector2i.Zero, mapGridComp: ProcessEnt.Comp3),
|
||||
Is.False,
|
||||
"Expected no airtightness for reconstructed AirtightData after atmos tick.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that the cached airtight map reflects properly when an entity is deleted
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task Delete_CacheUpdatesOnAtmosTick()
|
||||
{
|
||||
// Ensure grid/atmos is initialized.
|
||||
SAtmos.RunProcessingFull(ProcessEnt, MapData.Grid.Owner, SAtmos.AtmosTickRate);
|
||||
|
||||
await Server.WaitPost(delegate
|
||||
{
|
||||
var coords = new EntityCoordinates(RelevantAtmos.Owner, Vector2.Zero);
|
||||
_targetWall = SEntMan.SpawnAtPosition(_wallProto, coords);
|
||||
});
|
||||
|
||||
SAtmos.RunProcessingFull(ProcessEnt, MapData.Grid.Owner, SAtmos.AtmosTickRate);
|
||||
|
||||
await Server.WaitPost(delegate
|
||||
{
|
||||
SEntMan.DeleteEntity(_targetWall);
|
||||
});
|
||||
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(
|
||||
SAtmos.IsTileAirBlockedCached(RelevantAtmos, Vector2i.Zero),
|
||||
Is.True,
|
||||
"Expected cached AirtightData to remain stale immediately after deletion before atmos tick.");
|
||||
|
||||
var tile = RelevantAtmos.Comp.Tiles[Vector2i.Zero];
|
||||
Assert.That(tile.AdjacentBits,
|
||||
Is.EqualTo(AtmosDirection.Invalid),
|
||||
"Expected tile to still show airtight state before atmos tick after deletion.");
|
||||
|
||||
Assert.That(tile.AirtightData.BlockedDirections,
|
||||
Is.EqualTo(AtmosDirection.All),
|
||||
"Expected AirtightData to reflect non-airtight state before after deletion before an atmos tick.");
|
||||
|
||||
for (var i = 0; i < Atmospherics.Directions; i++)
|
||||
{
|
||||
var direction = (AtmosDirection)(1 << i);
|
||||
var curTile = tile.AdjacentTiles[i];
|
||||
Assert.That(curTile, Is.Null, $"Center tile holds unexpected reference to adjacent tile in direction {direction}.");
|
||||
}
|
||||
}
|
||||
|
||||
SAtmos.RunProcessingFull(ProcessEnt, MapData.Grid.Owner, SAtmos.AtmosTickRate);
|
||||
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(
|
||||
SAtmos.IsTileAirBlockedCached(RelevantAtmos, Vector2i.Zero),
|
||||
Is.False,
|
||||
"Expected cached AirtightData to reflect deletion after atmos tick.");
|
||||
|
||||
var tile = RelevantAtmos.Comp.Tiles[Vector2i.Zero];
|
||||
Assert.That(tile.AdjacentBits,
|
||||
Is.EqualTo(AtmosDirection.All),
|
||||
"Expected tile to reflect non-airtight state after atmos tick.");
|
||||
|
||||
Assert.That(tile.AirtightData.BlockedDirections,
|
||||
Is.EqualTo(AtmosDirection.Invalid),
|
||||
"Expected AirtightData to reflect non-airtight state after atmos tick.");
|
||||
|
||||
for (var i = 0; i < Atmospherics.Directions; i++)
|
||||
{
|
||||
var direction = (AtmosDirection)(1 << i);
|
||||
var curTile = tile.AdjacentTiles[i];
|
||||
Assert.That(curTile, Is.Not.Null, $"Center tile does not hold expected reference to adjacent tile in direction {direction}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Multi-Tile Assertion
|
||||
|
||||
/*
|
||||
Tests for asserting multi-tile airtightness state on cached data.
|
||||
These tests spawn multiple entities and check that the center unblocked entity
|
||||
properly reflects partial airtightness states.
|
||||
|
||||
Note that reconstruction won't save you in the case where you're surrounded by airtight entities,
|
||||
as those don't show up in the reconstruction. Thus, only cached data tests are done here.
|
||||
*/
|
||||
|
||||
/// <summary>
|
||||
/// Tests that the cached airtight map reflects properly when airtight entities are spawned
|
||||
/// along the cardinal directions.
|
||||
/// </summary>
|
||||
/// <param name="atmosDirection">The direction to spawn the airtight entity in.</param>
|
||||
[Test]
|
||||
[TestCase(AtmosDirection.North)]
|
||||
[TestCase(AtmosDirection.South)]
|
||||
[TestCase(AtmosDirection.East)]
|
||||
[TestCase(AtmosDirection.West)]
|
||||
public async Task MultiTile_Spawn_CacheUpdatesOnAtmosTick(AtmosDirection atmosDirection)
|
||||
{
|
||||
// Ensure grid/atmos is initialized.
|
||||
SAtmos.RunProcessingFull(ProcessEnt, MapData.Grid.Owner, SAtmos.AtmosTickRate);
|
||||
|
||||
// Tile should be completely unblocked.
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
var tile = RelevantAtmos.Comp.Tiles[Vector2i.Zero];
|
||||
Assert.That(tile.AdjacentBits,
|
||||
Is.EqualTo(AtmosDirection.All),
|
||||
"Expected tile to be completely unblocked before spawning an airtight entity.");
|
||||
|
||||
for (var i = 0; i < Atmospherics.Directions; i++)
|
||||
{
|
||||
var direction = (AtmosDirection)(1 << i);
|
||||
var curTile = tile.AdjacentTiles[i];
|
||||
Assert.That(curTile, Is.Not.Null, $"Center tile does not hold expected reference to adjacent tile in direction {direction}.");
|
||||
}
|
||||
}
|
||||
|
||||
await Server.WaitPost(delegate
|
||||
{
|
||||
var offsetVec = Vector2i.Zero.Offset(atmosDirection);
|
||||
var coords = new EntityCoordinates(RelevantAtmos.Owner, offsetVec);
|
||||
_targetWall = SEntMan.SpawnAtPosition(_wallProto, coords);
|
||||
});
|
||||
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
var tile = RelevantAtmos.Comp.Tiles[Vector2i.Zero];
|
||||
Assert.That(tile.AdjacentBits,
|
||||
Is.EqualTo(AtmosDirection.All),
|
||||
"Expected tile to still show non-airtight state before an atmos tick.");
|
||||
|
||||
for (var i = 0; i < Atmospherics.Directions; i++)
|
||||
{
|
||||
var direction = (AtmosDirection)(1 << i);
|
||||
var curTile = tile.AdjacentTiles[i];
|
||||
Assert.That(curTile, Is.Not.Null, $"Center tile does not hold expected reference to adjacent tile in direction {direction}.");
|
||||
}
|
||||
}
|
||||
|
||||
SAtmos.RunProcessingFull(ProcessEnt, MapData.Grid.Owner, SAtmos.AtmosTickRate);
|
||||
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
var tile = RelevantAtmos.Comp.Tiles[Vector2i.Zero];
|
||||
Assert.That(tile.AdjacentBits,
|
||||
Is.EqualTo(AtmosDirection.All & ~atmosDirection),
|
||||
"Expected tile to reflect airtight state after atmos tick.");
|
||||
|
||||
for (var i = 0; i < Atmospherics.Directions; i++)
|
||||
{
|
||||
var direction = (AtmosDirection)(1 << i);
|
||||
var curTile = tile.AdjacentTiles[i];
|
||||
if (direction == atmosDirection)
|
||||
{
|
||||
Assert.That(curTile, Is.Null, $"Center tile holds unexpected reference to adjacent tile in direction {direction}.");
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.That(curTile, Is.Not.Null, $"Center tile does not hold expected reference to adjacent tile in direction {direction}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that the cached airtight map reflects properly when an airtight entity is deleted
|
||||
/// along a cardinal direction.
|
||||
/// </summary>
|
||||
/// <param name="atmosDirection">The direction the airtight entity is spawned and then deleted in.</param>
|
||||
[Test]
|
||||
[TestCase(AtmosDirection.North)]
|
||||
[TestCase(AtmosDirection.South)]
|
||||
[TestCase(AtmosDirection.East)]
|
||||
[TestCase(AtmosDirection.West)]
|
||||
public async Task MultiTile_Delete_CacheUpdatesOnAtmosTick(AtmosDirection atmosDirection)
|
||||
{
|
||||
// Ensure grid/atmos is initialized.
|
||||
SAtmos.RunProcessingFull(ProcessEnt, MapData.Grid.Owner, SAtmos.AtmosTickRate);
|
||||
|
||||
await Server.WaitPost(delegate
|
||||
{
|
||||
var offsetVec = Vector2i.Zero.Offset(atmosDirection);
|
||||
var coords = new EntityCoordinates(RelevantAtmos.Owner, offsetVec);
|
||||
_targetWall = SEntMan.SpawnAtPosition(_wallProto, coords);
|
||||
});
|
||||
|
||||
SAtmos.RunProcessingFull(ProcessEnt, MapData.Grid.Owner, SAtmos.AtmosTickRate);
|
||||
|
||||
await Server.WaitPost(delegate
|
||||
{
|
||||
SEntMan.DeleteEntity(_targetWall);
|
||||
});
|
||||
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
var tile = RelevantAtmos.Comp.Tiles[Vector2i.Zero];
|
||||
Assert.That(tile.AdjacentBits,
|
||||
Is.EqualTo(AtmosDirection.All & ~atmosDirection),
|
||||
"Expected tile to remain stale immediately after deletion before an atmos tick.");
|
||||
|
||||
for (var i = 0; i < Atmospherics.Directions; i++)
|
||||
{
|
||||
var direction = (AtmosDirection)(1 << i);
|
||||
var curTile = tile.AdjacentTiles[i];
|
||||
if (direction == atmosDirection)
|
||||
{
|
||||
Assert.That(curTile, Is.Null, $"Center tile holds unexpected reference to adjacent tile in direction {direction}.");
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.That(curTile, Is.Not.Null, $"Center tile does not hold expected reference to adjacent tile in direction {direction}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tick to update cache after deletion.
|
||||
SAtmos.RunProcessingFull(ProcessEnt, MapData.Grid.Owner, SAtmos.AtmosTickRate);
|
||||
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
var tile = RelevantAtmos.Comp.Tiles[Vector2i.Zero];
|
||||
Assert.That(tile.AdjacentBits,
|
||||
Is.EqualTo(AtmosDirection.All),
|
||||
"Expected tile to reflect non-airtight state after deletion after atmos tick.");
|
||||
|
||||
for (var i = 0; i < Atmospherics.Directions; i++)
|
||||
{
|
||||
var direction = (AtmosDirection)(1 << i);
|
||||
var curTile = tile.AdjacentTiles[i];
|
||||
Assert.That(curTile, Is.Not.Null, $"Center tile does not hold expected reference to adjacent tile in direction {direction}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Rotation Assertion
|
||||
|
||||
/// <summary>
|
||||
/// Asserts that an airtight entity with a directional air blocked direction
|
||||
/// properly reflects rotation on spawn.
|
||||
/// </summary>
|
||||
/// <param name="degrees">The degrees to rotate the entity on spawn.</param>
|
||||
/// <param name="expected">The expected blocked direction after rotation.</param>
|
||||
/// <remarks>Yeah, so here I learned that RT handles rotation directions
|
||||
/// as positive == counterclockwise.</remarks>
|
||||
[Test]
|
||||
[TestCase(0f, AtmosDirection.North)]
|
||||
[TestCase(90f, AtmosDirection.West)]
|
||||
[TestCase(180f, AtmosDirection.South)]
|
||||
[TestCase(270f, AtmosDirection.East)]
|
||||
[TestCase(-90f, AtmosDirection.East)]
|
||||
[TestCase(-180f, AtmosDirection.South)]
|
||||
[TestCase(-270f, AtmosDirection.West)]
|
||||
public async Task Rotation_AirBlockedDirectionsOnSpawn(float degrees, AtmosDirection expected)
|
||||
{
|
||||
SAtmos.RunProcessingFull(ProcessEnt, MapData.Grid.Owner, SAtmos.AtmosTickRate);
|
||||
|
||||
var rotation = Angle.FromDegrees(degrees);
|
||||
|
||||
await Server.WaitPost(delegate
|
||||
{
|
||||
var coords = new EntityCoordinates(RelevantAtmos.Owner, Vector2.Zero);
|
||||
_targetRotationEnt = SEntMan.SpawnAtPosition("AirtightDirectionalRotationTest", coords);
|
||||
|
||||
Transform.SetLocalRotation(_targetRotationEnt, rotation);
|
||||
});
|
||||
|
||||
SAtmos.RunProcessingFull(ProcessEnt, MapData.Grid.Owner, SAtmos.AtmosTickRate);
|
||||
|
||||
await Server.WaitAssertion(delegate
|
||||
{
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
SEntMan.TryGetComponent<AirtightComponent>(_targetRotationEnt, out var airtight);
|
||||
Assert.That(airtight, Is.Not.Null);
|
||||
|
||||
var initial = (AtmosDirection)airtight.InitialAirBlockedDirection;
|
||||
Assert.That(initial,
|
||||
Is.EqualTo(AtmosDirection.North),
|
||||
"Directional airtight entity should block North on spawn.");
|
||||
|
||||
Assert.That(airtight.AirBlockedDirection,
|
||||
Is.EqualTo(expected),
|
||||
$"Expected AirBlockedDirection to be {expected} after rotating by {degrees} degrees on spawn.");
|
||||
|
||||
// i dont trust you airtightsystem
|
||||
if (degrees is 90f or 270f)
|
||||
{
|
||||
Assert.That(expected,
|
||||
Is.Not.EqualTo(initial),
|
||||
"Rotated directions should differ for 90/270 degrees.");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -18,6 +18,7 @@ namespace Content.IntegrationTests.Tests.Atmos;
|
||||
public abstract class AtmosTest : InteractionTest
|
||||
{
|
||||
protected AtmosphereSystem SAtmos = default!;
|
||||
protected Content.Client.Atmos.EntitySystems.AtmosphereSystem CAtmos = default!;
|
||||
protected EntityLookupSystem LookupSystem = default!;
|
||||
|
||||
protected Entity<GridAtmosphereComponent> RelevantAtmos;
|
||||
@@ -38,6 +39,7 @@ public abstract class AtmosTest : InteractionTest
|
||||
await base.Setup();
|
||||
|
||||
SAtmos = SEntMan.System<AtmosphereSystem>();
|
||||
CAtmos = CEntMan.System<Content.Client.Atmos.EntitySystems.AtmosphereSystem>();
|
||||
LookupSystem = SEntMan.System<EntityLookupSystem>();
|
||||
|
||||
SEntMan.TryGetComponent<GridAtmosphereComponent>(MapData.Grid, out var gridAtmosComp);
|
||||
|
||||
@@ -0,0 +1,275 @@
|
||||
using Content.Client.Atmos.EntitySystems;
|
||||
using Content.IntegrationTests.Pair;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Atmos.EntitySystems;
|
||||
using Content.Shared.CCVar;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.UnitTesting;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.Atmos;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for asserting that various gas specific heat operations agree with each other and do not deviate
|
||||
/// across client and server.
|
||||
/// </summary>
|
||||
[TestOf(nameof(SharedAtmosphereSystem))]
|
||||
public sealed class SharedGasSpecificHeatsTest
|
||||
{
|
||||
private IConfigurationManager _sConfig;
|
||||
private IConfigurationManager _cConfig;
|
||||
|
||||
private TestPair _pair = default!;
|
||||
|
||||
private RobustIntegrationTest.ServerIntegrationInstance Server => _pair.Server;
|
||||
private RobustIntegrationTest.ClientIntegrationInstance Client => _pair.Client;
|
||||
|
||||
private IEntityManager _sEntMan = default!;
|
||||
private Content.Server.Atmos.EntitySystems.AtmosphereSystem _sAtmos = default!;
|
||||
|
||||
private IEntityManager _cEntMan = default!;
|
||||
private AtmosphereSystem _cAtmos = default!;
|
||||
|
||||
[SetUp]
|
||||
public async Task SetUp()
|
||||
{
|
||||
var poolSettings = new PoolSettings
|
||||
{
|
||||
Connected = true,
|
||||
};
|
||||
_pair = await PoolManager.GetServerClient(poolSettings);
|
||||
|
||||
_sEntMan = Server.ResolveDependency<IEntityManager>();
|
||||
_cEntMan = Client.ResolveDependency<IEntityManager>();
|
||||
|
||||
_sAtmos = _sEntMan.System<Content.Server.Atmos.EntitySystems.AtmosphereSystem>();
|
||||
_cAtmos = _cEntMan.System<AtmosphereSystem>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asserts that the cached gas specific heat arrays agree with each other.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task GasSpecificHeats_Agree()
|
||||
{
|
||||
var serverSpecificHeats = Array.Empty<float>();
|
||||
var clientSpecificHeats = Array.Empty<float>();
|
||||
await Server.WaitPost(delegate
|
||||
{
|
||||
serverSpecificHeats = _sAtmos.GasSpecificHeats;
|
||||
});
|
||||
|
||||
await Client.WaitPost(delegate
|
||||
{
|
||||
clientSpecificHeats = _cAtmos.GasSpecificHeats;
|
||||
});
|
||||
|
||||
Assert.That(serverSpecificHeats,
|
||||
Is.EqualTo(clientSpecificHeats),
|
||||
"Server and client gas specific heat arrays do not agree.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asserts that heat capacity calculations agree for the same gas mixture.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task HeatCapacity_Agree()
|
||||
{
|
||||
const float volume = 2500f;
|
||||
const float temperature = 293.15f;
|
||||
|
||||
const float o2 = 12.3f;
|
||||
const float n2 = 45.6f;
|
||||
const float co2 = 0.42f;
|
||||
const float plasma = 0.05f;
|
||||
|
||||
var serverScaled = 0f;
|
||||
var serverUnscaled = 0f;
|
||||
var clientScaled = 0f;
|
||||
var clientUnscaled = 0f;
|
||||
|
||||
await Server.WaitPost(delegate
|
||||
{
|
||||
var mix = new GasMixture(volume) { Temperature = temperature };
|
||||
mix.AdjustMoles(Gas.Oxygen, o2);
|
||||
mix.AdjustMoles(Gas.Nitrogen, n2);
|
||||
mix.AdjustMoles(Gas.CarbonDioxide, co2);
|
||||
mix.AdjustMoles(Gas.Plasma, plasma);
|
||||
|
||||
serverScaled = _sAtmos.GetHeatCapacity(mix, applyScaling: true);
|
||||
serverUnscaled = _sAtmos.GetHeatCapacity(mix, applyScaling: false);
|
||||
});
|
||||
|
||||
await Client.WaitPost(delegate
|
||||
{
|
||||
var mix = new GasMixture(volume) { Temperature = temperature };
|
||||
mix.AdjustMoles(Gas.Oxygen, o2);
|
||||
mix.AdjustMoles(Gas.Nitrogen, n2);
|
||||
mix.AdjustMoles(Gas.CarbonDioxide, co2);
|
||||
mix.AdjustMoles(Gas.Plasma, plasma);
|
||||
|
||||
clientScaled = _cAtmos.GetHeatCapacity(mix, applyScaling: true);
|
||||
clientUnscaled = _cAtmos.GetHeatCapacity(mix, applyScaling: false);
|
||||
});
|
||||
|
||||
// none of these should be exploding or nonzero.
|
||||
// they could potentially agree at insane values and pass the test
|
||||
// so check for if they're sane.
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(serverScaled,
|
||||
Is.GreaterThan(0f),
|
||||
"Heat capacity calculated on server with scaling is not greater than zero.");
|
||||
Assert.That(serverUnscaled,
|
||||
Is.GreaterThan(0f),
|
||||
"Heat capacity calculated on server without scaling is not greater than zero.");
|
||||
Assert.That(clientScaled,
|
||||
Is.GreaterThan(0f),
|
||||
"Heat capacity calculated on client with scaling is not greater than zero.");
|
||||
Assert.That(clientUnscaled,
|
||||
Is.GreaterThan(0f),
|
||||
"Heat capacity calculated on client without scaling is not greater than zero.");
|
||||
|
||||
Assert.That(float.IsFinite(serverScaled),
|
||||
Is.True,
|
||||
"Heat capacity calculated on server with scaling is not finite.");
|
||||
Assert.That(float.IsFinite(serverUnscaled),
|
||||
Is.True,
|
||||
"Heat capacity calculated on server without scaling is not finite.");
|
||||
Assert.That(float.IsFinite(clientScaled),
|
||||
Is.True,
|
||||
"Heat capacity calculated on client with scaling is not finite.");
|
||||
Assert.That(float.IsFinite(clientUnscaled),
|
||||
Is.True,
|
||||
"Heat capacity calculated on client without scaling is not finite.");
|
||||
}
|
||||
|
||||
const float epsilon = 1e-4f;
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(serverScaled,
|
||||
Is.EqualTo(clientScaled).Within(epsilon),
|
||||
"Heat capacity calculated with scaling does not agree between client and server.");
|
||||
Assert.That(serverUnscaled,
|
||||
Is.EqualTo(clientUnscaled).Within(epsilon),
|
||||
"Heat capacity calculated without scaling does not agree between client and server.");
|
||||
|
||||
Assert.That(serverUnscaled,
|
||||
Is.EqualTo(serverScaled * _sAtmos.HeatScale).Within(epsilon),
|
||||
"Heat capacity calculated on server without scaling does not equal scaled value multiplied by HeatScale.");
|
||||
Assert.That(clientUnscaled,
|
||||
Is.EqualTo(clientScaled * _cAtmos.HeatScale).Within(epsilon),
|
||||
"Heat capacity calculated on client without scaling does not equal scaled value multiplied by HeatScale.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// HeatScale CVAR is required for specific heat calculations.
|
||||
/// Assert that they agree across client and server, and that changing the CVAR
|
||||
/// replicates properly and updates the cached value.
|
||||
/// Also assert that calculations using the updated HeatScale agree properly.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task HeatScaleCVar_Replicates_Agree()
|
||||
{
|
||||
// ensure that replicated value changes by testing a new value
|
||||
const float newHeatScale = 13f;
|
||||
|
||||
_sConfig = Server.ResolveDependency<IConfigurationManager>();
|
||||
_cConfig = Client.ResolveDependency<IConfigurationManager>();
|
||||
|
||||
await Server.WaitPost(delegate
|
||||
{
|
||||
_sConfig.SetCVar(CCVars.AtmosHeatScale, newHeatScale);
|
||||
});
|
||||
|
||||
await Server.WaitRunTicks(5);
|
||||
await Client.WaitRunTicks(5);
|
||||
|
||||
// assert agreement between client and server
|
||||
float serverCVar = 0;
|
||||
float clientCVar = 0;
|
||||
float serverHeatScale = 0;
|
||||
float clientHeatScale = 0;
|
||||
|
||||
await Server.WaitPost(delegate
|
||||
{
|
||||
serverCVar = _sConfig.GetCVar(CCVars.AtmosHeatScale);
|
||||
serverHeatScale = _sAtmos.HeatScale;
|
||||
});
|
||||
|
||||
await Client.WaitPost(delegate
|
||||
{
|
||||
clientCVar = _cConfig.GetCVar(CCVars.AtmosHeatScale);
|
||||
clientHeatScale = _cAtmos.HeatScale;
|
||||
});
|
||||
|
||||
const float epsilon = 1e-4f;
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(serverCVar,
|
||||
Is.EqualTo(newHeatScale).Within(epsilon),
|
||||
"Server CVAR value for AtmosHeatScale does not equal the set value.");
|
||||
Assert.That(clientCVar,
|
||||
Is.EqualTo(newHeatScale).Within(epsilon),
|
||||
"Client CVAR value for AtmosHeatScale does not equal the set value.");
|
||||
|
||||
Assert.That(serverHeatScale,
|
||||
Is.EqualTo(newHeatScale).Within(epsilon),
|
||||
"Server cached HeatScale does not equal the set CVAR value.");
|
||||
Assert.That(clientHeatScale,
|
||||
Is.EqualTo(newHeatScale).Within(epsilon),
|
||||
"Client cached HeatScale does not equal the set CVAR value.");
|
||||
|
||||
Assert.That(serverHeatScale,
|
||||
Is.EqualTo(clientHeatScale).Within(epsilon),
|
||||
"Client and server cached HeatScale values do not agree.");
|
||||
}
|
||||
|
||||
// verify that anything calculated using the shared HeatScale agrees properly
|
||||
const float volume = 2500f;
|
||||
const float temperature = 293.15f;
|
||||
|
||||
var sScaled = 0f;
|
||||
var sUnscaled = 0f;
|
||||
var cScaled = 0f;
|
||||
var cUnscaled = 0f;
|
||||
|
||||
await Server.WaitPost(delegate
|
||||
{
|
||||
var mix = new GasMixture(volume) { Temperature = temperature };
|
||||
mix.AdjustMoles(Gas.Oxygen, 10f);
|
||||
mix.AdjustMoles(Gas.Nitrogen, 20f);
|
||||
|
||||
sScaled = _sAtmos.GetHeatCapacity(mix, applyScaling: true);
|
||||
sUnscaled = _sAtmos.GetHeatCapacity(mix, applyScaling: false);
|
||||
});
|
||||
|
||||
await Client.WaitPost(delegate
|
||||
{
|
||||
var mix = new GasMixture(volume) { Temperature = temperature };
|
||||
mix.AdjustMoles(Gas.Oxygen, 10f);
|
||||
mix.AdjustMoles(Gas.Nitrogen, 20f);
|
||||
|
||||
cScaled = _cAtmos.GetHeatCapacity(mix, applyScaling: true);
|
||||
cUnscaled = _cAtmos.GetHeatCapacity(mix, applyScaling: false);
|
||||
});
|
||||
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(sScaled,
|
||||
Is.GreaterThan(0f),
|
||||
"Heat capacity calculated on server with scaling is not greater than zero after CVAR change.");
|
||||
Assert.That(cScaled,
|
||||
Is.GreaterThan(0f),
|
||||
"Heat capacity calculated on client with scaling is not greater than zero after CVAR change.");
|
||||
|
||||
Assert.That(sUnscaled,
|
||||
Is.EqualTo(sScaled * serverHeatScale).Within(epsilon),
|
||||
"Heat capacity calculated on server without scaling does not equal scaled value multiplied by updated HeatScale.");
|
||||
Assert.That(cUnscaled,
|
||||
Is.EqualTo(cScaled * clientHeatScale).Within(epsilon),
|
||||
"Heat capacity calculated on client without scaling does not equal scaled value multiplied by updated HeatScale.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
#nullable enable
|
||||
using Content.Server.Body.Systems;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.Body;
|
||||
|
||||
[TestFixture]
|
||||
public sealed class GibTest
|
||||
{
|
||||
[Test]
|
||||
public async Task TestGib()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient(new PoolSettings { Connected = true });
|
||||
var (server, client) = (pair.Server, pair.Client);
|
||||
var map = await pair.CreateTestMap();
|
||||
|
||||
EntityUid target1 = default;
|
||||
EntityUid target2 = default;
|
||||
|
||||
await server.WaitAssertion(() => target1 = server.EntMan.Spawn("MobHuman", map.MapCoords));
|
||||
await server.WaitAssertion(() => target2 = server.EntMan.Spawn("MobHuman", map.MapCoords));
|
||||
await pair.WaitCommand($"setoutfit {server.EntMan.GetNetEntity(target1)} CaptainGear");
|
||||
await pair.WaitCommand($"setoutfit {server.EntMan.GetNetEntity(target2)} CaptainGear");
|
||||
|
||||
await pair.RunTicksSync(5);
|
||||
var nuid1 = pair.ToClientUid(target1);
|
||||
var nuid2 = pair.ToClientUid(target2);
|
||||
Assert.That(client.EntMan.EntityExists(nuid1));
|
||||
Assert.That(client.EntMan.EntityExists(nuid2));
|
||||
|
||||
await server.WaitAssertion(() => server.System<BodySystem>().GibBody(target1, gibOrgans: false));
|
||||
await server.WaitAssertion(() => server.System<BodySystem>().GibBody(target2, gibOrgans: true));
|
||||
|
||||
await pair.RunTicksSync(5);
|
||||
await pair.WaitCommand("dirty");
|
||||
await pair.RunTicksSync(5);
|
||||
|
||||
Assert.That(!client.EntMan.EntityExists(nuid1));
|
||||
Assert.That(!client.EntMan.EntityExists(nuid2));
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
}
|
||||
124
Content.IntegrationTests/Tests/Chemistry/DrainTest.cs
Normal file
124
Content.IntegrationTests/Tests/Chemistry/DrainTest.cs
Normal file
@@ -0,0 +1,124 @@
|
||||
using Content.IntegrationTests.Tests.Interaction;
|
||||
using Content.Shared.Chemistry.EntitySystems;
|
||||
using Content.Shared.Chemistry.Reagent;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Fluids.Components;
|
||||
using Content.Shared.Nutrition.Components;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.Chemistry;
|
||||
|
||||
public sealed class DrainTest : InteractionTest
|
||||
{
|
||||
private static readonly EntProtoId PizzaPrototype = "FoodPizzaMargherita";
|
||||
private static readonly EntProtoId DrainPrototype = "FloorDrain";
|
||||
private static readonly EntProtoId BucketPrototype = "Bucket";
|
||||
private static readonly ProtoId<ReagentPrototype> BloodReagent = "Blood";
|
||||
private static readonly ProtoId<ReagentPrototype> WaterReagent = "Water";
|
||||
private static readonly FixedPoint2 WaterVolume = 50; // 50u
|
||||
private static readonly FixedPoint2 PuddleVolume = 30; // 30u
|
||||
|
||||
[TestPrototypes]
|
||||
private static readonly string Prototypes = @$"
|
||||
- type: entity
|
||||
parent: Puddle
|
||||
id: PuddleBloodTest
|
||||
suffix: Blood (30u)
|
||||
components:
|
||||
- type: SolutionContainerManager
|
||||
solutions:
|
||||
puddle:
|
||||
maxVol: 1000
|
||||
reagents:
|
||||
- ReagentId: {BloodReagent}
|
||||
Quantity: {PuddleVolume}
|
||||
";
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Tests that drag drop interactions with drains are working as intended.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task DragDropOntoDrainTest()
|
||||
{
|
||||
var solutionContainerSys = SEntMan.System<SharedSolutionContainerSystem>();
|
||||
|
||||
// Spawn a drain one tile away.
|
||||
var drain = await Spawn(DrainPrototype);
|
||||
|
||||
// Spawn a bucket at the player's coordinates.
|
||||
var bucket = await Spawn(BucketPrototype, PlayerCoords);
|
||||
|
||||
// Add water to the bucket.
|
||||
Assert.That(solutionContainerSys.TryGetDrainableSolution(ToServer(bucket), out var solutionEnt, out var solution), "Bucket had no drainable solution.");
|
||||
await Server.WaitAssertion(() =>
|
||||
{
|
||||
Assert.That(solutionContainerSys.TryAddReagent(solutionEnt.Value, WaterReagent, WaterVolume), "Could not add water to the bucket.");
|
||||
});
|
||||
|
||||
// Check that the bucket was filled.
|
||||
Assert.That(solutionContainerSys.TryGetDrainableSolution(ToServer(bucket), out solutionEnt, out solution), "Bucket had no drainable solution after filling it.");
|
||||
Assert.That(solution.Volume, Is.EqualTo(WaterVolume));
|
||||
|
||||
// Drag drop the bucket onto the drain.
|
||||
await DragDrop(bucket, drain);
|
||||
|
||||
// Check that the bucket is empty.
|
||||
Assert.That(solutionContainerSys.TryGetDrainableSolution(ToServer(bucket), out solutionEnt, out solution), "Bucket had no drainable solution after draining it.");
|
||||
Assert.That(solution.Volume, Is.EqualTo(FixedPoint2.Zero), "Bucket was not empty after draining it.");
|
||||
|
||||
await Delete(bucket);
|
||||
|
||||
// Spawn a pizza at the player's coordinates.
|
||||
var pizza = await Spawn(PizzaPrototype, PlayerCoords);
|
||||
|
||||
// Check that the pizza is not empty.
|
||||
var edibleSolutionId = Comp<EdibleComponent>(pizza).Solution;
|
||||
Assert.That(solutionContainerSys.TryGetSolution(ToServer(pizza), edibleSolutionId, out solutionEnt, out solution), "Pizza had no edible solution.");
|
||||
var pizzaVolume = solution.Volume;
|
||||
Assert.That(pizzaVolume, Is.GreaterThan(FixedPoint2.Zero), "Pizza had no reagents inside its edible solution.");
|
||||
|
||||
// Drag drop the pizza onto the drain.
|
||||
// Yes, this was a bug that existed before.
|
||||
await DragDrop(pizza, drain);
|
||||
|
||||
// Check that the pizza did not get deleted or had its reagents drained.
|
||||
AssertExists(pizza);
|
||||
Assert.That(solutionContainerSys.TryGetSolution(ToServer(pizza), edibleSolutionId, out solutionEnt, out solution), "Pizza had no edible solution.");
|
||||
Assert.That(solution.Volume, Is.EqualTo(pizzaVolume), "Pizza lost reagents when drag dropped onto a drain.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that drains make puddles next to them disappear.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task DrainPuddleTest()
|
||||
{
|
||||
var solutionContainerSys = SEntMan.System<SharedSolutionContainerSystem>();
|
||||
|
||||
// Spawn a puddle at the player coordinates;
|
||||
var puddle = await Spawn("PuddleBloodTest", PlayerCoords);
|
||||
|
||||
// Make sure the reagent chosen for this test does not evaporate on its own.
|
||||
// If you are a fork that made more reagents evaporate, just change BloodReagent ProtoId above to something else.
|
||||
Assert.That(HasComp<EvaporationComponent>(puddle), Is.False, "The chosen reagent is evaporating on its own and we cannot use it for the drain test.");
|
||||
|
||||
var puddleSolutionId = Comp<PuddleComponent>(puddle).SolutionName;
|
||||
Assert.That(solutionContainerSys.TryGetSolution(ToServer(puddle), puddleSolutionId, out _, out var solution), "Puddle had no solution.");
|
||||
Assert.That(solution.Volume, Is.EqualTo(PuddleVolume), "Puddle had the wrong amount of reagents after spawning.");
|
||||
|
||||
// Wait a few seconds and check that the puddle did not disappear on its own.
|
||||
await RunSeconds(10);
|
||||
Assert.That(solutionContainerSys.TryGetSolution(ToServer(puddle), puddleSolutionId, out _, out solution), "Puddle had no solution.");
|
||||
Assert.That(solution.Volume, Is.EqualTo(PuddleVolume), "Puddle had the wrong amount of reagents after spawning.");
|
||||
|
||||
// Spawn a drain one tile away.
|
||||
await Spawn(DrainPrototype);
|
||||
|
||||
// Wait a few seconds.
|
||||
await RunSeconds(10);
|
||||
|
||||
// Make sure the puddle was deleted by the drain.
|
||||
AssertDeleted(puddle);
|
||||
}
|
||||
}
|
||||
@@ -50,11 +50,12 @@ namespace Content.IntegrationTests.Tests.Chemistry
|
||||
beaker = entityManager.SpawnEntity("TestSolutionContainer", coordinates);
|
||||
Assert.That(solutionContainerSystem
|
||||
.TryGetSolution(beaker, "beaker", out solutionEnt, out solution));
|
||||
solutionEnt.Value.Comp.Solution.CanReact = false;
|
||||
foreach (var (id, reactant) in reactionPrototype.Reactants)
|
||||
{
|
||||
#pragma warning disable NUnit2045
|
||||
Assert.That(solutionContainerSystem
|
||||
.TryAddReagent(solutionEnt.Value, id, reactant.Amount, out var quantity));
|
||||
.TryAddReagent(solutionEnt.Value, id, reactant.Amount, out var quantity, reactionPrototype.MinimumTemperature));
|
||||
Assert.That(reactant.Amount, Is.EqualTo(quantity));
|
||||
#pragma warning restore NUnit2045
|
||||
}
|
||||
@@ -67,7 +68,7 @@ namespace Content.IntegrationTests.Tests.Chemistry
|
||||
//Check if the reaction is the first to occur when heated
|
||||
foreach (var possibleReaction in possibleReactions.OrderBy(r => r.MinimumTemperature))
|
||||
{
|
||||
if (possibleReaction.MinimumTemperature < reactionPrototype.MinimumTemperature && possibleReaction.MixingCategories == reactionPrototype.MixingCategories)
|
||||
if (possibleReaction.Priority >= reactionPrototype.Priority && possibleReaction.MinimumTemperature < reactionPrototype.MinimumTemperature && possibleReaction.MixingCategories == reactionPrototype.MixingCategories)
|
||||
{
|
||||
Assert.Fail($"The {possibleReaction.ID} reaction may occur before {reactionPrototype.ID} when heated.");
|
||||
}
|
||||
@@ -76,14 +77,16 @@ namespace Content.IntegrationTests.Tests.Chemistry
|
||||
//Check if the reaction is the first to occur when freezing
|
||||
foreach (var possibleReaction in possibleReactions.OrderBy(r => r.MaximumTemperature))
|
||||
{
|
||||
if (possibleReaction.MaximumTemperature > reactionPrototype.MaximumTemperature && possibleReaction.MixingCategories == reactionPrototype.MixingCategories)
|
||||
if (possibleReaction.Priority >= reactionPrototype.Priority && possibleReaction.MaximumTemperature > reactionPrototype.MaximumTemperature && possibleReaction.MixingCategories == reactionPrototype.MixingCategories)
|
||||
{
|
||||
Assert.Fail($"The {possibleReaction.ID} reaction may occur before {reactionPrototype.ID} when freezing.");
|
||||
}
|
||||
}
|
||||
|
||||
//Now safe set the temperature and mix the reagents
|
||||
solutionEnt.Value.Comp.Solution.CanReact = true;
|
||||
solutionContainerSystem.SetTemperature(solutionEnt.Value, reactionPrototype.MinimumTemperature);
|
||||
solutionContainerSystem.UpdateChemicals(solutionEnt.Value);
|
||||
|
||||
if (reactionPrototype.MixingCategories != null)
|
||||
{
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System.Linq;
|
||||
using Content.Shared.Chemistry;
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.Prototypes;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Prototypes;
|
||||
@@ -13,14 +15,17 @@ namespace Content.IntegrationTests.Tests;
|
||||
public sealed class FillLevelSpriteTest
|
||||
{
|
||||
private static readonly string[] HandStateNames = ["left", "right"];
|
||||
private static readonly string[] EquipStateNames = ["back", "suitstorage"];
|
||||
|
||||
[Test]
|
||||
public async Task FillLevelSpritesExist()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient();
|
||||
await using var pair = await PoolManager.GetServerClient(new PoolSettings { Connected = true });
|
||||
var client = pair.Client;
|
||||
var protoMan = client.ResolveDependency<IPrototypeManager>();
|
||||
var componentFactory = client.ResolveDependency<IComponentFactory>();
|
||||
var entMan = client.ResolveDependency<IEntityManager>();
|
||||
var spriteSystem = client.System<SpriteSystem>();
|
||||
|
||||
await client.WaitAssertion(() =>
|
||||
{
|
||||
@@ -31,39 +36,70 @@ public sealed class FillLevelSpriteTest
|
||||
.OrderBy(p => p.ID)
|
||||
.ToList();
|
||||
|
||||
foreach (var proto in protos)
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(proto.TryGetComponent<SolutionContainerVisualsComponent>(out var visuals, componentFactory));
|
||||
Assert.That(proto.TryGetComponent<SpriteComponent>(out var sprite, componentFactory));
|
||||
|
||||
var rsi = sprite.BaseRSI;
|
||||
|
||||
// Test base sprite fills
|
||||
if (!string.IsNullOrEmpty(visuals.FillBaseName))
|
||||
foreach (var proto in protos)
|
||||
{
|
||||
for (var i = 1; i <= visuals.MaxFillLevels; i++)
|
||||
Assert.That(proto.TryGetComponent<SolutionContainerVisualsComponent>(out var visuals, componentFactory));
|
||||
Assert.That(proto.TryGetComponent<SpriteComponent>(out var sprite, componentFactory));
|
||||
if (!proto.HasComponent<AppearanceComponent>(componentFactory))
|
||||
{
|
||||
var state = $"{visuals.FillBaseName}{i}";
|
||||
Assert.That(rsi.TryGetState(state, out _), @$"{proto.ID} has SolutionContainerVisualsComponent with
|
||||
MaxFillLevels = {visuals.MaxFillLevels}, but {rsi.Path} doesn't have state {state}!");
|
||||
Assert.Fail(@$"{proto.ID} has SolutionContainerVisualsComponent but no AppearanceComponent.");
|
||||
}
|
||||
}
|
||||
|
||||
// Test inhand sprite fills
|
||||
if (!string.IsNullOrEmpty(visuals.InHandsFillBaseName))
|
||||
{
|
||||
for (var i = 1; i <= visuals.InHandsMaxFillLevels; i++)
|
||||
// Test base sprite fills
|
||||
if (!string.IsNullOrEmpty(visuals.FillBaseName) && visuals.MaxFillLevels > 0)
|
||||
{
|
||||
foreach (var handname in HandStateNames)
|
||||
var entity = entMan.Spawn(proto.ID);
|
||||
if (!spriteSystem.LayerMapTryGet(entity, SolutionContainerLayers.Fill, out var fillLayerId, false))
|
||||
{
|
||||
var state = $"inhand-{handname}{visuals.InHandsFillBaseName}{i}";
|
||||
Assert.That(rsi.TryGetState(state, out _), @$"{proto.ID} has SolutionContainerVisualsComponent with
|
||||
InHandsMaxFillLevels = {visuals.InHandsMaxFillLevels}, but {rsi.Path} doesn't have state {state}!");
|
||||
Assert.Fail(@$"{proto.ID} has SolutionContainerVisualsComponent but no fill layer map.");
|
||||
}
|
||||
if (!spriteSystem.TryGetLayer(entity, fillLayerId, out var fillLayer, false))
|
||||
{
|
||||
Assert.Fail(@$"{proto.ID} somehow lost a layer.");
|
||||
}
|
||||
var rsi = fillLayer.ActualRsi;
|
||||
|
||||
for (var i = 1; i <= visuals.MaxFillLevels; i++)
|
||||
{
|
||||
var state = $"{visuals.FillBaseName}{i}";
|
||||
Assert.That(rsi.TryGetState(state, out _), @$"{proto.ID} has SolutionContainerVisualsComponent with
|
||||
MaxFillLevels = {visuals.MaxFillLevels}, but {rsi.Path} doesn't have state {state}!");
|
||||
}
|
||||
}
|
||||
|
||||
// Test inhand sprite fills
|
||||
if (!string.IsNullOrEmpty(visuals.InHandsFillBaseName) && visuals.InHandsMaxFillLevels > 0)
|
||||
{
|
||||
var rsi = sprite.BaseRSI;
|
||||
for (var i = 1; i <= visuals.InHandsMaxFillLevels; i++)
|
||||
{
|
||||
foreach (var handname in HandStateNames)
|
||||
{
|
||||
var state = $"inhand-{handname}{visuals.InHandsFillBaseName}{i}";
|
||||
Assert.That(rsi.TryGetState(state, out _), @$"{proto.ID} has SolutionContainerVisualsComponent with
|
||||
InHandsMaxFillLevels = {visuals.InHandsMaxFillLevels}, but {rsi.Path} doesn't have state {state}!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test equipped sprite fills
|
||||
if (!string.IsNullOrEmpty(visuals.EquippedFillBaseName) && visuals.EquippedMaxFillLevels > 0)
|
||||
{
|
||||
var rsi = sprite.BaseRSI;
|
||||
for (var i = 1; i <= visuals.EquippedMaxFillLevels; i++)
|
||||
{
|
||||
foreach (var equipName in EquipStateNames)
|
||||
{
|
||||
var state = $"equipped-{equipName}{visuals.EquippedFillBaseName}{i}";
|
||||
Assert.That(rsi.TryGetState(state, out _), @$"{proto.ID} has SolutionContainerVisualsComponent with
|
||||
EquippedMaxFillLevels = {visuals.EquippedMaxFillLevels}, but {rsi.Path} doesn't have state {state}!");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
|
||||
@@ -26,7 +26,7 @@ namespace Content.IntegrationTests.Tests.GameObjects.Components
|
||||
.ToList()
|
||||
.AsParallel()
|
||||
.Where(filePath => filePath.Extension == "yml" &&
|
||||
!filePath.Filename.StartsWith(".", StringComparison.Ordinal))
|
||||
!filePath.Filename.StartsWith('.'))
|
||||
.ToArray();
|
||||
|
||||
var cComponentFactory = client.ResolveDependency<IComponentFactory>();
|
||||
@@ -34,6 +34,7 @@ namespace Content.IntegrationTests.Tests.GameObjects.Components
|
||||
|
||||
var unknownComponentsClient = new List<(string entityId, string component)>();
|
||||
var unknownComponentsServer = new List<(string entityId, string component)>();
|
||||
var doubleIgnoredComponents = new List<(string entityId, string component)>();
|
||||
var entitiesValidated = 0;
|
||||
var componentsValidated = 0;
|
||||
|
||||
@@ -72,26 +73,32 @@ namespace Content.IntegrationTests.Tests.GameObjects.Components
|
||||
|
||||
var componentType = component.GetNode("type").AsString();
|
||||
var clientAvailability = cComponentFactory.GetComponentAvailability(componentType);
|
||||
|
||||
if (clientAvailability == ComponentAvailability.Unknown)
|
||||
{
|
||||
var entityId = node.GetNode("id").AsString();
|
||||
unknownComponentsClient.Add((entityId, componentType));
|
||||
}
|
||||
|
||||
var serverAvailability = sComponentFactory.GetComponentAvailability(componentType);
|
||||
|
||||
if (serverAvailability == ComponentAvailability.Unknown)
|
||||
var entityId = node.GetNode("id").AsString();
|
||||
|
||||
if ((clientAvailability, serverAvailability) is
|
||||
(ComponentAvailability.Ignore, ComponentAvailability.Ignore))
|
||||
{
|
||||
var entityId = node.GetNode("id").AsString();
|
||||
unknownComponentsServer.Add((entityId, componentType));
|
||||
doubleIgnoredComponents.Add((entityId, componentType));
|
||||
continue;
|
||||
}
|
||||
|
||||
// NOTE: currently, the client's component factory is configured to ignore /all/
|
||||
// non-registered components, meaning this case will never succeed. This is here
|
||||
// mainly for future proofing plus any downstreams that were brave enough to not
|
||||
// ignore all unknown components on clientside.
|
||||
if (clientAvailability == ComponentAvailability.Unknown)
|
||||
unknownComponentsClient.Add((entityId, componentType));
|
||||
|
||||
if (serverAvailability == ComponentAvailability.Unknown)
|
||||
unknownComponentsServer.Add((entityId, componentType));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (unknownComponentsClient.Count + unknownComponentsServer.Count == 0)
|
||||
if (unknownComponentsClient.Count + unknownComponentsServer.Count + doubleIgnoredComponents.Count == 0)
|
||||
{
|
||||
await pair.CleanReturnAsync();
|
||||
Assert.Pass($"Validated {entitiesValidated} entities with {componentsValidated} components in {paths.Length} files.");
|
||||
@@ -112,6 +119,12 @@ namespace Content.IntegrationTests.Tests.GameObjects.Components
|
||||
$"SERVER: Unknown component {component} in prototype {entityId}\n");
|
||||
}
|
||||
|
||||
foreach (var (entityId, component) in doubleIgnoredComponents)
|
||||
{
|
||||
message.Append(
|
||||
$"Component {component} in prototype {entityId} is ignored by both client and serverV\n");
|
||||
}
|
||||
|
||||
Assert.Fail(message.ToString());
|
||||
}
|
||||
|
||||
|
||||
36
Content.IntegrationTests/Tests/Gibbing/GibTest.cs
Normal file
36
Content.IntegrationTests/Tests/Gibbing/GibTest.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
#nullable enable
|
||||
using Content.Shared.Gibbing;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.Body;
|
||||
|
||||
[TestFixture]
|
||||
public sealed class GibTest
|
||||
{
|
||||
[Test]
|
||||
public async Task TestGib()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient(new PoolSettings { Connected = true });
|
||||
var (server, client) = (pair.Server, pair.Client);
|
||||
var map = await pair.CreateTestMap();
|
||||
|
||||
EntityUid target = default;
|
||||
|
||||
await server.WaitAssertion(() => target = server.EntMan.Spawn("MobHuman", map.MapCoords));
|
||||
await pair.WaitCommand($"setoutfit {server.EntMan.GetNetEntity(target)} CaptainGear");
|
||||
|
||||
await pair.RunTicksSync(5);
|
||||
var nuid = pair.ToClientUid(target);
|
||||
Assert.That(client.EntMan.EntityExists(nuid));
|
||||
|
||||
await server.WaitAssertion(() => server.System<GibbingSystem>().Gib(target));
|
||||
|
||||
await pair.RunTicksSync(5);
|
||||
await pair.WaitCommand("dirty");
|
||||
await pair.RunTicksSync(5);
|
||||
|
||||
Assert.That(!client.EntMan.EntityExists(nuid));
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
}
|
||||
@@ -91,16 +91,18 @@ public abstract partial class InteractionTest
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spawn an entity at the target coordinates and set it as the target.
|
||||
/// Spawn an entity at the given coordinates and set it as the target.
|
||||
/// If no coordinates are given it will default to <see cref="TargetCoords"/>
|
||||
/// </summary>
|
||||
[MemberNotNull(nameof(Target), nameof(STarget), nameof(CTarget))]
|
||||
#pragma warning disable CS8774 // Member must have a non-null value when exiting.
|
||||
protected async Task<NetEntity> SpawnTarget(string prototype)
|
||||
protected async Task<NetEntity> SpawnTarget(string prototype, NetCoordinates? coords = null)
|
||||
{
|
||||
coords ??= TargetCoords;
|
||||
Target = NetEntity.Invalid;
|
||||
await Server.WaitPost(() =>
|
||||
{
|
||||
Target = SEntMan.GetNetEntity(SEntMan.SpawnAtPosition(prototype, SEntMan.GetCoordinates(TargetCoords)));
|
||||
Target = SEntMan.GetNetEntity(SEntMan.SpawnAtPosition(prototype, SEntMan.GetCoordinates(coords.Value)));
|
||||
});
|
||||
|
||||
await RunTicks(5);
|
||||
@@ -110,14 +112,16 @@ public abstract partial class InteractionTest
|
||||
#pragma warning restore CS8774 // Member must have a non-null value when exiting.
|
||||
|
||||
/// <summary>
|
||||
/// Spawn an entity entity at the target coordinates without setting it as the target.
|
||||
/// Spawn an entity entity at the given coordinates without setting it as the target.
|
||||
/// If no coordinates are given it will default to <see cref="TargetCoords"/>
|
||||
/// </summary>
|
||||
protected async Task<NetEntity> Spawn(string prototype)
|
||||
protected async Task<NetEntity> Spawn(string prototype, NetCoordinates? coords = null)
|
||||
{
|
||||
coords ??= TargetCoords;
|
||||
var entity = NetEntity.Invalid;
|
||||
await Server.WaitPost(() =>
|
||||
{
|
||||
entity = SEntMan.GetNetEntity(SEntMan.SpawnAtPosition(prototype, SEntMan.GetCoordinates(TargetCoords)));
|
||||
entity = SEntMan.GetNetEntity(SEntMan.SpawnAtPosition(prototype, SEntMan.GetCoordinates(coords.Value)));
|
||||
});
|
||||
|
||||
await RunTicks(5);
|
||||
@@ -407,6 +411,33 @@ public abstract partial class InteractionTest
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simulates a drag and drop mouse interaction from one entity to another.
|
||||
/// </summary>
|
||||
protected async Task DragDrop(NetEntity source, NetEntity target)
|
||||
{
|
||||
// ScreenCoordinates diff needs to be larger than DragDropSystem.Deadzone for the drag drop to initiate
|
||||
var screenX = CDragDropSys.Deadzone + 1f;
|
||||
|
||||
// Start drag
|
||||
await SetKey(EngineKeyFunctions.Use,
|
||||
BoundKeyState.Down,
|
||||
NetPosition(source),
|
||||
source,
|
||||
screenCoordinates: new ScreenCoordinates(screenX, 0f, WindowId.Main));
|
||||
|
||||
await RunTicks(3);
|
||||
|
||||
// End drag
|
||||
await SetKey(EngineKeyFunctions.Use,
|
||||
BoundKeyState.Up,
|
||||
NetPosition(target),
|
||||
target,
|
||||
screenCoordinates: new ScreenCoordinates(0f, 0f, WindowId.Main));
|
||||
|
||||
await RunTicks(3);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throw the currently held entity. Defaults to targeting the current <see cref="TargetCoords"/>
|
||||
/// </summary>
|
||||
@@ -478,11 +509,11 @@ public abstract partial class InteractionTest
|
||||
var wasInCombatMode = IsInCombatMode();
|
||||
await SetCombatMode(true);
|
||||
|
||||
Assert.That(SGun.TryGetGun(SPlayer, out var gunUid, out var gunComp), "Player was not holding a gun!");
|
||||
Assert.That(SGun.TryGetGun(SPlayer, out var gun), "Player was not holding a gun!");
|
||||
|
||||
await Server.WaitAssertion(() =>
|
||||
{
|
||||
var success = SGun.AttemptShoot(SPlayer, gunUid, gunComp!, actualTarget);
|
||||
var success = SGun.AttemptShoot(SPlayer, gun, actualTarget);
|
||||
if (assert)
|
||||
Assert.That(success, "Gun failed to shoot.");
|
||||
});
|
||||
@@ -517,11 +548,11 @@ public abstract partial class InteractionTest
|
||||
var wasInCombatMode = IsInCombatMode();
|
||||
await SetCombatMode(true);
|
||||
|
||||
Assert.That(SGun.TryGetGun(SPlayer, out var gunUid, out var gunComp), "Player was not holding a gun!");
|
||||
Assert.That(SGun.TryGetGun(SPlayer, out var gun), "Player was not holding a gun!");
|
||||
|
||||
await Server.WaitAssertion(() =>
|
||||
{
|
||||
var success = SGun.AttemptShoot(SPlayer, gunUid, gunComp!, Position(actualTarget!.Value), ToServer(actualTarget));
|
||||
var success = SGun.AttemptShoot(SPlayer, gun, Position(actualTarget!.Value), ToServer(actualTarget));
|
||||
if (assert)
|
||||
Assert.That(success, "Gun failed to shoot.");
|
||||
});
|
||||
@@ -839,7 +870,7 @@ public abstract partial class InteractionTest
|
||||
/// <param name="uid">The entity at which the events were directed</param>
|
||||
/// <param name="count">How many new events are expected</param>
|
||||
/// <param name="predicate">A predicate that can be used to filter the recorded events</param>
|
||||
protected void AssertEvent<TEvent>(EntityUid? uid = null, int count = 1, Func<TEvent,bool>? predicate = null)
|
||||
protected void AssertEvent<TEvent>(EntityUid? uid = null, int count = 1, Func<TEvent, bool>? predicate = null)
|
||||
where TEvent : notnull
|
||||
{
|
||||
Assert.That(GetEvents(uid, predicate).Count, Is.EqualTo(count));
|
||||
@@ -872,7 +903,7 @@ public abstract partial class InteractionTest
|
||||
where TEvent : notnull
|
||||
{
|
||||
if (_listenerCache.TryGetValue(typeof(TEvent), out var listener))
|
||||
return (TestListenerSystem<TEvent>) listener;
|
||||
return (TestListenerSystem<TEvent>)listener;
|
||||
|
||||
var type = Server.Resolve<IReflectionManager>().GetAllChildren<TestListenerSystem<TEvent>>().Single();
|
||||
if (!SEntMan.EntitySysManager.TryGetEntitySystem(type, out var systemObj))
|
||||
@@ -1607,6 +1638,7 @@ public abstract partial class InteractionTest
|
||||
|
||||
protected EntityCoordinates Position(NetEntity uid) => Position(ToServer(uid));
|
||||
protected EntityCoordinates Position(EntityUid uid) => Xform(uid).Coordinates;
|
||||
protected NetCoordinates NetPosition(NetEntity uid) => FromServer(Position(uid));
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Numerics;
|
||||
using Content.Client.Construction;
|
||||
using Content.Client.Examine;
|
||||
using Content.Client.Gameplay;
|
||||
using Content.Client.Interaction;
|
||||
using Content.IntegrationTests.Pair;
|
||||
using Content.Server.Hands.Systems;
|
||||
using Content.Server.Stack;
|
||||
@@ -134,6 +135,7 @@ public abstract partial class InteractionTest
|
||||
protected InteractionTestSystem CTestSystem = default!;
|
||||
protected ISawmill CLogger = default!;
|
||||
protected SharedUserInterfaceSystem CUiSys = default!;
|
||||
protected DragDropSystem CDragDropSys = default!;
|
||||
|
||||
// player components
|
||||
protected HandsComponent? Hands;
|
||||
@@ -208,6 +210,7 @@ public abstract partial class InteractionTest
|
||||
CConSys = CEntMan.System<ConstructionSystem>();
|
||||
ExamineSys = CEntMan.System<ExamineSystem>();
|
||||
CUiSys = CEntMan.System<SharedUserInterfaceSystem>();
|
||||
CDragDropSys = CEntMan.System<DragDropSystem>();
|
||||
|
||||
// Setup map.
|
||||
if (TestMapPath == null)
|
||||
|
||||
102
Content.IntegrationTests/Tests/Medical/DefibrillatorTest.cs
Normal file
102
Content.IntegrationTests/Tests/Medical/DefibrillatorTest.cs
Normal file
@@ -0,0 +1,102 @@
|
||||
#nullable enable
|
||||
using Content.IntegrationTests.Tests.Interaction;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Damage.Components;
|
||||
using Content.Shared.Damage.Prototypes;
|
||||
using Content.Shared.Damage.Systems;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Medical;
|
||||
using Content.Shared.Mobs;
|
||||
using Content.Shared.Mobs.Components;
|
||||
using Content.Shared.Mobs.Systems;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.Medical;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for defibrilators.
|
||||
/// </summary>
|
||||
[TestOf(typeof(DefibrillatorComponent))]
|
||||
public sealed class DefibrillatorTest : InteractionTest
|
||||
{
|
||||
// We need two hands to use a defbrillator.
|
||||
protected override string PlayerPrototype => "MobHuman";
|
||||
|
||||
private static readonly EntProtoId DefibrillatorProtoId = "Defibrillator";
|
||||
private static readonly EntProtoId TargetProtoId = "MobHuman";
|
||||
private static readonly ProtoId<DamageTypePrototype> BluntDamageTypeId = "Blunt";
|
||||
|
||||
/// <summary>
|
||||
/// Kills a target mob, heals them and then revives them with a defibrillator.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task KillAndReviveTest()
|
||||
{
|
||||
var damageableSystem = SEntMan.System<DamageableSystem>();
|
||||
var mobThresholdsSystem = SEntMan.System<MobThresholdSystem>();
|
||||
|
||||
// Don't let the player and target suffocate.
|
||||
await AddAtmosphere();
|
||||
|
||||
await SpawnTarget(TargetProtoId);
|
||||
|
||||
var targetMobState = Comp<MobStateComponent>();
|
||||
var targetDamageable = Comp<DamageableComponent>();
|
||||
|
||||
// Check that the target has no damage and is not crit or dead.
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(targetMobState.CurrentState, Is.EqualTo(MobState.Alive), "Target mob was not alive when spawned.");
|
||||
Assert.That(targetDamageable.TotalDamage, Is.EqualTo(FixedPoint2.Zero), "Target mob was damaged when spawned.");
|
||||
});
|
||||
|
||||
// Get the damage needed to kill or crit the target.
|
||||
var critThreshold = mobThresholdsSystem.GetThresholdForState(STarget.Value, MobState.Critical);
|
||||
var deathThreshold = mobThresholdsSystem.GetThresholdForState(STarget.Value, MobState.Dead);
|
||||
var critDamage = new DamageSpecifier(ProtoMan.Index(BluntDamageTypeId), (critThreshold + deathThreshold) / 2);
|
||||
var deathDamage = new DamageSpecifier(ProtoMan.Index(BluntDamageTypeId), deathThreshold);
|
||||
|
||||
// Kill the target by applying blunt damage.
|
||||
await Server.WaitPost(() => damageableSystem.SetDamage((STarget.Value, targetDamageable), deathDamage));
|
||||
await RunTicks(3);
|
||||
|
||||
// Check that the target is dead.
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(targetMobState.CurrentState, Is.EqualTo(MobState.Dead), "Target mob did not die from deadly damage amount.");
|
||||
Assert.That(targetDamageable.TotalDamage, Is.EqualTo(deathThreshold), "Target mob had the wrong total damage amount after being killed.");
|
||||
});
|
||||
|
||||
// Spawn a defib and activate it.
|
||||
var defib = await PlaceInHands(DefibrillatorProtoId, enableToggleable: true);
|
||||
var cooldown = Comp<DefibrillatorComponent>(defib).ZapDelay;
|
||||
|
||||
// Wait for the cooldown.
|
||||
await RunSeconds((float)cooldown.TotalSeconds);
|
||||
|
||||
// ZAP!
|
||||
await Interact();
|
||||
|
||||
// Check that the target is still dead since it is over the crit threshold.
|
||||
// And it should have taken some extra damage.
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(targetMobState.CurrentState, Is.EqualTo(MobState.Dead), "Target mob was revived despite being over the death damage threshold.");
|
||||
Assert.That(targetDamageable.TotalDamage, Is.GreaterThan(deathThreshold), "Target mob did not take damage from being defibrillated.");
|
||||
});
|
||||
|
||||
// Set the damage halfway between the crit and death thresholds so that the target can be revived.
|
||||
await Server.WaitPost(() => damageableSystem.SetDamage((STarget.Value, targetDamageable), critDamage));
|
||||
await RunTicks(3);
|
||||
|
||||
// Check that the target is still dead.
|
||||
Assert.That(targetMobState.CurrentState, Is.EqualTo(MobState.Dead), "Target mob revived on its own.");
|
||||
|
||||
// ZAP!
|
||||
await RunSeconds((float)cooldown.TotalSeconds);
|
||||
await Interact();
|
||||
|
||||
// The target should be revived, but in crit.
|
||||
Assert.That(targetMobState.CurrentState, Is.EqualTo(MobState.Critical), "Target mob was not revived from being defibrillated.");
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
using Content.Client.Interaction;
|
||||
using Content.IntegrationTests.Tests.Interaction;
|
||||
using Content.IntegrationTests.Tests.Interaction;
|
||||
using Content.Shared.Strip.Components;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Map;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.Strip;
|
||||
|
||||
@@ -10,37 +8,22 @@ public sealed class StrippableTest : InteractionTest
|
||||
{
|
||||
protected override string PlayerPrototype => "MobHuman";
|
||||
|
||||
/// <summary>
|
||||
/// Tests that the stripping UI is opened when drag dropping from another mob onto the player.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task DragDropOpensStrip()
|
||||
{
|
||||
// Spawn one tile away
|
||||
TargetCoords = SEntMan.GetNetCoordinates(new EntityCoordinates(MapData.MapUid, 1, 0));
|
||||
await SpawnTarget("MobHuman");
|
||||
|
||||
var userInterface = Comp<UserInterfaceComponent>(Target);
|
||||
Assert.That(userInterface.Actors.Count == 0);
|
||||
Assert.That(userInterface.Actors, Is.Empty);
|
||||
|
||||
// screenCoordinates diff needs to be larger than DragDropSystem._deadzone
|
||||
var screenX = CEntMan.System<DragDropSystem>().Deadzone + 1f;
|
||||
await DragDrop(Target.Value, Player);
|
||||
|
||||
// Start drag
|
||||
await SetKey(EngineKeyFunctions.Use,
|
||||
BoundKeyState.Down,
|
||||
TargetCoords,
|
||||
Target,
|
||||
screenCoordinates: new ScreenCoordinates(screenX, 0f, WindowId.Main));
|
||||
Assert.That(userInterface.Actors, Is.Not.Empty);
|
||||
|
||||
await RunTicks(5);
|
||||
|
||||
// End drag
|
||||
await SetKey(EngineKeyFunctions.Use,
|
||||
BoundKeyState.Up,
|
||||
PlayerCoords,
|
||||
Player,
|
||||
screenCoordinates: new ScreenCoordinates(0f, 0f, WindowId.Main));
|
||||
|
||||
await RunTicks(5);
|
||||
|
||||
Assert.That(userInterface.Actors.Count > 0);
|
||||
Assert.That(CUiSys.IsUiOpen(CTarget.Value, StrippingUiKey.Key));
|
||||
Assert.That(SUiSys.IsUiOpen(STarget.Value, StrippingUiKey.Key));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ using Content.Shared.Damage.Components;
|
||||
using Content.Shared.Damage.Systems;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Electrocution;
|
||||
using Content.Shared.Gibbing;
|
||||
using Content.Shared.Gravity;
|
||||
using Content.Shared.Interaction.Components;
|
||||
using Content.Shared.Inventory;
|
||||
@@ -92,6 +93,7 @@ public sealed partial class AdminVerbSystem
|
||||
[Dependency] private readonly SharedTransformSystem _transformSystem = default!;
|
||||
[Dependency] private readonly SuperBonkSystem _superBonkSystem = default!;
|
||||
[Dependency] private readonly SlipperySystem _slipperySystem = default!;
|
||||
[Dependency] private readonly GibbingSystem _gibbing = default!;
|
||||
|
||||
private readonly EntProtoId _actionViewLawsProtoId = "ActionViewLaws";
|
||||
private readonly ProtoId<SiliconLawsetPrototype> _crewsimovLawset = "Crewsimov";
|
||||
@@ -128,7 +130,7 @@ public sealed partial class AdminVerbSystem
|
||||
4, 1, 2, args.Target, maxTileBreak: 0), // it gibs, damage doesn't need to be high.
|
||||
CancellationToken.None);
|
||||
|
||||
_bodySystem.GibBody(args.Target);
|
||||
_gibbing.Gib(args.Target);
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = string.Join(": ", explodeName, Loc.GetString("admin-smite-explode-description")) // we do this so the description tells admins the Text to run it via console.
|
||||
|
||||
@@ -722,7 +722,7 @@ public sealed partial class AdminVerbSystem
|
||||
return;
|
||||
|
||||
_gun.SetBallisticUnspawned((args.Target, ballisticAmmo), result);
|
||||
_gun.UpdateBallisticAppearance(args.Target, ballisticAmmo);
|
||||
_gun.UpdateBallisticAppearance((args.Target, ballisticAmmo));
|
||||
});
|
||||
},
|
||||
Impact = LogImpact.Medium,
|
||||
|
||||
@@ -10,6 +10,7 @@ using Content.Shared.Anomaly.Effects;
|
||||
using Content.Shared.Body.Components;
|
||||
using Content.Shared.Chat;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Gibbing;
|
||||
using Content.Shared.Mobs;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Whitelist;
|
||||
@@ -25,7 +26,7 @@ public sealed class InnerBodyAnomalySystem : SharedInnerBodyAnomalySystem
|
||||
[Dependency] private readonly IAdminLogManager _adminLog = default!;
|
||||
[Dependency] private readonly AnomalySystem _anomaly = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
[Dependency] private readonly BodySystem _body = default!;
|
||||
[Dependency] private readonly GibbingSystem _gibbing = default!;
|
||||
[Dependency] private readonly IChatManager _chat = default!;
|
||||
[Dependency] private readonly ISharedPlayerManager _player = default!;
|
||||
[Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
|
||||
@@ -134,7 +135,7 @@ public sealed class InnerBodyAnomalySystem : SharedInnerBodyAnomalySystem
|
||||
if (!TryComp<BodyComponent>(ent, out var body))
|
||||
return;
|
||||
|
||||
_body.GibBody(ent, true, body, splatModifier: 5f);
|
||||
_gibbing.Gib(ent.Owner);
|
||||
}
|
||||
|
||||
private void OnSeverityChanged(Entity<InnerBodyAnomalyComponent> ent, ref AnomalySeverityChangedEvent args)
|
||||
|
||||
@@ -13,7 +13,7 @@ using Content.Server.Players.PlayTimeTracking;
|
||||
using Content.Server.Preferences.Managers;
|
||||
using Content.Server.Roles;
|
||||
using Content.Server.Roles.Jobs;
|
||||
using Content.Server.Shuttles.Components;
|
||||
using Content.Server.Shuttles.Systems;
|
||||
using Content.Shared.Administration.Logs;
|
||||
using Content.Shared.Antag;
|
||||
using Content.Shared.Clothing;
|
||||
@@ -54,6 +54,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
||||
[Dependency] private readonly TransformSystem _transform = default!;
|
||||
[Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
|
||||
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
|
||||
[Dependency] private readonly ArrivalsSystem _arrivals = default!;
|
||||
|
||||
// arbitrary random number to give late joining some mild interest.
|
||||
public const float LateJoinRandomChance = 0.5f;
|
||||
@@ -168,6 +169,15 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
||||
if (!args.LateJoin)
|
||||
return;
|
||||
|
||||
TryMakeLateJoinAntag(args.Player);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempt to make this player be a late-join antag.
|
||||
/// </summary>
|
||||
/// <param name="session">The session to attempt to make antag.</param>
|
||||
public void TryMakeLateJoinAntag(ICommonSession session)
|
||||
{
|
||||
// TODO: this really doesn't handle multiple latejoin definitions well
|
||||
// eventually this should probably store the players per definition with some kind of unique identifier.
|
||||
// something to figure out later.
|
||||
@@ -197,7 +207,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
||||
if (!TryGetNextAvailableDefinition((uid, antag), out var def, players))
|
||||
continue;
|
||||
|
||||
if (TryMakeAntag((uid, antag), args.Player, def.Value))
|
||||
if (TryMakeAntag((uid, antag), session, def.Value))
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -577,7 +587,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
||||
if (entity == null)
|
||||
return true;
|
||||
|
||||
if (HasComp<PendingClockInComponent>(entity))
|
||||
if (_arrivals.IsOnArrivals((entity.Value, null)))
|
||||
return false;
|
||||
|
||||
if (!def.AllowNonHumans && !HasComp<HumanoidAppearanceComponent>(entity))
|
||||
|
||||
@@ -40,15 +40,10 @@ namespace Content.Server.Atmos.Components
|
||||
// depressurizing a room. However it can also effectively be used as a means of generating gasses for free
|
||||
// TODO ATMOS Mass conservation. Make it actually push/pull air from adjacent tiles instead of destroying & creating,
|
||||
|
||||
|
||||
// TODO ATMOS Do we need these two fields?
|
||||
// TODO ATMOS slate for removal. Stuff doesn't use this.
|
||||
[DataField("rotateAirBlocked")]
|
||||
public bool RotateAirBlocked { get; set; } = true;
|
||||
|
||||
// TODO ATMOS remove this? What is this even for??
|
||||
[DataField("fixAirBlockedDirectionInitialize")]
|
||||
public bool FixAirBlockedDirectionInitialize { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// If true, then the tile that this entity is on will have no air at all if all directions are blocked.
|
||||
/// </summary>
|
||||
|
||||
@@ -25,13 +25,17 @@ namespace Content.Server.Atmos.EntitySystems
|
||||
|
||||
private void OnAirtightInit(Entity<AirtightComponent> airtight, ref ComponentInit args)
|
||||
{
|
||||
// TODO AIRTIGHT what FixAirBlockedDirectionInitialize even for?
|
||||
if (!airtight.Comp.FixAirBlockedDirectionInitialize)
|
||||
// If this entity blocks air in all directions (e.g. full tile walls, doors, and windows)
|
||||
// we can skip some expensive logic.
|
||||
if (airtight.Comp.InitialAirBlockedDirection == (int)AtmosDirection.All)
|
||||
{
|
||||
airtight.Comp.CurrentAirBlockedDirection = airtight.Comp.InitialAirBlockedDirection;
|
||||
UpdatePosition(airtight);
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise we need to determine its current blocked air directions based on rotation
|
||||
// and check if it's still airtight.
|
||||
var xform = Transform(airtight);
|
||||
airtight.Comp.CurrentAirBlockedDirection =
|
||||
(int) Rotate((AtmosDirection) airtight.Comp.InitialAirBlockedDirection, xform.LocalRotation);
|
||||
|
||||
@@ -300,6 +300,8 @@ public partial class AtmosphereSystem
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a tile on a grid is air-blocked in the specified directions.
|
||||
/// This only checks for if the current tile, and only the current tile, is blocking
|
||||
/// air.
|
||||
/// </summary>
|
||||
/// <param name="gridUid">The grid to check.</param>
|
||||
/// <param name="tile">The tile on the grid to check.</param>
|
||||
@@ -323,6 +325,8 @@ public partial class AtmosphereSystem
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a tile on a grid is air-blocked in the specified directions, using cached data.
|
||||
/// This only checks for if the current tile, and only the current tile, is blocking
|
||||
/// air.
|
||||
/// </summary>
|
||||
/// <param name="grid">The grid to check.</param>
|
||||
/// <param name="tile">The tile on the grid to check.</param>
|
||||
|
||||
@@ -25,7 +25,6 @@ namespace Content.Server.Atmos.EntitySystems
|
||||
public float AtmosMaxProcessTime { get; private set; }
|
||||
public float AtmosTickRate { get; private set; }
|
||||
public float Speedup { get; private set; }
|
||||
public float HeatScale { get; private set; }
|
||||
public bool DeltaPressureDamage { get; private set; }
|
||||
public int DeltaPressureParallelProcessPerIteration { get; private set; }
|
||||
public int DeltaPressureParallelBatchSize { get; private set; }
|
||||
@@ -55,7 +54,6 @@ namespace Content.Server.Atmos.EntitySystems
|
||||
Subs.CVar(_cfg, CCVars.AtmosMaxProcessTime, value => AtmosMaxProcessTime = value, true);
|
||||
Subs.CVar(_cfg, CCVars.AtmosTickRate, value => AtmosTickRate = value, true);
|
||||
Subs.CVar(_cfg, CCVars.AtmosSpeedup, value => Speedup = value, true);
|
||||
Subs.CVar(_cfg, CCVars.AtmosHeatScale, value => { HeatScale = value; InitializeGases(); }, true);
|
||||
Subs.CVar(_cfg, CCVars.ExcitedGroups, value => ExcitedGroups = value, true);
|
||||
Subs.CVar(_cfg, CCVars.ExcitedGroupsSpaceIsAllConsuming, value => ExcitedGroupsSpaceIsAllConsuming = value, true);
|
||||
Subs.CVar(_cfg, CCVars.DeltaPressureDamage, value => DeltaPressureDamage = value, true);
|
||||
|
||||
@@ -13,53 +13,23 @@ namespace Content.Server.Atmos.EntitySystems
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _protoMan = default!;
|
||||
|
||||
private GasReactionPrototype[] _gasReactions = Array.Empty<GasReactionPrototype>();
|
||||
private float[] _gasSpecificHeats = new float[Atmospherics.TotalNumberOfGases];
|
||||
private GasReactionPrototype[] _gasReactions = [];
|
||||
|
||||
/// <summary>
|
||||
/// List of gas reactions ordered by priority.
|
||||
/// </summary>
|
||||
public IEnumerable<GasReactionPrototype> GasReactions => _gasReactions;
|
||||
|
||||
/// <summary>
|
||||
/// Cached array of gas specific heats.
|
||||
/// </summary>
|
||||
public float[] GasSpecificHeats => _gasSpecificHeats;
|
||||
|
||||
private void InitializeGases()
|
||||
public override void InitializeGases()
|
||||
{
|
||||
base.InitializeGases();
|
||||
|
||||
_gasReactions = _protoMan.EnumeratePrototypes<GasReactionPrototype>().ToArray();
|
||||
Array.Sort(_gasReactions, (a, b) => b.Priority.CompareTo(a.Priority));
|
||||
|
||||
Array.Resize(ref _gasSpecificHeats, MathHelper.NextMultipleOf(Atmospherics.TotalNumberOfGases, 4));
|
||||
|
||||
for (var i = 0; i < GasPrototypes.Length; i++)
|
||||
{
|
||||
_gasSpecificHeats[i] = GasPrototypes[i].SpecificHeat / HeatScale;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the heat capacity for a gas mixture.
|
||||
/// </summary>
|
||||
/// <param name="mixture">The mixture whose heat capacity should be calculated</param>
|
||||
/// <param name="applyScaling"> Whether the internal heat capacity scaling should be applied. This should not be
|
||||
/// used outside of atmospheric related heat transfer.</param>
|
||||
/// <returns></returns>
|
||||
public float GetHeatCapacity(GasMixture mixture, bool applyScaling)
|
||||
{
|
||||
var scale = GetHeatCapacityCalculation(mixture.Moles, mixture.Immutable);
|
||||
|
||||
// By default GetHeatCapacityCalculation() has the heat-scale divisor pre-applied.
|
||||
// So if we want the un-scaled heat capacity, we have to multiply by the scale.
|
||||
return applyScaling ? scale : scale * HeatScale;
|
||||
}
|
||||
|
||||
private float GetHeatCapacity(GasMixture mixture)
|
||||
=> GetHeatCapacityCalculation(mixture.Moles, mixture.Immutable);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private float GetHeatCapacityCalculation(float[] moles, bool space)
|
||||
protected override float GetHeatCapacityCalculation(float[] moles, bool space)
|
||||
{
|
||||
// Little hack to make space gas mixtures have heat capacity, therefore allowing them to cool down rooms.
|
||||
if (space && MathHelper.CloseTo(NumericsHelpers.HorizontalAdd(moles), 0f))
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Atmos.Components;
|
||||
using Content.Server.NodeContainer;
|
||||
using Content.Server.NodeContainer.Nodes;
|
||||
using Content.Server.Popups;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Atmos.Components;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Interaction.Events;
|
||||
using Content.Shared.NodeContainer;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Server.GameObjects;
|
||||
@@ -159,16 +157,8 @@ public sealed class GasAnalyzerSystem : EntitySystem
|
||||
|
||||
// Fetch the environmental atmosphere around the scanner. This must be the first entry
|
||||
var tileMixture = _atmo.GetContainingMixture(uid, true);
|
||||
if (tileMixture != null)
|
||||
{
|
||||
gasMixList.Add(new GasMixEntry(Loc.GetString("gas-analyzer-window-environment-tab-label"), tileMixture.Volume, tileMixture.Pressure, tileMixture.Temperature,
|
||||
GenerateGasEntryArray(tileMixture)));
|
||||
}
|
||||
else
|
||||
{
|
||||
// No gases were found
|
||||
gasMixList.Add(new GasMixEntry(Loc.GetString("gas-analyzer-window-environment-tab-label"), 0f, 0f, 0f));
|
||||
}
|
||||
var tileMixtureName = Loc.GetString("gas-analyzer-window-environment-tab-label");
|
||||
gasMixList.Add(GenerateGasMixEntry(tileMixtureName, tileMixture));
|
||||
|
||||
var deviceFlipped = false;
|
||||
if (component.Target != null)
|
||||
@@ -192,7 +182,7 @@ public sealed class GasAnalyzerSystem : EntitySystem
|
||||
{
|
||||
if (mixes.Item2 != null)
|
||||
{
|
||||
gasMixList.Add(new GasMixEntry(mixes.Item1, mixes.Item2.Volume, mixes.Item2.Pressure, mixes.Item2.Temperature, GenerateGasEntryArray(mixes.Item2)));
|
||||
gasMixList.Add(GenerateGasMixEntry(mixes.Item1, mixes.Item2));
|
||||
validTarget = true;
|
||||
}
|
||||
}
|
||||
@@ -215,7 +205,7 @@ public sealed class GasAnalyzerSystem : EntitySystem
|
||||
var pipeAir = pipeNode.Air.Clone();
|
||||
pipeAir.Multiply(pipeNode.Volume / pipeNode.Air.Volume);
|
||||
pipeAir.Volume = pipeNode.Volume;
|
||||
gasMixList.Add(new GasMixEntry(pair.Key, pipeAir.Volume, pipeAir.Pressure, pipeAir.Temperature, GenerateGasEntryArray(pipeAir)));
|
||||
gasMixList.Add(GenerateGasMixEntry(pair.Key, pipeAir));
|
||||
validTarget = true;
|
||||
}
|
||||
}
|
||||
@@ -242,6 +232,23 @@ public sealed class GasAnalyzerSystem : EntitySystem
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a GasMixEntry for a given GasMixture
|
||||
/// </summary>
|
||||
public GasMixEntry GenerateGasMixEntry(string name, GasMixture? mixture)
|
||||
{
|
||||
if (mixture == null)
|
||||
return new GasMixEntry(name, 0, 0, 0);
|
||||
|
||||
return new GasMixEntry(
|
||||
name,
|
||||
mixture.Volume,
|
||||
mixture.Pressure,
|
||||
mixture.Temperature,
|
||||
GenerateGasEntryArray(mixture)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a GasEntry array for a given GasMixture
|
||||
/// </summary>
|
||||
|
||||
@@ -27,12 +27,14 @@ namespace Content.Server.Atmos.Reactions
|
||||
burnedFuel = initialTrit;
|
||||
|
||||
mixture.AdjustMoles(Gas.Tritium, -burnedFuel);
|
||||
mixture.AdjustMoles(Gas.Oxygen, -burnedFuel / Atmospherics.TritiumBurnFuelRatio);
|
||||
}
|
||||
else
|
||||
{
|
||||
burnedFuel = initialTrit;
|
||||
mixture.SetMoles(Gas.Tritium, mixture.GetMoles(Gas.Tritium ) * (1 - 1 / Atmospherics.TritiumBurnTritFactor));
|
||||
mixture.AdjustMoles(Gas.Oxygen, -mixture.GetMoles(Gas.Tritium));
|
||||
// Limit the amount of fuel burned by the limiting reactant, either our initial tritium or the amount of oxygen available given the burn ratio.
|
||||
burnedFuel = Math.Min(initialTrit, mixture.GetMoles(Gas.Oxygen) / Atmospherics.TritiumBurnFuelRatio) / Atmospherics.TritiumBurnTritFactor;
|
||||
mixture.AdjustMoles(Gas.Tritium, -burnedFuel);
|
||||
mixture.AdjustMoles(Gas.Oxygen, -burnedFuel / Atmospherics.TritiumBurnFuelRatio);
|
||||
energyReleased += (Atmospherics.FireHydrogenEnergyReleased * burnedFuel * (Atmospherics.TritiumBurnTritFactor - 1));
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ using Content.Shared.Atmos;
|
||||
using Content.Shared.Atmos.Rotting;
|
||||
using Content.Shared.Body.Events;
|
||||
using Content.Shared.Damage.Systems;
|
||||
using Content.Shared.Gibbing;
|
||||
using Content.Shared.Temperature.Components;
|
||||
using Robust.Server.Containers;
|
||||
using Robust.Shared.Physics.Components;
|
||||
@@ -21,12 +22,12 @@ public sealed class RottingSystem : SharedRottingSystem
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<RottingComponent, BeingGibbedEvent>(OnGibbed);
|
||||
SubscribeLocalEvent<RottingComponent, GibbedBeforeDeletionEvent>(OnGibbed);
|
||||
|
||||
SubscribeLocalEvent<TemperatureComponent, IsRottingEvent>(OnTempIsRotting);
|
||||
}
|
||||
|
||||
private void OnGibbed(EntityUid uid, RottingComponent component, BeingGibbedEvent args)
|
||||
private void OnGibbed(EntityUid uid, RottingComponent component, GibbedBeforeDeletionEvent args)
|
||||
{
|
||||
if (!TryComp<PhysicsComponent>(uid, out var physics))
|
||||
return;
|
||||
@@ -72,19 +73,21 @@ public sealed class RottingSystem : SharedRottingSystem
|
||||
{
|
||||
if (_timing.CurTime < perishable.RotNextUpdate)
|
||||
continue;
|
||||
|
||||
perishable.RotNextUpdate += perishable.PerishUpdateRate;
|
||||
|
||||
var stage = PerishStage((uid, perishable), MaxStages);
|
||||
if (stage != perishable.Stage)
|
||||
{
|
||||
perishable.Stage = stage;
|
||||
Dirty(uid, perishable);
|
||||
DirtyField(uid, perishable, nameof(PerishableComponent.Stage));
|
||||
}
|
||||
|
||||
if (IsRotten(uid) || !IsRotProgressing(uid, perishable))
|
||||
continue;
|
||||
|
||||
perishable.RotAccumulator += perishable.PerishUpdateRate * GetRotRate(uid);
|
||||
DirtyField(uid, perishable, nameof(PerishableComponent.RotAccumulator));
|
||||
if (perishable.RotAccumulator >= perishable.RotAfter)
|
||||
{
|
||||
var rot = AddComp<RottingComponent>(uid);
|
||||
|
||||
@@ -92,40 +92,4 @@ public sealed class BodySystem : SharedBodySystem
|
||||
var layers = HumanoidVisualLayersExtension.Sublayers(layer.Value);
|
||||
_humanoidSystem.SetLayersVisibility((bodyEnt, humanoid), layers, visible: false);
|
||||
}
|
||||
|
||||
public override HashSet<EntityUid> GibBody(
|
||||
EntityUid bodyId,
|
||||
bool gibOrgans = false,
|
||||
BodyComponent? body = null,
|
||||
bool launchGibs = true,
|
||||
Vector2? splatDirection = null,
|
||||
float splatModifier = 1,
|
||||
Angle splatCone = default,
|
||||
SoundSpecifier? gibSoundOverride = null
|
||||
)
|
||||
{
|
||||
if (!Resolve(bodyId, ref body, logMissing: false)
|
||||
|| TerminatingOrDeleted(bodyId)
|
||||
|| EntityManager.IsQueuedForDeletion(bodyId))
|
||||
{
|
||||
return new HashSet<EntityUid>();
|
||||
}
|
||||
|
||||
if (HasComp<GodmodeComponent>(bodyId))
|
||||
return new HashSet<EntityUid>();
|
||||
|
||||
var xform = Transform(bodyId);
|
||||
if (xform.MapUid is null)
|
||||
return new HashSet<EntityUid>();
|
||||
|
||||
var gibs = base.GibBody(bodyId, gibOrgans, body, launchGibs: launchGibs,
|
||||
splatDirection: splatDirection, splatModifier: splatModifier, splatCone:splatCone);
|
||||
|
||||
var ev = new BeingGibbedEvent(gibs);
|
||||
RaiseLocalEvent(bodyId, ref ev);
|
||||
|
||||
QueueDel(bodyId);
|
||||
|
||||
return gibs;
|
||||
}
|
||||
}
|
||||
|
||||
70
Content.Server/Buckle/Systems/IgniteOnBuckleSystem.cs
Normal file
70
Content.Server/Buckle/Systems/IgniteOnBuckleSystem.cs
Normal file
@@ -0,0 +1,70 @@
|
||||
using Content.Server.Atmos.EntitySystems;
|
||||
using Content.Shared.Atmos.Components;
|
||||
using Content.Shared.Buckle.Components;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Server.Buckle.Systems;
|
||||
|
||||
public sealed class IgniteOnBuckleSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly FlammableSystem _flammable = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<IgniteOnBuckleComponent, StrappedEvent>(OnStrapped);
|
||||
SubscribeLocalEvent<IgniteOnBuckleComponent, UnstrappedEvent>(OnUnstrapped);
|
||||
|
||||
SubscribeLocalEvent<ActiveIgniteOnBuckleComponent, MapInitEvent>(ActiveOnInit);
|
||||
}
|
||||
|
||||
private void OnStrapped(Entity<IgniteOnBuckleComponent> ent, ref StrappedEvent args)
|
||||
{
|
||||
// We cache the values here to the other component.
|
||||
// This is done so we have to do less lookups
|
||||
var comp = EnsureComp<ActiveIgniteOnBuckleComponent>(args.Buckle);
|
||||
comp.FireStacks = ent.Comp.FireStacks;
|
||||
comp.MaxFireStacks = ent.Comp.MaxFireStacks;
|
||||
comp.IgniteTime = ent.Comp.IgniteTime;
|
||||
}
|
||||
|
||||
private void ActiveOnInit(Entity<ActiveIgniteOnBuckleComponent> ent, ref MapInitEvent args)
|
||||
{
|
||||
// Handle this via a separate MapInit so the component can be added by itself if need be.
|
||||
ent.Comp.NextIgniteTime = _timing.CurTime + ent.Comp.NextIgniteTime;
|
||||
Dirty(ent);
|
||||
}
|
||||
|
||||
private void OnUnstrapped(Entity<IgniteOnBuckleComponent> ent, ref UnstrappedEvent args)
|
||||
{
|
||||
RemCompDeferred<ActiveIgniteOnBuckleComponent>(args.Buckle);
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
|
||||
var curTime = _timing.CurTime;
|
||||
|
||||
var query = EntityQueryEnumerator<ActiveIgniteOnBuckleComponent, FlammableComponent>();
|
||||
while (query.MoveNext(out var uid, out var igniteComponent, out var flammableComponent))
|
||||
{
|
||||
if (curTime < igniteComponent.NextIgniteTime)
|
||||
continue;
|
||||
|
||||
igniteComponent.NextIgniteTime += TimeSpan.FromSeconds(igniteComponent.IgniteTime);
|
||||
Dirty(uid, igniteComponent);
|
||||
|
||||
if (flammableComponent.FireStacks > igniteComponent.MaxFireStacks)
|
||||
continue;
|
||||
|
||||
var stacks = flammableComponent.FireStacks + igniteComponent.FireStacks;
|
||||
if (igniteComponent.MaxFireStacks.HasValue)
|
||||
stacks = Math.Min(stacks, igniteComponent.MaxFireStacks.Value);
|
||||
|
||||
_flammable.SetFireStacks(uid, stacks, flammableComponent, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -94,6 +94,7 @@ public sealed class ChatSanitizationManager : IChatSanitizationManager
|
||||
Entry(".-.", "chatsan-confused"),
|
||||
Entry("-_-", "chatsan-unimpressed"),
|
||||
Entry("smh", "chatsan-unimpressed"),
|
||||
Entry(":?", "chatsan-shrugs"),
|
||||
Entry("o/", "chatsan-waves"),
|
||||
Entry("^^/", "chatsan-waves"),
|
||||
Entry(":/", "chatsan-uncertain"),
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
using Content.Server.Audio;
|
||||
using Content.Server.Power.Components;
|
||||
using Content.Server.Power.EntitySystems;
|
||||
using Content.Shared.Construction;
|
||||
using Content.Shared.Construction.Components;
|
||||
using Content.Shared.Containers.ItemSlots;
|
||||
using Content.Shared.Power;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Server.Construction;
|
||||
@@ -35,16 +33,8 @@ public sealed class FlatpackSystem : SharedFlatpackSystem
|
||||
if (!_itemSlots.TryGetSlot(uid, comp.SlotId, out var itemSlot) || itemSlot.Item is not { } board)
|
||||
return;
|
||||
|
||||
Dictionary<string, int> cost;
|
||||
if (TryComp<MachineBoardComponent>(board, out var machine))
|
||||
cost = GetFlatpackCreationCost(ent, (board, machine));
|
||||
else if (TryComp<ComputerBoardComponent>(board, out var computer) && computer.Prototype != null)
|
||||
cost = GetFlatpackCreationCost(ent, null);
|
||||
else
|
||||
{
|
||||
Log.Error($"Encountered invalid flatpack board while packing: {ToPrettyString(board)}");
|
||||
if (!TryGetFlatpackCreationCost(ent, board, out var cost))
|
||||
return;
|
||||
}
|
||||
|
||||
if (!MaterialStorage.CanChangeMaterialAmount(uid, cost))
|
||||
return;
|
||||
@@ -80,29 +70,15 @@ public sealed class FlatpackSystem : SharedFlatpackSystem
|
||||
if (!_itemSlots.TryGetSlot(uid, comp.SlotId, out var itemSlot) || itemSlot.Item is not { } board)
|
||||
return;
|
||||
|
||||
Dictionary<string, int> cost;
|
||||
EntProtoId proto;
|
||||
if (TryComp<MachineBoardComponent>(board, out var machine))
|
||||
{
|
||||
cost = GetFlatpackCreationCost(ent, (board, machine));
|
||||
proto = machine.Prototype;
|
||||
}
|
||||
else if (TryComp<ComputerBoardComponent>(board, out var computer) && computer.Prototype != null)
|
||||
{
|
||||
cost = GetFlatpackCreationCost(ent, null);
|
||||
proto = computer.Prototype;
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Error($"Encountered invalid flatpack board while packing: {ToPrettyString(board)}");
|
||||
if (!TryGetFlatpackCreationCost(ent, board, out var cost) ||
|
||||
!TryGetFlatpackResultPrototype(board, out var proto))
|
||||
return;
|
||||
}
|
||||
|
||||
if (!MaterialStorage.TryChangeMaterialAmount((ent, null), cost))
|
||||
return;
|
||||
|
||||
var flatpack = Spawn(comp.BaseFlatpackPrototype, Transform(ent).Coordinates);
|
||||
SetupFlatpack(flatpack, proto, board);
|
||||
SetupFlatpack(flatpack, proto.Value, board);
|
||||
Del(board);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Content.Server.Administration.Logs;
|
||||
using Content.Server.Atmos.EntitySystems;
|
||||
using Content.Server.Body.Systems;
|
||||
using Content.Server.Construction;
|
||||
using Content.Server.Destructible.Thresholds;
|
||||
using Content.Server.Destructible.Thresholds.Behaviors;
|
||||
@@ -16,6 +15,7 @@ using Content.Shared.Database;
|
||||
using Content.Shared.Destructible;
|
||||
using Content.Shared.Destructible.Thresholds.Triggers;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Gibbing;
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.Trigger.Systems;
|
||||
using JetBrains.Annotations;
|
||||
@@ -34,7 +34,7 @@ namespace Content.Server.Destructible
|
||||
|
||||
[Dependency] public readonly AtmosphereSystem AtmosphereSystem = default!;
|
||||
[Dependency] public readonly AudioSystem AudioSystem = default!;
|
||||
[Dependency] public readonly BodySystem BodySystem = default!;
|
||||
[Dependency] public readonly GibbingSystem Gibbing = default!;
|
||||
[Dependency] public readonly ConstructionSystem ConstructionSystem = default!;
|
||||
[Dependency] public readonly ExplosionSystem ExplosionSystem = default!;
|
||||
[Dependency] public readonly StackSystem StackSystem = default!;
|
||||
|
||||
@@ -11,6 +11,11 @@ namespace Content.Server.Destructible.Thresholds.Behaviors;
|
||||
[DataDefinition]
|
||||
public sealed partial class BurnBodyBehavior : IThresholdBehavior
|
||||
{
|
||||
/// <summary>
|
||||
/// The popup displayed upon destruction.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public LocId PopupMessage = "bodyburn-text-others";
|
||||
|
||||
public void Execute(EntityUid bodyId, DestructibleSystem system, EntityUid? cause = null)
|
||||
{
|
||||
@@ -27,7 +32,7 @@ public sealed partial class BurnBodyBehavior : IThresholdBehavior
|
||||
}
|
||||
|
||||
var bodyIdentity = Identity.Entity(bodyId, system.EntityManager);
|
||||
sharedPopupSystem.PopupCoordinates(Loc.GetString("bodyburn-text-others", ("name", bodyIdentity)), transformSystem.GetMoverCoordinates(bodyId), PopupType.LargeCaution);
|
||||
sharedPopupSystem.PopupCoordinates(Loc.GetString(PopupMessage, ("name", bodyIdentity)), transformSystem.GetMoverCoordinates(bodyId), PopupType.LargeCaution);
|
||||
|
||||
system.EntityManager.QueueDeleteEntity(bodyId);
|
||||
}
|
||||
|
||||
@@ -14,10 +14,7 @@ namespace Content.Server.Destructible.Thresholds.Behaviors
|
||||
|
||||
public void Execute(EntityUid owner, DestructibleSystem system, EntityUid? cause = null)
|
||||
{
|
||||
if (system.EntityManager.TryGetComponent(owner, out BodyComponent? body))
|
||||
{
|
||||
system.BodySystem.GibBody(owner, _recursive, body);
|
||||
}
|
||||
system.Gibbing.Gib(owner, _recursive);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,18 +27,18 @@ public sealed partial class GunSignalControlSystem : EntitySystem
|
||||
return;
|
||||
|
||||
if (args.Port == gunControl.Comp.TriggerPort)
|
||||
_gun.AttemptShoot(gunControl, gun);
|
||||
_gun.AttemptShoot((gunControl, gun));
|
||||
|
||||
if (!TryComp<AutoShootGunComponent>(gunControl, out var autoShoot))
|
||||
return;
|
||||
|
||||
if (args.Port == gunControl.Comp.TogglePort)
|
||||
_gun.SetEnabled(gunControl, autoShoot, !autoShoot.Enabled);
|
||||
_gun.SetEnabled((gunControl, autoShoot), !autoShoot.Enabled);
|
||||
|
||||
if (args.Port == gunControl.Comp.OnPort)
|
||||
_gun.SetEnabled(gunControl, autoShoot, true);
|
||||
_gun.SetEnabled((gunControl, autoShoot), true);
|
||||
|
||||
if (args.Port == gunControl.Comp.OffPort)
|
||||
_gun.SetEnabled(gunControl, autoShoot, false);
|
||||
_gun.SetEnabled((gunControl, autoShoot), false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,9 +268,6 @@ public sealed partial class DragonSystem : EntitySystem
|
||||
if (!Resolve(uid, ref comp))
|
||||
return;
|
||||
|
||||
// do reset the rift count since crew destroyed the rift, not deleted by the dragon dying.
|
||||
DeleteRifts(uid, true, comp);
|
||||
|
||||
// We can't predict the rift being destroyed anyway so no point adding weakened to shared.
|
||||
comp.WeakenedAccumulator = comp.WeakenedDuration;
|
||||
_movement.RefreshMovementSpeedModifiers(uid);
|
||||
|
||||
@@ -3,6 +3,7 @@ using Content.Server.Botany.Components;
|
||||
using Content.Shared.EntityEffects;
|
||||
using Content.Shared.EntityEffects.Effects.Botany;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Random.Helpers;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
@@ -19,12 +20,11 @@ public sealed partial class PlantMutateChemicalsEntityEffectSystem : EntityEffec
|
||||
return;
|
||||
|
||||
var chemicals = entity.Comp.Seed.Chemicals;
|
||||
var randomChems = _proto.Index(args.Effect.RandomPickBotanyReagent).Fills;
|
||||
var randomChems = _proto.Index(args.Effect.RandomPickBotanyReagent);
|
||||
|
||||
// Add a random amount of a random chemical to this set of chemicals
|
||||
var pick = _random.Pick(randomChems);
|
||||
var chemicalId = _random.Pick(pick.Reagents);
|
||||
var amount = _random.NextFloat(0.1f, (float)pick.Quantity);
|
||||
var (chemicalId, quantity) = randomChems.Pick(_random);
|
||||
var amount = FixedPoint2.Max(_random.NextFloat(0f, 1f) * quantity, FixedPoint2.Epsilon);
|
||||
var seedChemQuantity = new SeedChemQuantity();
|
||||
if (chemicals.ContainsKey(chemicalId))
|
||||
{
|
||||
@@ -34,7 +34,7 @@ public sealed partial class PlantMutateChemicalsEntityEffectSystem : EntityEffec
|
||||
else
|
||||
{
|
||||
//Set the minimum to a fifth of the quantity to give some level of bad luck protection
|
||||
seedChemQuantity.Min = FixedPoint2.Clamp(pick.Quantity / 5f, FixedPoint2.Epsilon, 1f);
|
||||
seedChemQuantity.Min = FixedPoint2.Clamp(quantity / 5f, FixedPoint2.Epsilon, 1f);
|
||||
seedChemQuantity.Max = seedChemQuantity.Min + amount;
|
||||
seedChemQuantity.Inherent = false;
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ public sealed class SpraySystem : SharedSpraySystem
|
||||
|
||||
public override void Spray(Entity<SprayComponent> entity, MapCoordinates mapcoord, EntityUid? user = null)
|
||||
{
|
||||
if (!_solutionContainer.TryGetSolution(entity.Owner, SprayComponent.SolutionName, out var soln, out var solution))
|
||||
if (!_solutionContainer.TryGetSolution(entity.Owner, entity.Comp.Solution, out var soln, out var solution))
|
||||
return;
|
||||
|
||||
var ev = new SprayAttemptEvent(user);
|
||||
|
||||
@@ -13,6 +13,7 @@ using Content.Shared.DoAfter;
|
||||
using Content.Shared.Forensics;
|
||||
using Content.Shared.Forensics.Components;
|
||||
using Content.Shared.Forensics.Systems;
|
||||
using Content.Shared.Gibbing;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Interaction.Events;
|
||||
using Content.Shared.Inventory;
|
||||
@@ -39,7 +40,7 @@ namespace Content.Server.Forensics
|
||||
// The solution entities are spawned on MapInit as well, so we have to wait for that to be able to set the DNA in the bloodstream correctly without ResolveSolution failing
|
||||
SubscribeLocalEvent<DnaComponent, MapInitEvent>(OnDNAInit, after: new[] { typeof(BloodstreamSystem) });
|
||||
|
||||
SubscribeLocalEvent<ForensicsComponent, BeingGibbedEvent>(OnBeingGibbed);
|
||||
SubscribeLocalEvent<ForensicsComponent, GibbedBeforeDeletionEvent>(OnBeingGibbed);
|
||||
SubscribeLocalEvent<ForensicsComponent, MeleeHitEvent>(OnMeleeHit);
|
||||
SubscribeLocalEvent<ForensicsComponent, GotRehydratedEvent>(OnRehydrated);
|
||||
SubscribeLocalEvent<CleansForensicsComponent, AfterInteractEvent>(OnAfterInteract, after: new[] { typeof(AbsorbentSystem) });
|
||||
@@ -85,14 +86,14 @@ namespace Content.Server.Forensics
|
||||
}
|
||||
}
|
||||
|
||||
private void OnBeingGibbed(EntityUid uid, ForensicsComponent component, BeingGibbedEvent args)
|
||||
private void OnBeingGibbed(Entity<ForensicsComponent> ent, ref GibbedBeforeDeletionEvent args)
|
||||
{
|
||||
string dna = Loc.GetString("forensics-dna-unknown");
|
||||
|
||||
if (TryComp(uid, out DnaComponent? dnaComp) && dnaComp.DNA != null)
|
||||
if (TryComp(ent, out DnaComponent? dnaComp) && dnaComp.DNA != null)
|
||||
dna = dnaComp.DNA;
|
||||
|
||||
foreach (EntityUid part in args.GibbedParts)
|
||||
foreach (var part in args.Giblets)
|
||||
{
|
||||
var partComp = EnsureComp<ForensicsComponent>(part);
|
||||
partComp.DNAs.Add(dna);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Content.Corvax.Interfaces.Server;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.GameTicking;
|
||||
using Content.Shared.GameWindow;
|
||||
using Content.Shared.Players;
|
||||
@@ -95,6 +96,7 @@ namespace Content.Server.GameTicking
|
||||
else
|
||||
SpawnWaitDb();
|
||||
|
||||
_adminLogger.Add(LogType.Connection, LogImpact.Low, $"User {args.Session:Player} attached to {(args.Session.AttachedEntity != null ? ToPrettyString(args.Session.AttachedEntity) : "nothing"):entity} connected to the game.");
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -121,6 +123,8 @@ namespace Content.Server.GameTicking
|
||||
}
|
||||
}
|
||||
|
||||
_adminLogger.Add(LogType.Connection, LogImpact.Low, $"User {args.Session:Player} attached to {(args.Session.AttachedEntity != null ? ToPrettyString(args.Session.AttachedEntity) : "nothing"):entity} connected to the game.");
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -132,8 +136,13 @@ namespace Content.Server.GameTicking
|
||||
_pvsOverride.RemoveSessionOverride(mindId.Value, session);
|
||||
}
|
||||
|
||||
if (_playerGameStatuses.ContainsKey(args.Session.UserId)) // Corvax-Queue: Delete data only if player was in game
|
||||
// Corvax-Queue-start: Delete data only if player was in game
|
||||
if (_playerGameStatuses.ContainsKey(args.Session.UserId))
|
||||
{
|
||||
_userDb.ClientDisconnected(session);
|
||||
_adminLogger.Add(LogType.Connection, LogImpact.Low, $"User {args.Session:Player} attached to {(args.Session.AttachedEntity != null ? ToPrettyString(args.Session.AttachedEntity) : "nothing"):entity} disconnected from the game.");
|
||||
}
|
||||
// Corvax-Queue-end
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,6 +87,8 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
|
||||
return;
|
||||
|
||||
component.TargetStation = RobustRandom.Pick(eligible);
|
||||
var ev = new NukeopsTargetStationSelectedEvent(uid, component.TargetStation);
|
||||
RaiseLocalEvent(ref ev);
|
||||
}
|
||||
|
||||
#region Event Handlers
|
||||
@@ -551,3 +553,20 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raised when a station has been assigned as a target for the NukeOps rule.
|
||||
/// </summary>
|
||||
[ByRefEvent]
|
||||
public readonly struct NukeopsTargetStationSelectedEvent(EntityUid ruleEntity, EntityUid? targetStation)
|
||||
{
|
||||
/// <summary>
|
||||
/// The entity containing the NukeOps gamerule.
|
||||
/// </summary>
|
||||
public readonly EntityUid RuleEntity = ruleEntity;
|
||||
|
||||
/// <summary>
|
||||
/// The target station, if it exists.
|
||||
/// </summary>
|
||||
public readonly EntityUid? TargetStation = targetStation;
|
||||
}
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
using Content.Shared.Gibbing.Components;
|
||||
using Content.Shared.Mind;
|
||||
using Content.Shared.Objectives.Systems;
|
||||
using Content.Server.Body.Systems;
|
||||
using Content.Shared.Gibbing;
|
||||
|
||||
namespace Content.Server.Gibbing.Systems;
|
||||
public sealed class GibOnRoundEndSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly BodySystem _body = default!;
|
||||
[Dependency] private readonly GibbingSystem _gibbing = default!;
|
||||
[Dependency] private readonly SharedMindSystem _mind = default!;
|
||||
[Dependency] private readonly SharedObjectivesSystem _objectives = default!;
|
||||
|
||||
@@ -49,7 +49,7 @@ public sealed class GibOnRoundEndSystem : EntitySystem
|
||||
if (gibComp.SpawnProto != null)
|
||||
SpawnAtPosition(gibComp.SpawnProto, Transform(uid).Coordinates);
|
||||
|
||||
_body.GibBody(uid, splatModifier: 5f);
|
||||
_gibbing.Gib(uid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using Content.Server.Body.Systems;
|
||||
using Content.Server.Popups;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Damage.Systems;
|
||||
using Content.Shared.DoAfter;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.Gibbing;
|
||||
using Content.Shared.Guardian;
|
||||
using Content.Shared.Hands.Components;
|
||||
using Content.Shared.Hands.EntitySystems;
|
||||
@@ -31,7 +31,7 @@ namespace Content.Server.Guardian
|
||||
[Dependency] private readonly SharedActionsSystem _actionSystem = default!;
|
||||
[Dependency] private readonly SharedHandsSystem _handsSystem = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
[Dependency] private readonly BodySystem _bodySystem = default!;
|
||||
[Dependency] private readonly GibbingSystem _gibbing = default!;
|
||||
[Dependency] private readonly SharedContainerSystem _container = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _transform = default!;
|
||||
|
||||
@@ -130,7 +130,7 @@ namespace Content.Server.Guardian
|
||||
|
||||
// Ensure held items are dropped before deleting guardian.
|
||||
if (HasComp<HandsComponent>(guardian))
|
||||
_bodySystem.GibBody(component.HostedGuardian.Value);
|
||||
_gibbing.Gib(component.HostedGuardian.Value);
|
||||
|
||||
QueueDel(guardian);
|
||||
QueueDel(component.ActionEntity);
|
||||
|
||||
@@ -5,6 +5,7 @@ using Content.Server.Popups;
|
||||
using Content.Shared.Body.Components;
|
||||
using Content.Shared.Damage.Systems;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.Gibbing;
|
||||
using Content.Shared.Popups;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
using Robust.Shared.Map;
|
||||
@@ -20,7 +21,7 @@ public sealed class ImmovableRodSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
|
||||
[Dependency] private readonly BodySystem _bodySystem = default!;
|
||||
[Dependency] private readonly GibbingSystem _gibbing = default!;
|
||||
[Dependency] private readonly PopupSystem _popup = default!;
|
||||
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
@@ -125,7 +126,7 @@ public sealed class ImmovableRodSystem : EntitySystem
|
||||
return;
|
||||
}
|
||||
|
||||
_bodySystem.GibBody(ent, body: body);
|
||||
_gibbing.Gib(ent);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user