From 4129c77a5b7c4d5cdfa4763881bdaa443c128f33 Mon Sep 17 00:00:00 2001 From: Rainfey Date: Tue, 6 Feb 2024 13:20:09 +0000 Subject: [PATCH] Make Health Analysers UI continuously update (#22449) --- .../UI/HealthAnalyzerWindow.xaml | 16 +- .../UI/HealthAnalyzerWindow.xaml.cs | 15 +- .../Components/HealthAnalyzerComponent.cs | 74 ++++-- Content.Server/Medical/CryoPodSystem.cs | 3 +- .../Medical/HealthAnalyzerSystem.cs | 235 +++++++++++++----- .../HealthAnalyzerScannedUserMessage.cs | 4 +- .../components/health-analyzer-component.ftl | 4 + .../Specific/Medical/healthanalyzer.yml | 3 +- 8 files changed, 253 insertions(+), 101 deletions(-) diff --git a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml index b78c3c6a56..28872ef1d5 100644 --- a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml +++ b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml @@ -1,4 +1,6 @@ - @@ -12,6 +14,16 @@ Name="PatientDataContainer" Orientation="Vertical" Margin="0 0 5 10"> + + - \ No newline at end of file + diff --git a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs index 588eb88502..2f19cd0a05 100644 --- a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs +++ b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs @@ -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) diff --git a/Content.Server/Medical/Components/HealthAnalyzerComponent.cs b/Content.Server/Medical/Components/HealthAnalyzerComponent.cs index 39b1df573f..0002f275c5 100644 --- a/Content.Server/Medical/Components/HealthAnalyzerComponent.cs +++ b/Content.Server/Medical/Components/HealthAnalyzerComponent.cs @@ -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; + +/// +/// After scanning, retrieves the target Uid to use with its related UI. +/// +[RegisterComponent] +[Access(typeof(HealthAnalyzerSystem))] +public sealed partial class HealthAnalyzerComponent : Component { /// - /// After scanning, retrieves the target Uid to use with its related UI. + /// When should the next update be sent for the patient + /// + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] + public TimeSpan NextUpdate = TimeSpan.Zero; + + /// + /// The delay between patient health updates + /// + [DataField] + public TimeSpan UpdateInterval = TimeSpan.FromSeconds(1); + + /// + /// How long it takes to scan someone. + /// + [DataField] + public TimeSpan ScanDelay = TimeSpan.FromSeconds(0.8); + + /// + /// Which entity has been scanned, for continuous updates + /// + [DataField] + public EntityUid? ScannedEntity; + + /// + /// The maximum range in tiles at which the analyzer can receive continuous updates + /// + [DataField] + public float MaxScanRange = 2.5f; + + /// + /// Sound played on scanning begin + /// + [DataField] + public SoundSpecifier? ScanningBeginSound; + + /// + /// Sound played on scanning end /// - [RegisterComponent] - public sealed partial class HealthAnalyzerComponent : Component - { - /// - /// How long it takes to scan someone. - /// - [DataField("scanDelay")] - public float ScanDelay = 0.8f; - - /// - /// Sound played on scanning begin - /// - [DataField("scanningBeginSound")] - public SoundSpecifier? ScanningBeginSound; - - /// - /// Sound played on scanning end - /// - [DataField("scanningEndSound")] - public SoundSpecifier? ScanningEndSound; - } + [DataField] + public SoundSpecifier? ScanningEndSound; } diff --git a/Content.Server/Medical/CryoPodSystem.cs b/Content.Server/Medical/CryoPodSystem.cs index 2f08dfddd1..25a47933a8 100644 --- a/Content.Server/Medical/CryoPodSystem.cs +++ b/Content.Server/Medical/CryoPodSystem.cs @@ -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 )); } diff --git a/Content.Server/Medical/HealthAnalyzerSystem.cs b/Content.Server/Medical/HealthAnalyzerSystem.cs index d9559a9626..5c7d265e61 100644 --- a/Content.Server/Medical/HealthAnalyzerSystem.cs +++ b/Content.Server/Medical/HealthAnalyzerSystem.cs @@ -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(OnEntityUnpaused); + SubscribeLocalEvent(OnAfterInteract); + SubscribeLocalEvent(OnDoAfter); + SubscribeLocalEvent(OnInsertedIntoContainer); + SubscribeLocalEvent(OnPowerCellSlotEmpty); + SubscribeLocalEvent(OnDropped); + } - public override void Initialize() + public override void Update(float frameTime) + { + var analyzerQuery = EntityQueryEnumerator(); + while (analyzerQuery.MoveNext(out var uid, out var component, out var transform)) { - base.Initialize(); - SubscribeLocalEvent(OnAfterInteract); - SubscribeLocalEvent(OnDoAfter); - } + //Update rate limited to 1 second + if (component.NextUpdate > _timing.CurTime) + continue; - private void OnAfterInteract(Entity entity, ref AfterInteractEvent args) - { - if (args.Target == null || !args.CanReach || !HasComp(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 entity, ref HealthAnalyzerDoAfterEvent args) - { - if (args.Handled || args.Cancelled || args.Target == null || !_cell.TryUseActivatableCharge(entity.Owner, user: args.User)) - return; + private void OnEntityUnpaused(Entity ent, ref EntityUnpausedEvent args) + { + ent.Comp.NextUpdate += args.PausedTime; + } - _audio.PlayPvs(entity.Comp.ScanningEndSound, args.User); + /// + /// Trigger the doafter for scanning + /// + private void OnAfterInteract(Entity uid, ref AfterInteractEvent args) + { + if (args.Target == null || !args.CanReach || !HasComp(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(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 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(target)) - return; - - float bodyTemperature; - if (TryComp(target, out var temp)) - bodyTemperature = temp.CurrentTemperature; - else - bodyTemperature = float.NaN; - - float bloodAmount; - if (TryComp(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; + } + + /// + /// Turn off when placed into a storage item or moved between slots/hands + /// + private void OnInsertedIntoContainer(Entity uid, ref EntGotInsertedIntoContainerMessage args) + { + if (uid.Comp.ScannedEntity is { } patient) + StopAnalyzingEntity(uid, patient); + } + + /// + /// Disable continuous updates once battery is dead + /// + private void OnPowerCellSlotEmpty(Entity uid, ref PowerCellSlotEmptyEvent args) + { + if (uid.Comp.ScannedEntity is { } patient) + StopAnalyzingEntity(uid, patient); + } + + /// + /// Turn off the analyser when dropped + /// + private void OnDropped(Entity uid, ref DroppedEvent args) + { + if (uid.Comp.ScannedEntity is { } patient) + StopAnalyzingEntity(uid, patient); + } + + private void OpenUserInterface(EntityUid user, EntityUid analyzer) + { + if (!TryComp(user, out var actor) || !_uiSystem.TryGetUi(analyzer, HealthAnalyzerUiKey.Key, out var ui)) + return; + + _uiSystem.OpenUi(ui, actor.PlayerSession); + } + + /// + /// Mark the entity as having its health analyzed, and link the analyzer to it + /// + /// The health analyzer that should receive the updates + /// The entity to start analyzing + private void BeginAnalyzingEntity(Entity healthAnalyzer, EntityUid target) + { + //Link the health analyzer to the scanned entity + healthAnalyzer.Comp.ScannedEntity = target; + + _cell.SetPowerCellDrawEnabled(healthAnalyzer, true); + + UpdateScannedUser(healthAnalyzer, target, true); + } + + /// + /// Remove the analyzer from the active list, and remove the component if it has no active analyzers + /// + /// The health analyzer that's receiving the updates + /// The entity to analyze + private void StopAnalyzingEntity(Entity healthAnalyzer, EntityUid target) + { + //Unlink the analyzer + healthAnalyzer.Comp.ScannedEntity = null; + + _cell.SetPowerCellDrawEnabled(target, false); + + UpdateScannedUser(healthAnalyzer, target, false); + } + + /// + /// Send an update for the target to the healthAnalyzer + /// + /// The health analyzer + /// The entity being scanned + /// True makes the UI show ACTIVE, False makes the UI show INACTIVE + public void UpdateScannedUser(EntityUid healthAnalyzer, EntityUid target, bool scanMode) + { + if (!_uiSystem.TryGetUi(healthAnalyzer, HealthAnalyzerUiKey.Key, out var ui)) + return; + + if (!HasComp(target)) + return; + + var bodyTemperature = float.NaN; + + if (TryComp(target, out var temp)) + bodyTemperature = temp.CurrentTemperature; + + var bloodAmount = float.NaN; + + if (TryComp(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 + )); } } diff --git a/Content.Shared/MedicalScanner/HealthAnalyzerScannedUserMessage.cs b/Content.Shared/MedicalScanner/HealthAnalyzerScannedUserMessage.cs index eb50323d38..1e2c2575d9 100644 --- a/Content.Shared/MedicalScanner/HealthAnalyzerScannedUserMessage.cs +++ b/Content.Shared/MedicalScanner/HealthAnalyzerScannedUserMessage.cs @@ -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; } } diff --git a/Resources/Locale/en-US/medical/components/health-analyzer-component.ftl b/Resources/Locale/en-US/medical/components/health-analyzer-component.ftl index d232be5c4d..9b0a8dd3ee 100644 --- a/Resources/Locale/en-US/medical/components/health-analyzer-component.ftl +++ b/Resources/Locale/en-US/medical/components/health-analyzer-component.ftl @@ -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 diff --git a/Resources/Prototypes/Entities/Objects/Specific/Medical/healthanalyzer.yml b/Resources/Prototypes/Entities/Objects/Specific/Medical/healthanalyzer.yml index 752f98740a..64bd04569b 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Medical/healthanalyzer.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Medical/healthanalyzer.yml @@ -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 -- 2.51.2