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:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 />
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
100
Content.Shared/Xenoarchaeology/Equipment/NodeScannerSystem.cs
Normal file
100
Content.Shared/Xenoarchaeology/Equipment/NodeScannerSystem.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user