]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Feature/auto sync node scanner (#36635)
authorFildrance <fildrance@gmail.com>
Sun, 27 Apr 2025 15:11:13 +0000 (18:11 +0300)
committerGitHub <noreply@github.com>
Sun, 27 Apr 2025 15:11:13 +0000 (11:11 -0400)
* feat: node scanner now auto-updates artifact details if in range

* refactor: minor cleanup

* refactor: optimization for update and query of range checking

* refactor: fix xml-doc

---------

Co-authored-by: pa.pecherskij <pa.pecherskij@interfax.ru>
Content.Client/Xenoarchaeology/Equipment/NodeScannerSystem.cs [deleted file]
Content.Client/Xenoarchaeology/Ui/NodeScannerBoundUserInterface.cs
Content.Client/Xenoarchaeology/Ui/NodeScannerDisplay.xaml.cs
Content.Server/Xenoarchaeology/Equipment/Systems/NodeScannerSystem.cs [deleted file]
Content.Shared/Xenoarchaeology/Equipment/Components/NodeScannerComponent.cs
Content.Shared/Xenoarchaeology/Equipment/NodeScannerSystem.cs [new file with mode: 0644]
Content.Shared/Xenoarchaeology/Equipment/SharedNodeScannerSystem.cs [deleted file]
Resources/Locale/en-US/xenoarchaeology/node-scanner.ftl

diff --git a/Content.Client/Xenoarchaeology/Equipment/NodeScannerSystem.cs b/Content.Client/Xenoarchaeology/Equipment/NodeScannerSystem.cs
deleted file mode 100644 (file)
index 9b517c0..0000000
+++ /dev/null
@@ -1,31 +0,0 @@
-using Content.Client.Xenoarchaeology.Ui;
-using Content.Shared.Xenoarchaeology.Equipment;
-using Content.Shared.Xenoarchaeology.Equipment.Components;
-using Robust.Client.GameObjects;
-
-namespace Content.Client.Xenoarchaeology.Equipment;
-
-/// <inheritdoc cref="SharedNodeScannerSystem"/>
-public sealed class NodeScannerSystem : SharedNodeScannerSystem
-{
-    [Dependency] private readonly UserInterfaceSystem _ui = default!;
-
-    /// <inheritdoc />
-    public override void Initialize()
-    {
-        base.Initialize();
-
-        SubscribeLocalEvent<NodeScannerComponent, AfterAutoHandleStateEvent>(OnAnalysisConsoleAfterAutoHandleState);
-    }
-
-    protected override void TryOpenUi(Entity<NodeScannerComponent> device, EntityUid actor)
-    {
-        _ui.TryOpenUi(device.Owner, NodeScannerUiKey.Key, actor, true);
-    }
-
-    private void OnAnalysisConsoleAfterAutoHandleState(Entity<NodeScannerComponent> ent, ref AfterAutoHandleStateEvent args)
-    {
-        if (_ui.TryGetOpenUi<NodeScannerBoundUserInterface>(ent.Owner, NodeScannerUiKey.Key, out var bui))
-            bui.Update(ent);
-    }
-}
index 3ed937c32e6f2233ff6b5b041f43bb1ef3bcf6ee..a30bde47d7281160a99eefa97ead094c148c6ad8 100644 (file)
@@ -1,4 +1,3 @@
-using Content.Shared.Xenoarchaeology.Equipment.Components;
 using Robust.Client.UserInterface;
 
 namespace Content.Client.Xenoarchaeology.Ui;
@@ -18,15 +17,6 @@ public sealed class NodeScannerBoundUserInterface(EntityUid owner, Enum uiKey) :
 
         _scannerDisplay = this.CreateWindow<NodeScannerDisplay>();
         _scannerDisplay.SetOwner(Owner);
-        _scannerDisplay.OnClose += Close;
-    }
-
-    /// <summary>
-    /// Update UI state based on corresponding component.
-    /// </summary>
-    public void Update(Entity<NodeScannerComponent> ent)
-    {
-        _scannerDisplay?.Update(ent);
     }
 
     /// <inheritdoc />
index f687f80073f0666c67fe4397e2c5d37d05a7ac62..372471ab2e5eb8a53e1551d4bd92ee1d8f16e8d8 100644 (file)
@@ -1,8 +1,12 @@
 using Content.Client.UserInterface.Controls;
+using Content.Shared.NameIdentifier;
+using Content.Shared.Xenoarchaeology.Artifact;
+using Content.Shared.Xenoarchaeology.Artifact.Components;
 using Content.Shared.Xenoarchaeology.Equipment.Components;
 using Robust.Client.AutoGenerated;
 using Robust.Client.UserInterface.Controls;
 using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Timing;
 
 namespace Content.Client.Xenoarchaeology.Ui;
 
@@ -10,12 +14,21 @@ namespace Content.Client.Xenoarchaeology.Ui;
 public sealed partial class NodeScannerDisplay : FancyWindow
 {
     [Dependency] private readonly IEntityManager _ent = default!;
+    [Dependency] private readonly IGameTiming _timing= default!;
+
+    private readonly SharedXenoArtifactSystem _artifact;
+    private TimeSpan? _nextUpdate;
+    private EntityUid _owner;
+    private TimeSpan _updateFromAttachedFrequency;
+    private readonly HashSet<string> _triggeredNodeNames = new();
 
     public NodeScannerDisplay()
     {
         RobustXamlLoader.Load(this);
 
         IoCManager.InjectDependencies(this);
+
+        _artifact = _ent.System<SharedXenoArtifactSystem>();
     }
 
     /// <summary>
@@ -30,30 +43,80 @@ public sealed partial class NodeScannerDisplay : FancyWindow
             return;
         }
 
-        Update((scannerEntityUid, scannerComponent));
+        _updateFromAttachedFrequency = scannerComponent.DisplayDataUpdateInterval;
+        _owner = scannerEntityUid;
+    }
+
+    /// <inheritdoc />
+    protected override void FrameUpdate(FrameEventArgs args)
+    {
+        base.FrameUpdate(args);
+
+        if(_nextUpdate  != null && _timing.CurTime < _nextUpdate)
+            return;
+
+        _nextUpdate = _timing.CurTime + _updateFromAttachedFrequency;
+
+        if (!_ent.TryGetComponent(_owner, out NodeScannerConnectedComponent? connectedScanner))
+        {
+            Update(false, ArtifactState.None);
+            return;
+        }
+
+        var attachedArtifactEnt = connectedScanner.AttachedTo;
+        if (!_ent.TryGetComponent(attachedArtifactEnt, out XenoArtifactComponent? artifactComponent))
+            return;
+
+        _ent.TryGetComponent(attachedArtifactEnt, out XenoArtifactUnlockingComponent? unlockingComponent);
+
+        _triggeredNodeNames.Clear();
+        ArtifactState artifactState;
+        if (unlockingComponent == null)
+        {
+            var timeToUnlockAvailable = artifactComponent.NextUnlockTime - _timing.CurTime;
+            artifactState = timeToUnlockAvailable > TimeSpan.Zero
+                ? ArtifactState.Cooldown
+                : ArtifactState.Ready;
+        }
+        else
+        {
+            var triggeredIndexes = unlockingComponent.TriggeredNodeIndexes;
+
+            foreach (var triggeredIndex in triggeredIndexes)
+            {
+                var node = _artifact.GetNode((attachedArtifactEnt, artifactComponent), triggeredIndex);
+                var triggeredNodeName = (_ent.GetComponentOrNull<NameIdentifierComponent>(node)?.Identifier ?? 0).ToString("D3");
+                _triggeredNodeNames.Add(triggeredNodeName);
+            }
+
+            artifactState = ArtifactState.Unlocking;
+        }
+
+        Update(true, artifactState, _triggeredNodeNames);
     }
 
     /// <summary>
     /// Updates labels with scanned artifact data and list of triggered nodes from component.
     /// </summary>
-    public void Update(Entity<NodeScannerComponent> ent)
+    private void Update(bool isConnected, ArtifactState artifactState, HashSet<string>? triggeredNodeNames = null)
     {
-        ArtifactStateLabel.Text = GetState(ent);
-        var scannedAt = ent.Comp.ScannedAt;
-        NodeScannerState.Text = scannedAt > TimeSpan.Zero
-            ? Loc.GetString("node-scanner-artifact-scanned-time", ("time", scannedAt.Value.ToString(@"hh\:mm\:ss")))
-            : Loc.GetString("node-scanner-artifact-scanned-time-none");
+        ArtifactStateLabel.Text = GetStateText(artifactState);
+        NodeScannerState.Text = isConnected
+            ? Loc.GetString("node-scanner-artifact-connected")
+            : Loc.GetString("node-scanner-artifact-non-connected");
 
         ActiveNodesList.Children.Clear();
 
-        var triggeredNodesSnapshot = ent.Comp.TriggeredNodesSnapshot;
-        if (triggeredNodesSnapshot.Count > 0)
+        if (triggeredNodeNames == null)
+            return;
+
+        if (triggeredNodeNames.Count > 0)
         {
             // show list of triggered nodes instead of 'no data' placeholder
             NoActiveNodeDataLabel.Visible = false;
             ActiveNodesList.Visible = true;
 
-            foreach (var nodeId in triggeredNodesSnapshot)
+            foreach (var nodeId in triggeredNodeNames)
             {
                 var nodeLabel = new Button
                 {
@@ -73,9 +136,9 @@ public sealed partial class NodeScannerDisplay : FancyWindow
         }
     }
 
-    private string GetState(Entity<NodeScannerComponent> ent)
+    private string GetStateText(ArtifactState state)
     {
-        return ent.Comp.ArtifactState switch
+        return state switch
         {
             ArtifactState.None => "\u2800", // placeholder for line to not be squeezed
             ArtifactState.Ready => Loc.GetString("node-scanner-artifact-state-ready"),
diff --git a/Content.Server/Xenoarchaeology/Equipment/Systems/NodeScannerSystem.cs b/Content.Server/Xenoarchaeology/Equipment/Systems/NodeScannerSystem.cs
deleted file mode 100644 (file)
index 2f8cd66..0000000
+++ /dev/null
@@ -1,13 +0,0 @@
-using Content.Shared.Xenoarchaeology.Equipment;
-using Content.Shared.Xenoarchaeology.Equipment.Components;
-
-namespace Content.Server.Xenoarchaeology.Equipment.Systems;
-
-/// <inheritdoc cref="SharedNodeScannerSystem"/>
-public sealed class NodeScannerSystem : SharedNodeScannerSystem
-{
-    protected override void TryOpenUi(Entity<NodeScannerComponent> device, EntityUid actor)
-    {
-        // no-op
-    }
-}
index 89403ee1e39faf927fc77211522b010a5338f02c..72223a366b086c4afa2fbbe3239ab6b539b846ea 100644 (file)
@@ -1,38 +1,54 @@
 using Robust.Shared.GameStates;
 using Robust.Shared.Serialization;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
 
 namespace Content.Shared.Xenoarchaeology.Equipment.Components;
 
 /// <summary>
-/// Component for managing data stored on NodeScanner hand-held device.
-/// Can store snapshot list of currently triggered artifact nodes.
+/// Component for NodeScanner hand-held device settings.
 /// </summary>
-[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true), Access(typeof(SharedNodeScannerSystem))]
+[RegisterComponent, NetworkedComponent]
+[Access(typeof(NodeScannerSystem))]
 public sealed partial class NodeScannerComponent : Component
 {
     /// <summary>
-    /// Identity-names (3-digit codes) of nodes that are triggered on scanned artifact.
+    /// Maximum range for keeping connection to artifact.
     /// </summary>
-    [DataField, AutoNetworkedField]
-    public HashSet<string> TriggeredNodesSnapshot = new();
+    [DataField]
+    public int MaxLinkedRange = 5;
 
     /// <summary>
-    /// State of artifact on the moment of scanning.
+    /// Update interval for link info.
     /// </summary>
-    [DataField, AutoNetworkedField]
-    public ArtifactState ArtifactState;
+    [DataField]
+    public TimeSpan DisplayDataUpdateInterval = TimeSpan.FromSeconds(1);
+}
 
+/// <summary>
+/// Component-marker that node scanner device (<see cref="NodeScannerComponent"/>) is connected to artifact.
+/// </summary>
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true), AutoGenerateComponentPause]
+public sealed partial class NodeScannerConnectedComponent : Component
+{
     /// <summary>
-    /// Time until next unlocking of scanned artifact can be started.
+    /// Xeno artifact entity, to which scanner is attached currently.
+    /// Upon detaching this component should be removed.
     /// </summary>
     [DataField, AutoNetworkedField]
-    public TimeSpan? WaitTime;
+    public EntityUid AttachedTo;
 
     /// <summary>
-    /// Moment of gametime, at which last artifact scanning was done.
+    /// Next update tick gametime.
     /// </summary>
-    [DataField, AutoNetworkedField]
-    public TimeSpan? ScannedAt;
+    [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
+    [AutoPausedField]
+    public TimeSpan NextUpdate = TimeSpan.Zero;
+
+    /// <summary>
+    /// Update interval for link info.
+    /// </summary>
+    [DataField]
+    public TimeSpan LinkUpdateInterval = TimeSpan.FromSeconds(1);
 }
 
 /// <summary>
diff --git a/Content.Shared/Xenoarchaeology/Equipment/NodeScannerSystem.cs b/Content.Shared/Xenoarchaeology/Equipment/NodeScannerSystem.cs
new file mode 100644 (file)
index 0000000..81802d6
--- /dev/null
@@ -0,0 +1,100 @@
+using Content.Shared.Interaction;
+using Content.Shared.Timing;
+using Content.Shared.Verbs;
+using Content.Shared.Xenoarchaeology.Artifact.Components;
+using Content.Shared.Xenoarchaeology.Equipment.Components;
+using Robust.Shared.Timing;
+
+namespace Content.Shared.Xenoarchaeology.Equipment;
+
+/// <summary> Controls behaviour of artifact node scanner device. </summary>
+public sealed class NodeScannerSystem : EntitySystem
+{
+    [Dependency] private readonly UseDelaySystem _useDelay = default!;
+    [Dependency] private readonly IGameTiming _timing = default!;
+    [Dependency] private readonly SharedUserInterfaceSystem _ui = default!;
+    [Dependency] private readonly SharedTransformSystem _transform = default!;
+
+    /// <inheritdoc/>
+    public override void Initialize()
+    {
+        SubscribeLocalEvent<NodeScannerComponent, BeforeRangedInteractEvent>(OnBeforeRangedInteract);
+        SubscribeLocalEvent<NodeScannerComponent, GetVerbsEvent<UtilityVerb>>(AddScanVerb);
+    }
+
+    /// <inheritdoc />
+    public override void Update(float frameTime)
+    {
+        var scannerQuery = EntityQueryEnumerator<NodeScannerConnectedComponent, NodeScannerComponent, TransformComponent>();
+        while (scannerQuery.MoveNext(out var uid, out var connected, out var scanner, out var transform))
+        {
+            if (connected.NextUpdate > _timing.CurTime)
+                continue;
+
+            connected.NextUpdate = _timing.CurTime + connected.LinkUpdateInterval;
+
+            var attachedArtifact = connected.AttachedTo;
+            var artifactCoordinates = Transform(attachedArtifact).Coordinates;
+            if (!_transform.InRange(artifactCoordinates, transform.Coordinates, scanner.MaxLinkedRange))
+            {
+                //scanner is too far, disconnect
+                RemCompDeferred(uid, connected);
+            }
+        }
+    }
+
+    private void OnBeforeRangedInteract(EntityUid uid, NodeScannerComponent component, BeforeRangedInteractEvent args)
+    {
+        if (args.Handled || !args.CanReach || args.Target is not { } target)
+            return;
+
+        Entity<XenoArtifactUnlockingComponent?> unlockingEnt = TryComp<XenoArtifactUnlockingComponent>(target, out var unlockingComponent)
+            ? (target, unlockingComponent)
+            : (target, null);
+
+        Attach((uid, component), unlockingEnt, args.User);
+
+        args.Handled = true;
+    }
+
+    private void AddScanVerb(EntityUid uid, NodeScannerComponent component, GetVerbsEvent<UtilityVerb> args)
+    {
+        if (!args.CanAccess)
+            return;
+
+        if (!TryComp<XenoArtifactUnlockingComponent>(args.Target, out var unlockingComponent))
+            return;
+
+        var verb = new UtilityVerb
+        {
+            Act = () => Attach((uid, component), (args.Target, unlockingComponent), args.User),
+            Text = Loc.GetString("node-scan-tooltip")
+        };
+
+        args.Verbs.Add(verb);
+    }
+
+    private void Attach(
+        Entity<NodeScannerComponent> device,
+        Entity<XenoArtifactUnlockingComponent?> unlockingEnt,
+        EntityUid actor
+    )
+    {
+        if (!_timing.IsFirstTimePredicted)
+            return;
+
+        if (TryComp(device, out UseDelayComponent? useDelay)
+            && !_useDelay.TryResetDelay((device, useDelay), true))
+            return;
+
+        var connected = EnsureComp<NodeScannerConnectedComponent>(device);
+        EntityUid artifact = unlockingEnt;
+        if (connected.AttachedTo != artifact)
+        {
+            connected.AttachedTo = artifact;
+            Dirty(device, connected);
+        }
+
+        _ui.TryOpenUi((device, null), NodeScannerUiKey.Key, actor, predicted: true);
+    }
+}
diff --git a/Content.Shared/Xenoarchaeology/Equipment/SharedNodeScannerSystem.cs b/Content.Shared/Xenoarchaeology/Equipment/SharedNodeScannerSystem.cs
deleted file mode 100644 (file)
index 7589cc8..0000000
+++ /dev/null
@@ -1,120 +0,0 @@
-using Content.Shared.Interaction;
-using Content.Shared.NameIdentifier;
-using Content.Shared.Timing;
-using Content.Shared.Verbs;
-using Content.Shared.Xenoarchaeology.Artifact;
-using Content.Shared.Xenoarchaeology.Artifact.Components;
-using Content.Shared.Xenoarchaeology.Equipment.Components;
-using Robust.Shared.Timing;
-
-namespace Content.Shared.Xenoarchaeology.Equipment;
-
-/// <summary> Controls behaviour of artifact node scanner device. </summary>
-public abstract class SharedNodeScannerSystem : EntitySystem
-{
-    [Dependency] private readonly UseDelaySystem _useDelay = default!;
-    [Dependency] private readonly SharedXenoArtifactSystem _artifact = default!;
-    [Dependency] private readonly IGameTiming _timing = default!;
-
-    /// <inheritdoc/>
-    public override void Initialize()
-    {
-        SubscribeLocalEvent<NodeScannerComponent, BeforeRangedInteractEvent>(OnBeforeRangedInteract);
-        SubscribeLocalEvent<NodeScannerComponent, GetVerbsEvent<UtilityVerb>>(AddScanVerb);
-    }
-
-    private void OnBeforeRangedInteract(EntityUid uid, NodeScannerComponent component, BeforeRangedInteractEvent args)
-    {
-        if (args.Handled || !args.CanReach || args.Target is not { } target)
-            return;
-
-        Entity<XenoArtifactUnlockingComponent?> unlockingEnt = TryComp<XenoArtifactUnlockingComponent>(target, out var unlockingComponent)
-            ? (target, unlockingComponent)
-            : (target, null);
-
-        TryMakeActiveNodesSnapshot((uid, component), unlockingEnt, args.User);
-
-        args.Handled = true;
-    }
-
-    private void AddScanVerb(EntityUid uid, NodeScannerComponent component, GetVerbsEvent<UtilityVerb> args)
-    {
-        if (!args.CanAccess)
-            return;
-
-        if (!TryComp<XenoArtifactUnlockingComponent>(args.Target, out var unlockingComponent))
-            return;
-
-        var verb = new UtilityVerb
-        {
-            Act = () =>
-            {
-                TryMakeActiveNodesSnapshot((uid, component), (args.Target, unlockingComponent), args.User);
-            },
-            Text = Loc.GetString("node-scan-tooltip")
-        };
-
-        args.Verbs.Add(verb);
-    }
-
-    private void TryMakeActiveNodesSnapshot(
-        Entity<NodeScannerComponent> device,
-        Entity<XenoArtifactUnlockingComponent?> unlockingEnt,
-        EntityUid actor
-    )
-    {
-        if (!_timing.IsFirstTimePredicted)
-            return;
-
-        if (TryComp(device, out UseDelayComponent? useDelay)
-            && !_useDelay.TryResetDelay((device, useDelay), true))
-            return;
-
-        if (!TryComp<XenoArtifactComponent>(unlockingEnt.Owner, out var artifactComponent))
-            return;
-
-        TryOpenUi(device, actor);
-
-        TimeSpan? waitTime = null;
-        HashSet<string> triggeredNodeNames;
-        ArtifactState artifactState;
-        if (unlockingEnt.Comp == null)
-        {
-            triggeredNodeNames = new HashSet<string>();
-            var timeToUnlockAvailable = artifactComponent.NextUnlockTime - _timing.CurTime;
-            if (timeToUnlockAvailable > TimeSpan.Zero)
-            {
-                artifactState = ArtifactState.Cooldown;
-                waitTime = timeToUnlockAvailable;
-            }
-            else
-            {
-                artifactState = ArtifactState.Ready;
-            }
-        }
-        else
-        {
-            var triggeredIndexes = unlockingEnt.Comp.TriggeredNodeIndexes;
-            triggeredNodeNames = new HashSet<string>(triggeredIndexes.Count);
-
-            foreach (var triggeredIndex in triggeredIndexes)
-            {
-                var node = _artifact.GetNode((unlockingEnt.Owner, artifactComponent), triggeredIndex);
-                var triggeredNodeName = (CompOrNull<NameIdentifierComponent>(node)?.Identifier ?? 0).ToString("D3");
-                triggeredNodeNames.Add(triggeredNodeName);
-            }
-
-            artifactState = ArtifactState.Unlocking;
-            waitTime = _timing.CurTime - unlockingEnt.Comp.EndTime;
-        }
-
-        device.Comp.ArtifactState = artifactState;
-        device.Comp.WaitTime = waitTime;
-        device.Comp.TriggeredNodesSnapshot = triggeredNodeNames;
-        device.Comp.ScannedAt = _timing.CurTime;
-
-        Dirty(device);
-    }
-
-    protected abstract void TryOpenUi(Entity<NodeScannerComponent> device, EntityUid actor);
-}
index f43843203e3cd9f541add973530ea6dc46c51e76..d9281683461231db5b6e97ea0cc9bb4c6e171ece 100644 (file)
@@ -2,8 +2,8 @@ node-scan-tooltip = Scan active nodes
 node-scan-no-data = No active node data found
 node-scan-display-title = Node scanner
 
-node-scanner-artifact-scanned-time = Artifact last scanned at {$time}
 node-scanner-artifact-state-ready = Artifact is ready for interaction
 node-scanner-artifact-state-unlocking = Artifact is resonating with your actions
 node-scanner-artifact-state-cooldown = Artifact is resting
-node-scanner-artifact-scanned-time-none = Scan artifact to see current state
+node-scanner-artifact-connected = Scanning artifact
+node-scanner-artifact-non-connected = Artifact not found or out of range