using System.Diagnostics.CodeAnalysis; using Content.Server.PowerCell; using Content.Shared.DoAfter; using Content.Shared.Interaction; using Content.Shared.Interaction.Events; using Content.Shared.Item.ItemToggle; using Content.Shared.Item.ItemToggle.Components; using Content.Shared.Popups; using Robust.Server.GameObjects; using Robust.Shared.Audio.Systems; using Robust.Shared.Containers; using Robust.Shared.Timing; namespace Content.Server.AbstractAnalyzer; public abstract class AbstractAnalyzerSystem : EntitySystem where TAnalyzerComponent : AbstractAnalyzerComponent where TAnalyzerDoAfterEvent : SimpleDoAfterEvent, new() { [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 ItemToggleSystem _toggle = default!; [Dependency] private readonly UserInterfaceSystem _uiSystem = default!; [Dependency] private readonly TransformSystem _transformSystem = default!; [Dependency] private readonly SharedPopupSystem _popupSystem = default!; public override void Initialize() { SubscribeLocalEvent(OnAfterInteract); SubscribeLocalEvent(OnDoAfter); SubscribeLocalEvent(OnInsertedIntoContainer); SubscribeLocalEvent(OnToggled); SubscribeLocalEvent(OnDropped); } public override void Update(float frameTime) { var analyzerQuery = EntityQueryEnumerator(); while (analyzerQuery.MoveNext(out var uid, out var component, out var transform)) { //Update rate limited to 1 second if (component.NextUpdate > _timing.CurTime || component.ScannedEntity is not { } target) continue; if (Deleted(target)) { StopAnalyzingEntity((uid, component), target); continue; } component.NextUpdate = _timing.CurTime + component.UpdateInterval; //Get distance between analyzer and the scanned entity var targetCoordinates = Transform(target).Coordinates; if (!_transformSystem.InRange(targetCoordinates, transform.Coordinates, component.MaxScanRange)) { //Range too far, disable updates StopAnalyzingEntity((uid, component), target); continue; } UpdateScannedUser(uid, target, true); } } /// /// Trigger the doafter for scanning /// private void OnAfterInteract(Entity uid, ref AfterInteractEvent args) { if (args.Target == null || !args.CanReach || !ValidScanTarget(args.Target) || !_cell.HasDrawCharge(uid, user: args.User)) return; _audio.PlayPvs(uid.Comp.ScanningBeginSound, uid); var doAfterCancelled = !_doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, args.User, uid.Comp.ScanDelay, new TAnalyzerDoAfterEvent(), uid, target: args.Target, used: uid) { NeedHand = true, BreakOnMove = true, }); if (args.Target == args.User || doAfterCancelled || uid.Comp.Silent || args.Target is null) return; if (ScanTargetPopupMessage(uid, args, out var msg)) _popupSystem.PopupEntity(msg, args.Target.Value, args.Target.Value, PopupType.Medium); } private void OnDoAfter(Entity uid, ref TAnalyzerDoAfterEvent args) { if (args.Handled || args.Cancelled || args.Target == null || !_cell.HasDrawCharge(uid, user: args.User)) return; if (!uid.Comp.Silent) _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 { }) _toggle.TryDeactivate(uid.Owner); } /// /// Disable continuous updates once turned off /// private void OnToggled(Entity ent, ref ItemToggledEvent args) { if (!args.Activated && ent.Comp.ScannedEntity is { } target) StopAnalyzingEntity(ent, target); } /// /// Turn off the analyser when dropped /// private void OnDropped(Entity uid, ref DroppedEvent args) { if (uid.Comp.ScannedEntity is { }) _toggle.TryDeactivate(uid.Owner); } private void OpenUserInterface(EntityUid user, EntityUid analyzer) { if (!_uiSystem.HasUi(analyzer, GetUiKey())) return; _uiSystem.OpenUi(analyzer, GetUiKey(), user); } /// /// Mark the entity as being analyzed, and link the analyzer to it /// /// The analyzer that should receive the updates /// The entity to start analyzing private void BeginAnalyzingEntity(Entity analyzer, EntityUid target) { //Link the analyzer to the scanned entity analyzer.Comp.ScannedEntity = target; _toggle.TryActivate(analyzer.Owner); _cell.SetDrawEnabled(analyzer.Owner, true); UpdateScannedUser(analyzer, target, true); } /// /// Remove the analyzer from the active list, and remove the component if it has no active analyzers /// /// The analyzer that's receiving the updates /// The entity to analyze private void StopAnalyzingEntity(Entity analyzer, EntityUid target) { //Unlink the analyzer analyzer.Comp.ScannedEntity = null; _toggle.TryDeactivate(analyzer.Owner); _cell.SetDrawEnabled(analyzer.Owner, false); UpdateScannedUser(analyzer, target, false); } /// /// Send an update for the target to the analyzer /// /// The analyzer /// The entity being scanned /// True makes the UI show ACTIVE, False makes the UI show INACTIVE public abstract void UpdateScannedUser(EntityUid analyzer, EntityUid target, bool scanMode); /// A byte enum key. protected abstract Enum GetUiKey(); /// /// The message the scan target receives on scan. /// /// true if the message should be shown protected abstract bool ScanTargetPopupMessage(Entity uid, AfterInteractEvent args, [NotNullWhen(true)] out string? message); /// /// Used to validate if a specific entity is a valid target for a specific analyzer. /// protected abstract bool ValidScanTarget(EntityUid? target); }