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 <pa.pecherskij@interfax.ru>
This commit is contained in:
Fildrance
2025-04-27 18:11:13 +03:00
committed by GitHub
parent b17e558046
commit 4690e62575
8 changed files with 207 additions and 202 deletions

View File

@@ -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);
}
}

View 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 />

View 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"),

View File

@@ -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
}
}

View 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]
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>
/// Xeno artifact entity, to which scanner is attached currently.
/// Upon detaching this component should be removed.
/// </summary>
[DataField, AutoNetworkedField]
public ArtifactState ArtifactState;
public EntityUid AttachedTo;
/// <summary>
/// Time until next unlocking of scanned artifact can be started.
/// Next update tick gametime.
/// </summary>
[DataField, AutoNetworkedField]
public TimeSpan? WaitTime;
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
[AutoPausedField]
public TimeSpan NextUpdate = TimeSpan.Zero;
/// <summary>
/// Moment of gametime, at which last artifact scanning was done.
/// Update interval for link info.
/// </summary>
[DataField, AutoNetworkedField]
public TimeSpan? ScannedAt;
[DataField]
public TimeSpan LinkUpdateInterval = TimeSpan.FromSeconds(1);
}
/// <summary>

View File

@@ -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);
}
}

View File

@@ -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);
}

View 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