]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Make Health Analysers UI continuously update (#22449)
authorRainfey <rainfey0+github@gmail.com>
Tue, 6 Feb 2024 13:20:09 +0000 (13:20 +0000)
committerGitHub <noreply@github.com>
Tue, 6 Feb 2024 13:20:09 +0000 (09:20 -0400)
Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml
Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs
Content.Server/Medical/Components/HealthAnalyzerComponent.cs
Content.Server/Medical/CryoPodSystem.cs
Content.Server/Medical/HealthAnalyzerSystem.cs
Content.Shared/MedicalScanner/HealthAnalyzerScannedUserMessage.cs
Resources/Locale/en-US/medical/components/health-analyzer-component.ftl
Resources/Prototypes/Entities/Objects/Specific/Medical/healthanalyzer.yml

index b78c3c6a567a8e82565816beb09acdc09aa17f32..28872ef1d5d5ac15742ef2e136eec51299d8e561 100644 (file)
@@ -1,4 +1,6 @@
-<DefaultWindow xmlns="https://spacestation14.io"
+<controls:FancyWindow
+    xmlns="https://spacestation14.io"
+    xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
     SetSize="250 100">
     <ScrollContainer
         VerticalExpand="True">
                 Name="PatientDataContainer"
                 Orientation="Vertical"
                 Margin="0 0 5 10">
+                <BoxContainer Name="ScanModePanel" HorizontalExpand="True" Visible="False" Margin="0 5 0 0">
+                    <Label
+                    Name="ScanMode"
+                    Align="Left"
+                    Text="{Loc health-analyzer-window-scan-mode-text}"/>
+                    <Label
+                    Name="ScanModeText"
+                    Align="Right"
+                    HorizontalExpand="True"/>
+                </BoxContainer>
                 <Label
                     Name="PatientName"/>
                 <Label
@@ -30,4 +42,4 @@
             </BoxContainer>
         </BoxContainer>
     </ScrollContainer>
-</DefaultWindow>
\ No newline at end of file
+</controls:FancyWindow>
index 588eb88502ed7473f9bdf37a7a94617100a8168d..2f19cd0a0500bdd4dd1b836b0354c6db6edc26fc 100644 (file)
@@ -1,5 +1,6 @@
 using System.Linq;
 using System.Numerics;
+using Content.Client.UserInterface.Controls;
 using Content.Shared.Damage;
 using Content.Shared.Damage.Prototypes;
 using Content.Shared.FixedPoint;
@@ -7,7 +8,6 @@ using Content.Shared.IdentityManagement;
 using Content.Shared.MedicalScanner;
 using Content.Shared.Nutrition.Components;
 using Robust.Client.AutoGenerated;
-using Robust.Client.UserInterface.CustomControls;
 using Robust.Client.UserInterface.XAML;
 using Robust.Client.GameObjects;
 using Robust.Client.Graphics;
@@ -20,7 +20,7 @@ using Robust.Shared.Utility;
 namespace Content.Client.HealthAnalyzer.UI
 {
     [GenerateTypedNameReferences]
-    public sealed partial class HealthAnalyzerWindow : DefaultWindow
+    public sealed partial class HealthAnalyzerWindow : FancyWindow
     {
         private readonly IEntityManager _entityManager;
         private readonly SpriteSystem _spriteSystem;
@@ -62,6 +62,17 @@ namespace Content.Client.HealthAnalyzer.UI
                 entityName = Identity.Name(target.Value, _entityManager);
             }
 
+            if (msg.ScanMode.HasValue)
+            {
+                ScanModePanel.Visible = true;
+                ScanModeText.Text = Loc.GetString(msg.ScanMode.Value ? "health-analyzer-window-scan-mode-active" : "health-analyzer-window-scan-mode-inactive");
+                ScanModeText.FontColorOverride = msg.ScanMode.Value ? Color.Green : Color.Red;
+            }
+            else
+            {
+                ScanModePanel.Visible = false;
+            }
+
             PatientName.Text = Loc.GetString(
                 "health-analyzer-window-entity-health-text",
                 ("entityName", entityName)
index 39b1df573f20641d9c1253fce52a1ff718f686e9..0002f275c5b05685fb61e8648422e253819c0b5d 100644 (file)
@@ -1,32 +1,54 @@
-using Content.Server.UserInterface;
-using Content.Shared.MedicalScanner;
-using Robust.Server.GameObjects;
 using Robust.Shared.Audio;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
 
-namespace Content.Server.Medical.Components
+namespace Content.Server.Medical.Components;
+
+/// <summary>
+/// After scanning, retrieves the target Uid to use with its related UI.
+/// </summary>
+[RegisterComponent]
+[Access(typeof(HealthAnalyzerSystem))]
+public sealed partial class HealthAnalyzerComponent : Component
 {
     /// <summary>
-    ///    After scanning, retrieves the target Uid to use with its related UI.
+    /// When should the next update be sent for the patient
+    /// </summary>
+    [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
+    public TimeSpan NextUpdate = TimeSpan.Zero;
+
+    /// <summary>
+    /// The delay between patient health updates
+    /// </summary>
+    [DataField]
+    public TimeSpan UpdateInterval = TimeSpan.FromSeconds(1);
+
+    /// <summary>
+    /// How long it takes to scan someone.
+    /// </summary>
+    [DataField]
+    public TimeSpan ScanDelay = TimeSpan.FromSeconds(0.8);
+
+    /// <summary>
+    /// Which entity has been scanned, for continuous updates
+    /// </summary>
+    [DataField]
+    public EntityUid? ScannedEntity;
+
+    /// <summary>
+    /// The maximum range in tiles at which the analyzer can receive continuous updates
+    /// </summary>
+    [DataField]
+    public float MaxScanRange = 2.5f;
+
+    /// <summary>
+    /// Sound played on scanning begin
+    /// </summary>
+    [DataField]
+    public SoundSpecifier? ScanningBeginSound;
+
+    /// <summary>
+    /// Sound played on scanning end
     /// </summary>
-    [RegisterComponent]
-    public sealed partial class HealthAnalyzerComponent : Component
-    {
-        /// <summary>
-        /// How long it takes to scan someone.
-        /// </summary>
-        [DataField("scanDelay")]
-        public float ScanDelay = 0.8f;
-
-        /// <summary>
-        ///     Sound played on scanning begin
-        /// </summary>
-        [DataField("scanningBeginSound")]
-        public SoundSpecifier? ScanningBeginSound;
-
-        /// <summary>
-        ///     Sound played on scanning end
-        /// </summary>
-        [DataField("scanningEndSound")]
-        public SoundSpecifier? ScanningEndSound;
-    }
+    [DataField]
+    public SoundSpecifier? ScanningEndSound;
 }
index 2f08dfddd171c0220a65aa88bba4603b7dd1f088..25a47933a84c937f3c2a5b1831ea4d3599f8df98 100644 (file)
@@ -195,7 +195,8 @@ public sealed partial class CryoPodSystem : SharedCryoPodSystem
             (bloodstream != null && _solutionContainerSystem.ResolveSolution(entity.Comp.BodyContainer.ContainedEntity.Value,
                 bloodstream.BloodSolutionName, ref bloodstream.BloodSolution, out var bloodSolution))
                 ? bloodSolution.FillFraction
-                : 0
+                : 0,
+            null
         ));
     }
 
index d9559a96264f18ff6e1061693f1ae17574be6955..5c7d265e615ed3605e6243dc5a512b70637ca294 100644 (file)
@@ -6,94 +6,195 @@ using Content.Server.Temperature.Components;
 using Content.Shared.Damage;
 using Content.Shared.DoAfter;
 using Content.Shared.Interaction;
+using Content.Shared.Interaction.Events;
 using Content.Shared.MedicalScanner;
 using Content.Shared.Mobs.Components;
+using Content.Shared.PowerCell;
 using Robust.Server.GameObjects;
 using Robust.Shared.Audio.Systems;
+using Robust.Shared.Containers;
 using Robust.Shared.Player;
+using Robust.Shared.Timing;
 
-namespace Content.Server.Medical
+namespace Content.Server.Medical;
+
+public sealed class HealthAnalyzerSystem : EntitySystem
 {
-    public sealed class HealthAnalyzerSystem : EntitySystem
+    [Dependency] private readonly IGameTiming _timing = default!;
+    [Dependency] private readonly PowerCellSystem _cell = default!;
+    [Dependency] private readonly SharedAudioSystem _audio = default!;
+    [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
+    [Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!;
+    [Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
+    [Dependency] private readonly TransformSystem _transformSystem = default!;
+
+    public override void Initialize()
     {
-        [Dependency] private readonly PowerCellSystem _cell = default!;
-        [Dependency] private readonly SharedAudioSystem _audio = default!;
-        [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
-        [Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!;
-        [Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
+        SubscribeLocalEvent<HealthAnalyzerComponent, EntityUnpausedEvent>(OnEntityUnpaused);
+        SubscribeLocalEvent<HealthAnalyzerComponent, AfterInteractEvent>(OnAfterInteract);
+        SubscribeLocalEvent<HealthAnalyzerComponent, HealthAnalyzerDoAfterEvent>(OnDoAfter);
+        SubscribeLocalEvent<HealthAnalyzerComponent, EntGotInsertedIntoContainerMessage>(OnInsertedIntoContainer);
+        SubscribeLocalEvent<HealthAnalyzerComponent, PowerCellSlotEmptyEvent>(OnPowerCellSlotEmpty);
+        SubscribeLocalEvent<HealthAnalyzerComponent, DroppedEvent>(OnDropped);
+    }
 
-        public override void Initialize()
+    public override void Update(float frameTime)
+    {
+        var analyzerQuery = EntityQueryEnumerator<HealthAnalyzerComponent, TransformComponent>();
+        while (analyzerQuery.MoveNext(out var uid, out var component, out var transform))
         {
-            base.Initialize();
-            SubscribeLocalEvent<HealthAnalyzerComponent, AfterInteractEvent>(OnAfterInteract);
-            SubscribeLocalEvent<HealthAnalyzerComponent, HealthAnalyzerDoAfterEvent>(OnDoAfter);
-        }
+            //Update rate limited to 1 second
+            if (component.NextUpdate > _timing.CurTime)
+                continue;
 
-        private void OnAfterInteract(Entity<HealthAnalyzerComponent> entity, ref AfterInteractEvent args)
-        {
-            if (args.Target == null || !args.CanReach || !HasComp<MobStateComponent>(args.Target) || !_cell.HasActivatableCharge(entity.Owner, user: args.User))
-                return;
+            if (component.ScannedEntity is not {} patient)
+                continue;
 
-            _audio.PlayPvs(entity.Comp.ScanningBeginSound, entity);
+            component.NextUpdate = _timing.CurTime + component.UpdateInterval;
 
-            _doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, args.User, TimeSpan.FromSeconds(entity.Comp.ScanDelay), new HealthAnalyzerDoAfterEvent(), entity.Owner, target: args.Target, used: entity.Owner)
+            //Get distance between health analyzer and the scanned entity
+            var patientCoordinates = Transform(patient).Coordinates;
+            if (!patientCoordinates.InRange(EntityManager, _transformSystem, transform.Coordinates, component.MaxScanRange))
             {
-                BreakOnTargetMove = true,
-                BreakOnUserMove = true,
-                NeedHand = true
-            });
+                //Range too far, disable updates
+                StopAnalyzingEntity((uid, component), patient);
+                continue;
+            }
+
+            UpdateScannedUser(uid, patient, true);
         }
+    }
 
-        private void OnDoAfter(Entity<HealthAnalyzerComponent> entity, ref HealthAnalyzerDoAfterEvent args)
-        {
-            if (args.Handled || args.Cancelled || args.Target == null || !_cell.TryUseActivatableCharge(entity.Owner, user: args.User))
-                return;
+    private void OnEntityUnpaused(Entity<HealthAnalyzerComponent> ent, ref EntityUnpausedEvent args)
+    {
+        ent.Comp.NextUpdate += args.PausedTime;
+    }
 
-            _audio.PlayPvs(entity.Comp.ScanningEndSound, args.User);
+    /// <summary>
+    /// Trigger the doafter for scanning
+    /// </summary>
+    private void OnAfterInteract(Entity<HealthAnalyzerComponent> uid, ref AfterInteractEvent args)
+    {
+        if (args.Target == null || !args.CanReach || !HasComp<MobStateComponent>(args.Target) || !_cell.HasDrawCharge(uid, user: args.User))
+            return;
 
-            UpdateScannedUser(entity, args.User, args.Target.Value, entity.Comp);
-            args.Handled = true;
-        }
+        _audio.PlayPvs(uid.Comp.ScanningBeginSound, uid);
 
-        private void OpenUserInterface(EntityUid user, EntityUid analyzer)
+        _doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, args.User, uid.Comp.ScanDelay, new HealthAnalyzerDoAfterEvent(), uid, target: args.Target, used: uid)
         {
-            if (!TryComp<ActorComponent>(user, out var actor) || !_uiSystem.TryGetUi(analyzer, HealthAnalyzerUiKey.Key, out var ui))
-                return;
+            BreakOnTargetMove = true,
+            BreakOnUserMove = true,
+            NeedHand = true
+        });
+    }
 
-            _uiSystem.OpenUi(ui ,actor.PlayerSession);
-        }
+    private void OnDoAfter(Entity<HealthAnalyzerComponent> uid, ref HealthAnalyzerDoAfterEvent args)
+    {
+        if (args.Handled || args.Cancelled || args.Target == null || !_cell.HasDrawCharge(uid, user: args.User))
+            return;
 
-        public void UpdateScannedUser(EntityUid uid, EntityUid user, EntityUid? target, HealthAnalyzerComponent? healthAnalyzer)
-        {
-            if (!Resolve(uid, ref healthAnalyzer))
-                return;
-
-            if (target == null || !_uiSystem.TryGetUi(uid, HealthAnalyzerUiKey.Key, out var ui))
-                return;
-
-            if (!HasComp<DamageableComponent>(target))
-                return;
-
-            float bodyTemperature;
-            if (TryComp<TemperatureComponent>(target, out var temp))
-                bodyTemperature = temp.CurrentTemperature;
-            else
-                bodyTemperature = float.NaN;
-
-            float bloodAmount;
-            if (TryComp<BloodstreamComponent>(target, out var bloodstream) &&
-                _solutionContainerSystem.ResolveSolution(target.Value, bloodstream.BloodSolutionName, ref bloodstream.BloodSolution, out var bloodSolution))
-                bloodAmount = bloodSolution.FillFraction;
-            else
-                bloodAmount = float.NaN;
-
-            OpenUserInterface(user, uid);
-
-            _uiSystem.SendUiMessage(ui, new HealthAnalyzerScannedUserMessage(
-                GetNetEntity(target),
-                bodyTemperature,
-                bloodAmount
-            ));
-        }
+        _audio.PlayPvs(uid.Comp.ScanningEndSound, uid);
+
+        OpenUserInterface(args.User, uid);
+        BeginAnalyzingEntity(uid, args.Target.Value);
+        args.Handled = true;
+    }
+
+    /// <summary>
+    /// Turn off when placed into a storage item or moved between slots/hands
+    /// </summary>
+    private void OnInsertedIntoContainer(Entity<HealthAnalyzerComponent> uid, ref EntGotInsertedIntoContainerMessage args)
+    {
+        if (uid.Comp.ScannedEntity is { } patient)
+            StopAnalyzingEntity(uid, patient);
+    }
+
+    /// <summary>
+    /// Disable continuous updates once battery is dead
+    /// </summary>
+    private void OnPowerCellSlotEmpty(Entity<HealthAnalyzerComponent> uid, ref PowerCellSlotEmptyEvent args)
+    {
+        if (uid.Comp.ScannedEntity is { } patient)
+            StopAnalyzingEntity(uid, patient);
+    }
+
+    /// <summary>
+    /// Turn off the analyser when dropped
+    /// </summary>
+    private void OnDropped(Entity<HealthAnalyzerComponent> uid, ref DroppedEvent args)
+    {
+        if (uid.Comp.ScannedEntity is { } patient)
+            StopAnalyzingEntity(uid, patient);
+    }
+
+    private void OpenUserInterface(EntityUid user, EntityUid analyzer)
+    {
+        if (!TryComp<ActorComponent>(user, out var actor) || !_uiSystem.TryGetUi(analyzer, HealthAnalyzerUiKey.Key, out var ui))
+            return;
+
+        _uiSystem.OpenUi(ui, actor.PlayerSession);
+    }
+
+    /// <summary>
+    /// Mark the entity as having its health analyzed, and link the analyzer to it
+    /// </summary>
+    /// <param name="healthAnalyzer">The health analyzer that should receive the updates</param>
+    /// <param name="target">The entity to start analyzing</param>
+    private void BeginAnalyzingEntity(Entity<HealthAnalyzerComponent> healthAnalyzer, EntityUid target)
+    {
+        //Link the health analyzer to the scanned entity
+        healthAnalyzer.Comp.ScannedEntity = target;
+
+        _cell.SetPowerCellDrawEnabled(healthAnalyzer, true);
+
+        UpdateScannedUser(healthAnalyzer, target, true);
+    }
+
+    /// <summary>
+    /// Remove the analyzer from the active list, and remove the component if it has no active analyzers
+    /// </summary>
+    /// <param name="healthAnalyzer">The health analyzer that's receiving the updates</param>
+    /// <param name="target">The entity to analyze</param>
+    private void StopAnalyzingEntity(Entity<HealthAnalyzerComponent> healthAnalyzer, EntityUid target)
+    {
+        //Unlink the analyzer
+        healthAnalyzer.Comp.ScannedEntity = null;
+
+        _cell.SetPowerCellDrawEnabled(target, false);
+
+        UpdateScannedUser(healthAnalyzer, target, false);
+    }
+
+    /// <summary>
+    /// Send an update for the target to the healthAnalyzer
+    /// </summary>
+    /// <param name="healthAnalyzer">The health analyzer</param>
+    /// <param name="target">The entity being scanned</param>
+    /// <param name="scanMode">True makes the UI show ACTIVE, False makes the UI show INACTIVE</param>
+    public void UpdateScannedUser(EntityUid healthAnalyzer, EntityUid target, bool scanMode)
+    {
+        if (!_uiSystem.TryGetUi(healthAnalyzer, HealthAnalyzerUiKey.Key, out var ui))
+            return;
+
+        if (!HasComp<DamageableComponent>(target))
+            return;
+
+        var bodyTemperature = float.NaN;
+
+        if (TryComp<TemperatureComponent>(target, out var temp))
+            bodyTemperature = temp.CurrentTemperature;
+
+        var bloodAmount = float.NaN;
+
+        if (TryComp<BloodstreamComponent>(target, out var bloodstream) &&
+            _solutionContainerSystem.ResolveSolution(target, bloodstream.BloodSolutionName, ref bloodstream.BloodSolution, out var bloodSolution))
+            bloodAmount = bloodSolution.FillFraction;
+
+        _uiSystem.SendUiMessage(ui, new HealthAnalyzerScannedUserMessage(
+            GetNetEntity(target),
+            bodyTemperature,
+            bloodAmount,
+            scanMode
+        ));
     }
 }
index eb50323d38f878e608eb590a8567c4878d67c5fa..1e2c2575d9c59b07e4f86c676f680fcdcebe177a 100644 (file)
@@ -11,12 +11,14 @@ public sealed class HealthAnalyzerScannedUserMessage : BoundUserInterfaceMessage
     public readonly NetEntity? TargetEntity;
     public float Temperature;
     public float BloodLevel;
+    public bool? ScanMode;
 
-    public HealthAnalyzerScannedUserMessage(NetEntity? targetEntity, float temperature, float bloodLevel)
+    public HealthAnalyzerScannedUserMessage(NetEntity? targetEntity, float temperature, float bloodLevel, bool? scanMode)
     {
         TargetEntity = targetEntity;
         Temperature = temperature;
         BloodLevel = bloodLevel;
+        ScanMode = scanMode;
     }
 }
 
index d232be5c4d9c99f609a053f1b35946f8a9903ee4..9b0a8dd3ee3acf88862e7cc93c970df416d74c36 100644 (file)
@@ -8,6 +8,10 @@ health-analyzer-window-damage-group-text = {$damageGroup}: {$amount}
 health-analyzer-window-damage-type-text = {$damageType}: {$amount}
 health-analyzer-window-damage-type-duplicate-text = {$damageType}: {$amount} (duplicate)
 
+health-analyzer-window-scan-mode-text = Scan Mode:
+health-analyzer-window-scan-mode-active = ACTIVE
+health-analyzer-window-scan-mode-inactive = INACTIVE
+
 health-analyzer-window-damage-group-Brute = Brute
 health-analyzer-window-damage-type-Blunt = Blunt
 health-analyzer-window-damage-type-Slash = Slash
index 752f98740adcb3dcce86484fa119b8fbd33ed6ac..64bd04569b5478288f86b1df0e8a69bc048a0ad2 100644 (file)
@@ -45,8 +45,7 @@
   suffix: Powered
   components:
   - type: PowerCellDraw
-    drawRate: 0
-    useRate: 20
+    drawRate: 1.2 #Calculated for 5 minutes on a small cell
   - type: ActivatableUIRequiresPowerCell
 
 - type: entity