using System.Linq; using Content.Server._NF.Atmos.Components; using Content.Server.Atmos.EntitySystems; using Content.Server.Popups; using Content.Shared._NF.Atmos.Components; using Content.Shared.Atmos; using Content.Shared.Interaction; using Content.Shared.Interaction.Events; using JetBrains.Annotations; using Robust.Server.GameObjects; using static Content.Shared._NF.Atmos.Components.GasDepositScannerComponent; namespace Content.Server._NF.Atmos.EntitySystems; /// /// Logic for the gas deposit scanner. Largely based off of the GasAnalyzerSystem. /// [UsedImplicitly] public sealed class GasDepositScannerSystem : EntitySystem { [Dependency] private readonly PopupSystem _popup = default!; [Dependency] private readonly AtmosphereSystem _atmos = default!; [Dependency] private readonly SharedAppearanceSystem _appearance = default!; [Dependency] private readonly UserInterfaceSystem _userInterface = default!; [Dependency] private readonly SharedInteractionSystem _interactionSystem = default!; /// /// Minimum moles of a gas to be included in the list. /// private const float UIMinMoles = 0.01f; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnAfterInteract); SubscribeLocalEvent(OnDisabledMessage); SubscribeLocalEvent(OnDropped); } public override void Update(float frameTime) { var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out var scanner)) { // Don't update every tick scanner.AccumulatedFrametime += frameTime; if (scanner.AccumulatedFrametime < scanner.UpdateInterval) continue; scanner.AccumulatedFrametime -= scanner.UpdateInterval; if (!UpdateScanner(uid)) RemCompDeferred(uid); } } /// /// Activates the scanner when used in the world, scanning the target entity if it exists. /// private void OnAfterInteract(Entity entity, ref AfterInteractEvent args) { var target = args.Target; if (target != null && !_interactionSystem.InRangeUnobstructed((args.User, null), (target.Value, null))) { target = null; // if the target is out of reach, invalidate it args.Handled = true; return; } if (target != null) ActivateScanner(entity, args.User, target); args.Handled = true; } /// /// Handles scanner activation logic. /// private void ActivateScanner(Entity entity, EntityUid user, EntityUid? target = null) { if (!_userInterface.TryOpenUi(entity.Owner, GasDepositScannerUiKey.Key, user)) return; entity.Comp.Target = target; entity.Comp.User = user; entity.Comp.Enabled = true; Dirty(entity); _appearance.SetData(entity.Owner, GasDepositScannerVisuals.Enabled, entity.Comp.Enabled); EnsureComp(entity.Owner); UpdateScanner(entity.Owner, entity.Comp); } /// /// Close the UI, turn the scanner off, and don't update when it's dropped /// private void OnDropped(Entity entity, ref DroppedEvent args) { if (args.User is var userId && entity.Comp.Enabled) _popup.PopupEntity(Loc.GetString("gas-deposit-scanner-shutoff"), userId, userId); DisableScanner(entity, args.User); } /// /// Closes the UI, sets the icon to off, and removes it from the update list. /// private void DisableScanner(Entity entity, EntityUid? user = null) { _userInterface.CloseUi(entity.Owner, GasDepositScannerUiKey.Key, user); entity.Comp.Enabled = false; Dirty(entity); _appearance.SetData(entity.Owner, GasDepositScannerVisuals.Enabled, entity.Comp.Enabled); RemCompDeferred(entity.Owner); } /// /// Disables the analyzer when the user closes the UI /// private void OnDisabledMessage(Entity entity, ref GasDepositScannerDisableMessage message) { DisableScanner(entity); } /// /// Fetches fresh data for the scanner. Should only be called when the user requests an update. /// private bool UpdateScanner(EntityUid uid, GasDepositScannerComponent? component = null) { if (!Resolve(uid, ref component)) return false; // Check if the user has walked away from what they scanned. if (component.Target.HasValue) { if (!_interactionSystem.InRangeUnobstructed((component.User, null), (component.Target.Value, null))) { if (component.User is { } userId && component.Enabled) _popup.PopupEntity(Loc.GetString("gas-deposit-scanner-object-out-of-range"), userId, userId); component.Target = null; DisableScanner((uid, component), component.User); return false; } } GasEntry[]? gasMixList = null; if (component.Target != null) { if (Deleted(component.Target)) { component.Target = null; DisableScanner((uid, component), component.User); return false; } if (!TryComp(component.Target, out var gasDeposit)) { component.Target = null; DisableScanner((uid, component), component.User); return false; } gasMixList = GenerateGasEntryArray(gasDeposit.Deposit); } // Don't bother sending a UI message with no content, and stop updating I guess? if (gasMixList == null || gasMixList.Length <= 0) return false; _userInterface.ServerSendUiMessage(uid, GasDepositScannerUiKey.Key, new GasDepositScannerUserMessage(gasMixList.ToArray(), GetNetEntity(component.Target) ?? NetEntity.Invalid)); return true; } /// /// Generates a GasEntry array for a given GasMixture. /// private GasEntry[] GenerateGasEntryArray(GasMixture? mixture) { if (mixture == null) return []; var gases = new List(); for (var i = 0; i < Atmospherics.TotalNumberOfGases; i++) { var gas = _atmos.GetGas(i); if (mixture[i] <= UIMinMoles) continue; var gasName = Loc.GetString(gas.Name); ApproximateGasDepositSize depositSize; if (mixture[i] < 500.0) depositSize = ApproximateGasDepositSize.Trace; else if (mixture[i] < 3000.0) depositSize = ApproximateGasDepositSize.Small; else if (mixture[i] < 10000.0) depositSize = ApproximateGasDepositSize.Medium; else if (mixture[i] < 30000.0) depositSize = ApproximateGasDepositSize.Large; else depositSize = ApproximateGasDepositSize.Enormous; gases.Add(new GasEntry(gasName, depositSize)); } var gasesOrdered = gases.OrderByDescending(gas => gas.Amount); return gasesOrdered.ToArray(); } }