diff --git a/Content.Benchmarks/HeatCapacityBenchmark.cs b/Content.Benchmarks/HeatCapacityBenchmark.cs new file mode 100644 index 0000000000..cef5bc10c7 --- /dev/null +++ b/Content.Benchmarks/HeatCapacityBenchmark.cs @@ -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(); + _sEntMan = _pair.Server.ResolveDependency(); + _cAtmos = _cEntMan.System(); + _sAtmos = _sEntMan.System(); + + 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(); + } +} diff --git a/Content.Client/Atmos/EntitySystems/AtmosphereSystem.Gases.cs b/Content.Client/Atmos/EntitySystems/AtmosphereSystem.Gases.cs new file mode 100644 index 0000000000..17b994e64f --- /dev/null +++ b/Content.Client/Atmos/EntitySystems/AtmosphereSystem.Gases.cs @@ -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); + } +} diff --git a/Content.Client/Atmos/EntitySystems/AtmosphereSystem.cs b/Content.Client/Atmos/EntitySystems/AtmosphereSystem.cs index 44759372f4..30567abbf7 100644 --- a/Content.Client/Atmos/EntitySystems/AtmosphereSystem.cs +++ b/Content.Client/Atmos/EntitySystems/AtmosphereSystem.cs @@ -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() { diff --git a/Content.Client/BarSign/BarSignSystem.cs b/Content.Client/BarSign/BarSignSystem.cs deleted file mode 100644 index 1ea99864a1..0000000000 --- a/Content.Client/BarSign/BarSignSystem.cs +++ /dev/null @@ -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 -{ - [Dependency] private readonly IPrototypeManager _prototypeManager = default!; - [Dependency] private readonly UserInterfaceSystem _ui = default!; - - public override void Initialize() - { - base.Initialize(); - SubscribeLocalEvent(OnAfterAutoHandleState); - } - - private void OnAfterAutoHandleState(EntityUid uid, BarSignComponent component, ref AfterAutoHandleStateEvent args) - { - if (_ui.TryGetOpenUi(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(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); - } - } -} diff --git a/Content.Client/BarSign/BarSignVisualizerSystem.cs b/Content.Client/BarSign/BarSignVisualizerSystem.cs new file mode 100644 index 0000000000..3e641fed70 --- /dev/null +++ b/Content.Client/BarSign/BarSignVisualizerSystem.cs @@ -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 +{ + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + + protected override void OnAppearanceChange(EntityUid uid, BarSignComponent component, ref AppearanceChangeEvent args) + { + AppearanceSystem.TryGetData(uid, PowerDeviceVisuals.Powered, out var powered, args.Component); + AppearanceSystem.TryGetData(uid, BarSignVisuals.BarSignPrototype, out var currentSign, args.Component); + + if (powered + && currentSign != null + && _prototypeManager.Resolve(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); + } + } +} diff --git a/Content.Client/BarSign/Ui/BarSignBoundUserInterface.cs b/Content.Client/BarSign/Ui/BarSignBoundUserInterface.cs index fe07f0f1d1..8265877edf 100644 --- a/Content.Client/BarSign/Ui/BarSignBoundUserInterface.cs +++ b/Content.Client/BarSign/Ui/BarSignBoundUserInterface.cs @@ -19,32 +19,27 @@ public sealed class BarSignBoundUserInterface(EntityUid owner, Enum uiKey) : Bou var sign = EntMan.GetComponentOrNull(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? 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(Owner, out var signComp)) return; - _menu?.Dispose(); + + if (_prototype.Resolve(signComp.Current, out var signPrototype)) + _menu?.UpdateState(signPrototype); } } diff --git a/Content.Client/Chemistry/UI/TransferAmountBoundUserInterface.cs b/Content.Client/Chemistry/UI/TransferAmountBoundUserInterface.cs index 1bc1c0dba9..884a5db9da 100644 --- a/Content.Client/Chemistry/UI/TransferAmountBoundUserInterface.cs +++ b/Content.Client/Chemistry/UI/TransferAmountBoundUserInterface.cs @@ -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(); - public TransferAmountBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) + if (EntMan.TryGetComponent(Owner, out var comp)) + _window.SetBounds(comp.MinimumTransferAmount.Int(), comp.MaximumTransferAmount.Int()); + + _window.ApplyButton.OnPressed += _ => { - _owner = owner; - _entManager = IoCManager.Resolve(); - } - - protected override void Open() - { - base.Open(); - _window = this.CreateWindow(); - - if (_entManager.TryGetComponent(_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(); + } + }; } } diff --git a/Content.Client/Chemistry/UI/TransferAmountWindow.xaml.cs b/Content.Client/Chemistry/UI/TransferAmountWindow.xaml.cs index 6bae044441..2d01098213 100644 --- a/Content.Client/Chemistry/UI/TransferAmountWindow.xaml.cs +++ b/Content.Client/Chemistry/UI/TransferAmountWindow.xaml.cs @@ -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; } } diff --git a/Content.Client/CombatMode/CombatModeIndicatorsOverlay.cs b/Content.Client/CombatMode/CombatModeIndicatorsOverlay.cs index b2bdf2893d..d852e20c45 100644 --- a/Content.Client/CombatMode/CombatModeIndicatorsOverlay.cs +++ b/Content.Client/CombatMode/CombatModeIndicatorsOverlay.cs @@ -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; diff --git a/Content.Client/Construction/UI/FlatpackCreatorMenu.xaml.cs b/Content.Client/Construction/UI/FlatpackCreatorMenu.xaml.cs index 8ee8df48fd..7db15596bd 100644 --- a/Content.Client/Construction/UI/FlatpackCreatorMenu.xaml.cs +++ b/Content.Client/Construction/UI/FlatpackCreatorMenu.xaml.cs @@ -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 cost; - if (_entityManager.TryGetComponent(_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? 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(_currentBoard, out var newMachineBoardComp)) - { - prototype = newMachineBoardComp.Prototype; - cost = _flatpack.GetFlatpackCreationCost((_owner, flatpacker), (_currentBoard.Value, newMachineBoardComp)); - } - else if (_entityManager.TryGetComponent(_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(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(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; } } diff --git a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerControl.xaml b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerControl.xaml new file mode 100644 index 0000000000..06c6528f59 --- /dev/null +++ b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerControl.xaml @@ -0,0 +1,53 @@ + + diff --git a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerControl.xaml.cs b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerControl.xaml.cs new file mode 100644 index 0000000000..949b4770c4 --- /dev/null +++ b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerControl.xaml.cs @@ -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(); + _spriteSystem = _entityManager.System(); + _prototypes = dependencies.Resolve(); + _cache = dependencies.Resolve(); + } + + public void Populate(HealthAnalyzerUiState state) + { + var target = _entityManager.GetEntity(state.TargetEntity); + + if (target == null + || !_entityManager.TryGetComponent(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(target.Value) + ? Identity.Name(target.Value, _entityManager) + : Loc.GetString("health-analyzer-window-entity-unknown-text")); + NameLabel.SetMessage(name); + + SpeciesLabel.Text = + _entityManager.TryGetComponent(target.Value, + out var humanoidAppearanceComponent) + ? Loc.GetString(_prototypes.Index(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(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 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 groups, + IReadOnlyDictionary 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(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(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(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(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; + } +} diff --git a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml index aae8785b1f..932592ed37 100644 --- a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml +++ b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml @@ -1,64 +1,15 @@ - - + diff --git a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs index 533a8b9f2c..6c0ed360b0 100644 --- a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs +++ b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs @@ -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(); - _spriteSystem = _entityManager.System(); - _prototypes = dependencies.Resolve(); - _cache = dependencies.Resolve(); - } - - public void Populate(HealthAnalyzerScannedUserMessage msg) - { - var target = _entityManager.GetEntity(msg.TargetEntity); - - if (target == null - || !_entityManager.TryGetComponent(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(target.Value) - ? Identity.Name(target.Value, _entityManager) - : Loc.GetString("health-analyzer-window-entity-unknown-text")); - NameLabel.SetMessage(name); - - SpeciesLabel.Text = - _entityManager.TryGetComponent(target.Value, - out var humanoidAppearanceComponent) - ? Loc.GetString(_prototypes.Index(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(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 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 groups, - IReadOnlyDictionary 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(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(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(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(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); } } diff --git a/Content.Client/Kitchen/EntitySystems/ReagentGrinderSystem.cs b/Content.Client/Kitchen/EntitySystems/ReagentGrinderSystem.cs index 5eebd4a0fb..0aaa8ba8d8 100644 --- a/Content.Client/Kitchen/EntitySystems/ReagentGrinderSystem.cs +++ b/Content.Client/Kitchen/EntitySystems/ReagentGrinderSystem.cs @@ -5,4 +5,3 @@ namespace Content.Client.Kitchen.EntitySystems; [UsedImplicitly] public sealed class ReagentGrinderSystem : SharedReagentGrinderSystem; - diff --git a/Content.Client/Medical/Cryogenics/BeakerBarChart.cs b/Content.Client/Medical/Cryogenics/BeakerBarChart.cs new file mode 100644 index 0000000000..25301b5268 --- /dev/null +++ b/Content.Client/Medical/Cryogenics/BeakerBarChart.cs @@ -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 _entries = new(); + + + public BeakerBarChart() + { + MouseFilter = MouseFilterMode.Pass; + TooltipSupplier = SupplyTooltip; + } + + public void Clear() + { + foreach (var entry in _entries) + { + entry.TargetAmount = 0; + } + + _nextUpdateableEntry = 0; + } + + /// + /// Either adds a new entry to the chart if the UID doesn't appear yet, or updates the amount of an existing entry. + /// + 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; + } +} diff --git a/Content.Client/Medical/Cryogenics/CryoPodBoundUserInterface.cs b/Content.Client/Medical/Cryogenics/CryoPodBoundUserInterface.cs new file mode 100644 index 0000000000..5e64cea720 --- /dev/null +++ b/Content.Client/Medical/Cryogenics/CryoPodBoundUserInterface.cs @@ -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(); + _window.Title = EntMan.GetComponent(Owner).EntityName; + _window.OnEjectPatientPressed += EjectPatientPressed; + _window.OnEjectBeakerPressed += EjectBeakerPressed; + _window.OnInjectPressed += InjectPressed; + } + + private void EjectPatientPressed() + { + var isLocked = + EntMan.TryGetComponent(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); + } + } +} diff --git a/Content.Client/Medical/Cryogenics/CryoPodSystem.cs b/Content.Client/Medical/Cryogenics/CryoPodSystem.cs index c1cbfc573e..63c95a63d8 100644 --- a/Content.Client/Medical/Cryogenics/CryoPodSystem.cs +++ b/Content.Client/Medical/Cryogenics/CryoPodSystem.cs @@ -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(uid, CryoPodVisuals.ContainsEntity, out var isOpen, args.Component) - || !_appearance.TryGetData(uid, CryoPodVisuals.IsOn, out var isOn, args.Component)) + if (!Appearance.TryGetData(uid, CryoPodVisuals.ContainsEntity, out var isOpen, args.Component) + || !Appearance.TryGetData(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 cryoPod) + { + // Atmos and health scanner aren't predicted currently... + } } public enum CryoPodVisualLayers : byte diff --git a/Content.Client/Medical/Cryogenics/CryoPodWindow.xaml b/Content.Client/Medical/Cryogenics/CryoPodWindow.xaml new file mode 100644 index 0000000000..9bea37d582 --- /dev/null +++ b/Content.Client/Medical/Cryogenics/CryoPodWindow.xaml @@ -0,0 +1,232 @@ + + +