From: Fildrance Date: Sun, 27 Apr 2025 15:11:13 +0000 (+0300) Subject: Feature/auto sync node scanner (#36635) X-Git-Url: https://git.smokeofanarchy.ru/gitweb.cgi?a=commitdiff_plain;h=4690e62575c1e98e01fd165132b6c93fcfb63c5b;p=space-station-14.git Feature/auto sync node scanner (#36635) * 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 --- diff --git a/Content.Client/Xenoarchaeology/Equipment/NodeScannerSystem.cs b/Content.Client/Xenoarchaeology/Equipment/NodeScannerSystem.cs deleted file mode 100644 index 9b517c06a1..0000000000 --- a/Content.Client/Xenoarchaeology/Equipment/NodeScannerSystem.cs +++ /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; - -/// -public sealed class NodeScannerSystem : SharedNodeScannerSystem -{ - [Dependency] private readonly UserInterfaceSystem _ui = default!; - - /// - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnAnalysisConsoleAfterAutoHandleState); - } - - protected override void TryOpenUi(Entity device, EntityUid actor) - { - _ui.TryOpenUi(device.Owner, NodeScannerUiKey.Key, actor, true); - } - - private void OnAnalysisConsoleAfterAutoHandleState(Entity ent, ref AfterAutoHandleStateEvent args) - { - if (_ui.TryGetOpenUi(ent.Owner, NodeScannerUiKey.Key, out var bui)) - bui.Update(ent); - } -} diff --git a/Content.Client/Xenoarchaeology/Ui/NodeScannerBoundUserInterface.cs b/Content.Client/Xenoarchaeology/Ui/NodeScannerBoundUserInterface.cs index 3ed937c32e..a30bde47d7 100644 --- a/Content.Client/Xenoarchaeology/Ui/NodeScannerBoundUserInterface.cs +++ b/Content.Client/Xenoarchaeology/Ui/NodeScannerBoundUserInterface.cs @@ -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(); _scannerDisplay.SetOwner(Owner); - _scannerDisplay.OnClose += Close; - } - - /// - /// Update UI state based on corresponding component. - /// - public void Update(Entity ent) - { - _scannerDisplay?.Update(ent); } /// diff --git a/Content.Client/Xenoarchaeology/Ui/NodeScannerDisplay.xaml.cs b/Content.Client/Xenoarchaeology/Ui/NodeScannerDisplay.xaml.cs index f687f80073..372471ab2e 100644 --- a/Content.Client/Xenoarchaeology/Ui/NodeScannerDisplay.xaml.cs +++ b/Content.Client/Xenoarchaeology/Ui/NodeScannerDisplay.xaml.cs @@ -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 _triggeredNodeNames = new(); public NodeScannerDisplay() { RobustXamlLoader.Load(this); IoCManager.InjectDependencies(this); + + _artifact = _ent.System(); } /// @@ -30,30 +43,80 @@ public sealed partial class NodeScannerDisplay : FancyWindow return; } - Update((scannerEntityUid, scannerComponent)); + _updateFromAttachedFrequency = scannerComponent.DisplayDataUpdateInterval; + _owner = scannerEntityUid; + } + + /// + 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(node)?.Identifier ?? 0).ToString("D3"); + _triggeredNodeNames.Add(triggeredNodeName); + } + + artifactState = ArtifactState.Unlocking; + } + + Update(true, artifactState, _triggeredNodeNames); } /// /// Updates labels with scanned artifact data and list of triggered nodes from component. /// - public void Update(Entity ent) + private void Update(bool isConnected, ArtifactState artifactState, HashSet? 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 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 index 2f8cd66736..0000000000 --- a/Content.Server/Xenoarchaeology/Equipment/Systems/NodeScannerSystem.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Content.Shared.Xenoarchaeology.Equipment; -using Content.Shared.Xenoarchaeology.Equipment.Components; - -namespace Content.Server.Xenoarchaeology.Equipment.Systems; - -/// -public sealed class NodeScannerSystem : SharedNodeScannerSystem -{ - protected override void TryOpenUi(Entity device, EntityUid actor) - { - // no-op - } -} diff --git a/Content.Shared/Xenoarchaeology/Equipment/Components/NodeScannerComponent.cs b/Content.Shared/Xenoarchaeology/Equipment/Components/NodeScannerComponent.cs index 89403ee1e3..72223a366b 100644 --- a/Content.Shared/Xenoarchaeology/Equipment/Components/NodeScannerComponent.cs +++ b/Content.Shared/Xenoarchaeology/Equipment/Components/NodeScannerComponent.cs @@ -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; /// -/// 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. /// -[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true), Access(typeof(SharedNodeScannerSystem))] +[RegisterComponent, NetworkedComponent] +[Access(typeof(NodeScannerSystem))] public sealed partial class NodeScannerComponent : Component { /// - /// Identity-names (3-digit codes) of nodes that are triggered on scanned artifact. + /// Maximum range for keeping connection to artifact. /// - [DataField, AutoNetworkedField] - public HashSet TriggeredNodesSnapshot = new(); + [DataField] + public int MaxLinkedRange = 5; /// - /// State of artifact on the moment of scanning. + /// Update interval for link info. /// - [DataField, AutoNetworkedField] - public ArtifactState ArtifactState; + [DataField] + public TimeSpan DisplayDataUpdateInterval = TimeSpan.FromSeconds(1); +} +/// +/// Component-marker that node scanner device () is connected to artifact. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true), AutoGenerateComponentPause] +public sealed partial class NodeScannerConnectedComponent : Component +{ /// - /// 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. /// [DataField, AutoNetworkedField] - public TimeSpan? WaitTime; + public EntityUid AttachedTo; /// - /// Moment of gametime, at which last artifact scanning was done. + /// Next update tick gametime. /// - [DataField, AutoNetworkedField] - public TimeSpan? ScannedAt; + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] + [AutoPausedField] + public TimeSpan NextUpdate = TimeSpan.Zero; + + /// + /// Update interval for link info. + /// + [DataField] + public TimeSpan LinkUpdateInterval = TimeSpan.FromSeconds(1); } /// diff --git a/Content.Shared/Xenoarchaeology/Equipment/NodeScannerSystem.cs b/Content.Shared/Xenoarchaeology/Equipment/NodeScannerSystem.cs new file mode 100644 index 0000000000..81802d6b6e --- /dev/null +++ b/Content.Shared/Xenoarchaeology/Equipment/NodeScannerSystem.cs @@ -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; + +/// Controls behaviour of artifact node scanner device. +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!; + + /// + public override void Initialize() + { + SubscribeLocalEvent(OnBeforeRangedInteract); + SubscribeLocalEvent>(AddScanVerb); + } + + /// + public override void Update(float frameTime) + { + var scannerQuery = EntityQueryEnumerator(); + 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 unlockingEnt = TryComp(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 args) + { + if (!args.CanAccess) + return; + + if (!TryComp(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 device, + Entity unlockingEnt, + EntityUid actor + ) + { + if (!_timing.IsFirstTimePredicted) + return; + + if (TryComp(device, out UseDelayComponent? useDelay) + && !_useDelay.TryResetDelay((device, useDelay), true)) + return; + + var connected = EnsureComp(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 index 7589cc8971..0000000000 --- a/Content.Shared/Xenoarchaeology/Equipment/SharedNodeScannerSystem.cs +++ /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; - -/// Controls behaviour of artifact node scanner device. -public abstract class SharedNodeScannerSystem : EntitySystem -{ - [Dependency] private readonly UseDelaySystem _useDelay = default!; - [Dependency] private readonly SharedXenoArtifactSystem _artifact = default!; - [Dependency] private readonly IGameTiming _timing = default!; - - /// - public override void Initialize() - { - SubscribeLocalEvent(OnBeforeRangedInteract); - SubscribeLocalEvent>(AddScanVerb); - } - - private void OnBeforeRangedInteract(EntityUid uid, NodeScannerComponent component, BeforeRangedInteractEvent args) - { - if (args.Handled || !args.CanReach || args.Target is not { } target) - return; - - Entity unlockingEnt = TryComp(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 args) - { - if (!args.CanAccess) - return; - - if (!TryComp(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 device, - Entity unlockingEnt, - EntityUid actor - ) - { - if (!_timing.IsFirstTimePredicted) - return; - - if (TryComp(device, out UseDelayComponent? useDelay) - && !_useDelay.TryResetDelay((device, useDelay), true)) - return; - - if (!TryComp(unlockingEnt.Owner, out var artifactComponent)) - return; - - TryOpenUi(device, actor); - - TimeSpan? waitTime = null; - HashSet triggeredNodeNames; - ArtifactState artifactState; - if (unlockingEnt.Comp == null) - { - triggeredNodeNames = new HashSet(); - 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(triggeredIndexes.Count); - - foreach (var triggeredIndex in triggeredIndexes) - { - var node = _artifact.GetNode((unlockingEnt.Owner, artifactComponent), triggeredIndex); - var triggeredNodeName = (CompOrNull(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 device, EntityUid actor); -} diff --git a/Resources/Locale/en-US/xenoarchaeology/node-scanner.ftl b/Resources/Locale/en-US/xenoarchaeology/node-scanner.ftl index f43843203e..d928168346 100644 --- a/Resources/Locale/en-US/xenoarchaeology/node-scanner.ftl +++ b/Resources/Locale/en-US/xenoarchaeology/node-scanner.ftl @@ -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