Merge pull request #3488 from DIMMoon1/upstream01.2

Upstream01.2
This commit is contained in:
Dmitry
2026-01-20 12:46:48 +07:00
committed by GitHub
741 changed files with 14711 additions and 10736 deletions

View 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();
}
}

View 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);
}
}

View File

@@ -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()
{

View File

@@ -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);
}
}
}

View 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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
};
}
}

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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;
}
}

View 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>

View 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;
}
}

View File

@@ -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>

View File

@@ -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);
}
}

View File

@@ -5,4 +5,3 @@ namespace Content.Client.Kitchen.EntitySystems;
[UsedImplicitly]
public sealed class ReagentGrinderSystem : SharedReagentGrinderSystem;

View 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;
}
}

View File

@@ -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);
}
}
}

View File

@@ -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

View 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>

View 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;
}
}

View File

@@ -0,0 +1,5 @@
using Content.Shared.Medical;
namespace Content.Client.Medical;
public sealed class DefibrillatorSystem : SharedDefibrillatorSystem;

View File

@@ -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);
}

View File

@@ -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)

View File

@@ -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);
}
}
}

View File

@@ -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);

View File

@@ -3,5 +3,6 @@ namespace Content.Client.Power;
/// Remains in use by portable scrubbers and lathes.
public enum PowerDeviceVisualLayers : byte
{
Powered
Powered,
Charging
}

View File

@@ -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));
}

View File

@@ -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();
};

View File

@@ -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;
}

View File

@@ -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));

View File

@@ -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>

View File

@@ -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;
}
}

View File

@@ -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;
}

View 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();
}
}

View File

@@ -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();
}
}

View File

@@ -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),
];
}

View File

@@ -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`

View File

@@ -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)

View File

@@ -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>

View File

@@ -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));
}
}

View File

@@ -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
);
}
}
}

View File

@@ -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)

View File

@@ -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;

View File

@@ -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),
},
},
},
};
}

View File

@@ -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

View File

@@ -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";
}

View File

@@ -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);

View File

@@ -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)

View File

@@ -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);
}
}

View File

@@ -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>

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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)

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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();
}

View File

@@ -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);
}
}

View File

@@ -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) { }
}

View 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
}

View File

@@ -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);

View File

@@ -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.");
}
}
}

View File

@@ -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();
}
}

View 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);
}
}

View File

@@ -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)
{

View File

@@ -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();

View File

@@ -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());
}

View 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();
}
}

View File

@@ -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
}

View File

@@ -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)

View 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.");
}
}

View File

@@ -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));
}
}

View File

@@ -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.

View File

@@ -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,

View File

@@ -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)

View File

@@ -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))

View File

@@ -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>

View File

@@ -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);

View File

@@ -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>

View File

@@ -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);

View File

@@ -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))

View File

@@ -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>

View File

@@ -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));
}

View File

@@ -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);

View File

@@ -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;
}
}

View 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);
}
}
}

View File

@@ -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"),

View File

@@ -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);
}

View File

@@ -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!;

View File

@@ -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);
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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);

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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);
}
}
}

View File

@@ -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);

View File

@@ -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