diff --git a/Content.Client/Xenoarchaeology/Ui/NodeScannerBoundUserInterface.cs b/Content.Client/Xenoarchaeology/Ui/NodeScannerBoundUserInterface.cs
index a30bde47d7..b3c17f5149 100644
--- a/Content.Client/Xenoarchaeology/Ui/NodeScannerBoundUserInterface.cs
+++ b/Content.Client/Xenoarchaeology/Ui/NodeScannerBoundUserInterface.cs
@@ -1,3 +1,4 @@
+using Content.Client._DV.Xenoarchaeology.Ui;
using Robust.Client.UserInterface;
namespace Content.Client.Xenoarchaeology.Ui;
@@ -7,15 +8,19 @@ namespace Content.Client.Xenoarchaeology.Ui;
///
public sealed class NodeScannerBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey)
{
+ // DeltaV - start of node scanner overhaul
[ViewVariables]
- private NodeScannerDisplay? _scannerDisplay;
+ private DVNodeScannerDisplay? _scannerDisplay;
+ // DeltaV - end of node scanner overhaul
///
protected override void Open()
{
base.Open();
- _scannerDisplay = this.CreateWindow();
+ // DeltaV - start of node scanner overhaul
+ _scannerDisplay = this.CreateWindow();
+ // DeltaV - end of node scanner overhaul
_scannerDisplay.SetOwner(Owner);
}
diff --git a/Content.Client/Xenoarchaeology/Ui/NodeScannerDisplay.xaml b/Content.Client/Xenoarchaeology/Ui/NodeScannerDisplay.xaml
deleted file mode 100644
index c34dbab475..0000000000
--- a/Content.Client/Xenoarchaeology/Ui/NodeScannerDisplay.xaml
+++ /dev/null
@@ -1,20 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Content.Client/Xenoarchaeology/Ui/NodeScannerDisplay.xaml.cs b/Content.Client/Xenoarchaeology/Ui/NodeScannerDisplay.xaml.cs
deleted file mode 100644
index 372471ab2e..0000000000
--- a/Content.Client/Xenoarchaeology/Ui/NodeScannerDisplay.xaml.cs
+++ /dev/null
@@ -1,150 +0,0 @@
-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;
-
-[GenerateTypedNameReferences]
-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();
- }
-
- ///
- /// Sets entity that represents hand-held xeno artifact node scanner for which window is opened.
- /// Closes window if is not present on entity.
- ///
- public void SetOwner(EntityUid scannerEntityUid)
- {
- if (!_ent.TryGetComponent(scannerEntityUid, out var scannerComponent))
- {
- Close();
- return;
- }
-
- _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.
- ///
- private void Update(bool isConnected, ArtifactState artifactState, HashSet? triggeredNodeNames = null)
- {
- ArtifactStateLabel.Text = GetStateText(artifactState);
- NodeScannerState.Text = isConnected
- ? Loc.GetString("node-scanner-artifact-connected")
- : Loc.GetString("node-scanner-artifact-non-connected");
-
- ActiveNodesList.Children.Clear();
-
- 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 triggeredNodeNames)
- {
- var nodeLabel = new Button
- {
- Text = nodeId,
- Margin = new Thickness(15, 5, 0, 0),
- MaxHeight = 40,
- Disabled = true
- };
- ActiveNodesList.Children.Add(nodeLabel);
- }
- }
- else
- {
- // clear list of activated nodes (done previously), show 'no data' placeholder
- NoActiveNodeDataLabel.Visible = true;
- ActiveNodesList.Visible = false;
- }
- }
-
- private string GetStateText(ArtifactState state)
- {
- return state switch
- {
- ArtifactState.None => "\u2800", // placeholder for line to not be squeezed
- ArtifactState.Ready => Loc.GetString("node-scanner-artifact-state-ready"),
- ArtifactState.Unlocking => Loc.GetString("node-scanner-artifact-state-unlocking"),
- ArtifactState.Cooldown => Loc.GetString("node-scanner-artifact-state-cooldown"),
- _ => throw new ArgumentException("Invalid state"),
- };
- }
-}
diff --git a/Content.Client/_DV/Xenoarchaeology/Ui/DVNodeScannerDisplay.xaml b/Content.Client/_DV/Xenoarchaeology/Ui/DVNodeScannerDisplay.xaml
new file mode 100644
index 0000000000..315d801c9d
--- /dev/null
+++ b/Content.Client/_DV/Xenoarchaeology/Ui/DVNodeScannerDisplay.xaml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/_DV/Xenoarchaeology/Ui/DVNodeScannerDisplay.xaml.cs b/Content.Client/_DV/Xenoarchaeology/Ui/DVNodeScannerDisplay.xaml.cs
new file mode 100644
index 0000000000..7e3e7a0449
--- /dev/null
+++ b/Content.Client/_DV/Xenoarchaeology/Ui/DVNodeScannerDisplay.xaml.cs
@@ -0,0 +1,250 @@
+using System.Linq;
+using System.Numerics;
+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.GameObjects;
+using Robust.Client.Graphics;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Timing;
+using Robust.Shared.Utility;
+
+namespace Content.Client._DV.Xenoarchaeology.Ui;
+
+///
+/// DeltaV replacement for NodeScannerDisplay, the UI of the artifact node scanner.
+/// Displays extra information about the artifact unlocking phase.
+///
+[GenerateTypedNameReferences]
+public sealed partial class DVNodeScannerDisplay : FancyWindow
+{
+ [Dependency] private readonly IEntityManager _ent = default!;
+ [Dependency] private readonly IGameTiming _timing= default!;
+
+ private readonly SharedXenoArtifactSystem _artifact;
+ private readonly SpriteSystem _spriteSystem;
+
+ private Texture? _blipTexture;
+ private EntityUid _owner;
+
+ private float animatedTimeRemaining = 0;
+ private float animatedTimeRemainingVelocity = 0;
+
+ public DVNodeScannerDisplay()
+ {
+ RobustXamlLoader.Load(this);
+
+ IoCManager.InjectDependencies(this);
+
+ _artifact = _ent.System();
+ _spriteSystem = _ent.System();
+ }
+
+ ///
+ /// Sets entity that represents hand-held xeno artifact node scanner for which window is opened.
+ /// Closes window if is not present on entity.
+ ///
+ public void SetOwner(EntityUid scannerEntityUid)
+ {
+ if (!_ent.TryGetComponent(scannerEntityUid, out var scannerComponent))
+ {
+ Close();
+ return;
+ }
+
+ _owner = scannerEntityUid;
+
+ _blipTexture = _spriteSystem.Frame0(new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_circle.png")));
+ }
+
+ ///
+ protected override void FrameUpdate(FrameEventArgs args)
+ {
+ base.FrameUpdate(args);
+
+
+ // reset
+ NoActiveNodeDataLabel.Visible = true;
+ NodeScannerState.Text = Loc.GetString("node-scanner-artifact-non-connected");
+ ProgressBarContainer.Visible = false;
+ ActiveNodesList.Visible = false;
+ SystemWarningPanel.Visible = false;
+
+
+ if (!_ent.TryGetComponent(_owner, out NodeScannerConnectedComponent? connectedScanner))
+ return;
+
+ var attachedArtifactEnt = connectedScanner.AttachedTo;
+ if (!_ent.TryGetComponent(attachedArtifactEnt, out XenoArtifactComponent? artifactComponent))
+ return;
+
+ _ent.TryGetComponent(attachedArtifactEnt, out XenoArtifactUnlockingComponent? unlockingComponent);
+
+
+ // header banner
+ NodeScannerState.Text =
+ unlockingComponent != null
+ ? Loc.GetString("node-scanner-artifact-state-unlocking")
+ : _timing.CurTime < artifactComponent.NextUnlockTime
+ ? Loc.GetString("node-scanner-artifact-state-cooldown")
+ : Loc.GetString("node-scanner-artifact-state-ready");
+
+
+ // time remaining progress bar
+ var unlockTimeRemaining =
+ unlockingComponent != null
+ ? MathF.Max(0f, (float)(unlockingComponent.EndTime - _timing.CurTime).TotalSeconds)
+ : 0f;
+
+ // damped-oscillator-style system for animating sudden jumps in time remaining
+ animatedTimeRemainingVelocity += 0.001f * (unlockTimeRemaining - animatedTimeRemaining) - 0.1f * animatedTimeRemainingVelocity;
+ animatedTimeRemaining = MathF.Max(0f, animatedTimeRemaining + animatedTimeRemainingVelocity);
+
+ // sigmoid-style function to make sure progress bar never goes over 100%
+ ProgressBar.Value = 2f / (1f + MathF.Exp(-0.08f * animatedTimeRemaining)) - 1f;
+
+
+ // node data and warning panel
+ (List triggeredIndexesOrdered, HashSet triggeredIndexesRelated)? unlockingDataToShow = null;
+ if (unlockingComponent != null)
+ {
+ unlockingDataToShow = (
+ unlockingComponent.TriggeredNodeIndexesOrdered,
+ unlockingComponent.TriggeredNodeIndexesRelated
+ );
+ }
+ else if (
+ artifactComponent.LastUnlockingEndTime != null
+ && _timing.CurTime - artifactComponent.LastUnlockingEndTime < TimeSpan.FromSeconds(60)
+ )
+ {
+ unlockingDataToShow = (
+ artifactComponent.LastUnlockingTriggeredNodeIndexesOrdered,
+ artifactComponent.LastUnlockingTriggeredNodeIndexesRelated
+ );
+ }
+
+ if (
+ unlockingDataToShow is (List triggeredIndexesOrdered, HashSet triggeredIndexesRelated)
+ && triggeredIndexesOrdered.Count > 0
+ )
+ {
+ // show triggered node data instead of 'no data' placeholder
+ NoActiveNodeDataLabel.Visible = false;
+ ProgressBarContainer.Visible = true;
+ ActiveNodesList.Visible = true;
+
+
+ // node list
+ ActiveNodesList.Children.Clear();
+ foreach (var triggeredIndex in triggeredIndexesOrdered)
+ {
+ var node = _artifact.GetNode((attachedArtifactEnt, artifactComponent), triggeredIndex);
+
+ var nodeControl = new Button
+ {
+ Margin = new Thickness(15, 5, 15, 0),
+ MaxHeight = 40,
+ Disabled = true,
+ HorizontalExpand = true
+ };
+ ActiveNodesList.AddChild(nodeControl);
+
+ var mainContainer = new BoxContainer()
+ {
+ Orientation = BoxContainer.LayoutOrientation.Horizontal,
+ HorizontalExpand = true,
+ };
+ nodeControl.AddChild(mainContainer);
+
+ // mimics suitCoordsIndicator in CrewMonitoringWindow
+ var relatednessIndicator = new TextureRect()
+ {
+ Texture = _blipTexture,
+ TextureScale = new Vector2(0.25f, 0.25f),
+ Modulate = triggeredIndexesRelated.Contains(triggeredIndex) ? Color.LimeGreen : Color.DarkRed,
+ HorizontalAlignment = HAlignment.Center,
+ VerticalAlignment = VAlignment.Center,
+ };
+ mainContainer.AddChild(relatednessIndicator);
+
+ var triggerTip = Loc.GetString(node.Comp.TriggerTip!);
+ var triggeredNodeName = (_ent.GetComponentOrNull(node)?.Identifier ?? 0).ToString("D3");
+ var descriptionLabel = new Label()
+ {
+ Text = $"{triggerTip} ({triggeredNodeName})",
+ HorizontalExpand = true,
+ ClipText = true,
+ Margin = new Thickness(8, 0, 0, 0),
+ };
+ mainContainer.AddChild(descriptionLabel);
+ }
+
+
+ // warning panel (mimics that of PowerMonitoringWindow)
+ (Color background, Color border, string message, string subtext)? warningDataToShow = null;
+ if (unlockingComponent == null)
+ {
+ if (artifactComponent.LastUnlockingSuccessful)
+ {
+ warningDataToShow = (
+ background: Color.Green,
+ border: Color.DarkGreen,
+ message: Loc.GetString("node-scan-warning-unlock-complete"),
+ subtext: Loc.GetString("node-scan-warning-unlock-complete-subtext")
+ );
+ }
+ else if (triggeredIndexesOrdered.Count == triggeredIndexesRelated.Count)
+ {
+ warningDataToShow = (
+ background: Color.Orange,
+ border: Color.DarkOrange,
+ message: Loc.GetString("node-scan-warning-partial-complete"),
+ subtext: Loc.GetString("node-scan-warning-partial-complete-subtext")
+ );
+ }
+ else
+ {
+ warningDataToShow = (
+ background: Color.Red,
+ border: Color.DarkRed,
+ message: Loc.GetString("node-scan-warning-failure-complete"),
+ subtext: Loc.GetString("node-scan-warning-failure-complete-subtext")
+ );
+ }
+ }
+ else if (triggeredIndexesOrdered.Count != triggeredIndexesRelated.Count)
+ {
+ warningDataToShow = (
+ background: Color.Red,
+ border: Color.DarkRed,
+ message: Loc.GetString("node-scan-warning-failure-imminent"),
+ subtext: Loc.GetString("node-scan-warning-failure-imminent-subtext")
+ );
+ }
+
+ if (warningDataToShow is (Color background, Color border, string message, string subtext))
+ {
+ SystemWarningPanel.Visible = true;
+
+ SystemWarningPanel.PanelOverride = new StyleBoxFlat
+ {
+ BackgroundColor = background,
+ BorderColor = border,
+ BorderThickness = new Thickness(2),
+ };
+
+ // warning panel pulses during unlocking phase
+ var lit = unlockingComponent != null && _timing.RealTime.TotalSeconds % 1f > 0.5f;
+ SystemWarningPanel.Modulate = lit ? Color.White : new Color(178, 178, 178);
+
+ SystemWarningLabel.SetMessage(FormattedMessage.FromMarkupOrThrow(message));
+ SystemWarningSublabel.SetMessage(FormattedMessage.FromMarkupOrThrow(subtext));
+ }
+ }
+ }
+}
diff --git a/Content.Shared/Xenoarchaeology/Artifact/Components/XenoArtifactComponent.cs b/Content.Shared/Xenoarchaeology/Artifact/Components/XenoArtifactComponent.cs
index 2cdaed870a..40511abd66 100644
--- a/Content.Shared/Xenoarchaeology/Artifact/Components/XenoArtifactComponent.cs
+++ b/Content.Shared/Xenoarchaeology/Artifact/Components/XenoArtifactComponent.cs
@@ -91,6 +91,31 @@ public sealed partial class XenoArtifactComponent : Component
///
[DataField, AutoPausedField]
public TimeSpan NextUnlockTime;
+
+ ///
+ /// DeltaV - The end time of the last-completed XenoArtifactUnlockingComponent phase.
+ /// If null, no unlocking phase has occurred yet, and the other LastUnlocking... fields are also invalid.
+ ///
+ [DataField, AutoNetworkedField]
+ public TimeSpan? LastUnlockingEndTime = null;
+
+ ///
+ /// DeltaV - The set of related nodes that were triggered during the last completed XenoArtifactUnlockingComponent phase.
+ ///
+ [DataField, AutoNetworkedField]
+ public HashSet LastUnlockingTriggeredNodeIndexesRelated = new();
+
+ ///
+ /// DeltaV - The list of nodes that were triggered during the last completed XenoArtifactUnlockingComponent phase.
+ ///
+ [DataField, AutoNetworkedField]
+ public List LastUnlockingTriggeredNodeIndexesOrdered = new();
+
+ ///
+ /// DeltaV - Whether the last completed unlocking phase resulted in a newly-unlocked node.
+ ///
+ [DataField, AutoNetworkedField]
+ public bool LastUnlockingSuccessful = false;
#endregion
// NOTE: you should not be accessing any of these values directly. Use the methods in SharedXenoArtifactSystem.Graph
diff --git a/Content.Shared/Xenoarchaeology/Artifact/Components/XenoArtifactUnlockingComponent.cs b/Content.Shared/Xenoarchaeology/Artifact/Components/XenoArtifactUnlockingComponent.cs
index 6b2351cc2f..1fef43fca5 100644
--- a/Content.Shared/Xenoarchaeology/Artifact/Components/XenoArtifactUnlockingComponent.cs
+++ b/Content.Shared/Xenoarchaeology/Artifact/Components/XenoArtifactUnlockingComponent.cs
@@ -15,6 +15,21 @@ public sealed partial class XenoArtifactUnlockingComponent : Component
[DataField, AutoNetworkedField]
public HashSet TriggeredNodeIndexes = new();
+ ///
+ /// DeltaV - Ordered list of triggered nodes.
+ ///
+ [DataField, AutoNetworkedField]
+ public List TriggeredNodeIndexesOrdered = new();
+
+ ///
+ /// DeltaV - A subset of TriggeredNodeIndexes whose elements are "related" to eachother.
+ ///
+ ///
+ /// See definition of relatedness in GetRelatedNodes() of SharedXenoArtifactSystem.DV.cs
+ /// TriggeredNodeIndexesRelated = new();
+
///
/// The time at which the unlocking state ends.
///
diff --git a/Content.Shared/Xenoarchaeology/Artifact/SharedXenoArtifactSystem.Unlock.cs b/Content.Shared/Xenoarchaeology/Artifact/SharedXenoArtifactSystem.Unlock.cs
index 57d6502bfb..d9c7508fc8 100644
--- a/Content.Shared/Xenoarchaeology/Artifact/SharedXenoArtifactSystem.Unlock.cs
+++ b/Content.Shared/Xenoarchaeology/Artifact/SharedXenoArtifactSystem.Unlock.cs
@@ -82,11 +82,13 @@ public abstract partial class SharedXenoArtifactSystem
// var activated = ActivateNode((ent, artifactComponent), node.Value, null, null, Transform(ent).Coordinates, false);
// if (activated)
soundEffect = unlockingComponent.UnlockActivationSuccessfulSound;
+ artifactComponent.LastUnlockingSuccessful = true; // DeltaV - node scanner overhaul
}
else
{
unlockAttemptResultMsg = "artifact-unlock-state-end-failure";
soundEffect = unlockingComponent.UnlockActivationFailedSound;
+ artifactComponent.LastUnlockingSuccessful = false; // DeltaV - node scanner overhaul
}
if (_net.IsServer)
@@ -95,6 +97,21 @@ public abstract partial class SharedXenoArtifactSystem
_audio.PlayPvs(soundEffect, ent.Owner);
}
+ // DeltaV - start of node scanner overhaul
+ artifactComponent.LastUnlockingTriggeredNodeIndexesOrdered.Clear();
+ artifactComponent.LastUnlockingTriggeredNodeIndexesRelated.Clear();
+
+ artifactComponent.LastUnlockingTriggeredNodeIndexesOrdered
+ .AddRange(unlockingComponent.TriggeredNodeIndexesOrdered);
+ // one day we'll get a HashSet.AddRange
+ foreach (var i in unlockingComponent.TriggeredNodeIndexesRelated)
+ artifactComponent.LastUnlockingTriggeredNodeIndexesRelated.Add(i);
+
+ artifactComponent.LastUnlockingEndTime = unlockingComponent.EndTime;
+
+ Dirty(ent, artifactComponent);
+ // DeltaV - end of node scanner overhaul
+
RemComp(ent, unlockingComponent);
RaiseUnlockingFinished(ent, node);
artifactComponent.NextUnlockTime = _timing.CurTime + artifactComponent.UnlockStateRefractory;
diff --git a/Content.Shared/Xenoarchaeology/Artifact/SharedXenoArtifactSystem.XAT.cs b/Content.Shared/Xenoarchaeology/Artifact/SharedXenoArtifactSystem.XAT.cs
index f573ffd763..0efb267c30 100644
--- a/Content.Shared/Xenoarchaeology/Artifact/SharedXenoArtifactSystem.XAT.cs
+++ b/Content.Shared/Xenoarchaeology/Artifact/SharedXenoArtifactSystem.XAT.cs
@@ -63,6 +63,15 @@ public abstract partial class SharedXenoArtifactSystem
if (!force && _timing.CurTime < ent.Comp.NextUnlockTime)
return;
+ // DeltaV - start of node scanner overhaul
+ (Entity node, int index)? parsedNode =
+ (node == null)
+ ? null
+ : (node.Value, GetIndex(ent, node.Value));
+
+ bool partOfRelatedTriggersSet = true;
+ // DeltaV - end of node scanner overhaul
+
if (!_unlockingQuery.TryGetComponent(ent, out var unlockingComp))
{
unlockingComp = EnsureComp(ent);
@@ -73,24 +82,37 @@ public abstract partial class SharedXenoArtifactSystem
_popup.PopupEntity(Loc.GetString("artifact-unlock-state-begin"), ent);
Dirty(ent);
}
- else if (node != null)
+ else if (parsedNode != null)
{
- var index = GetIndex(ent, node.Value);
+ // DeltaV - start of node scanner overhaul
- var predecessorNodeIndices = GetPredecessorNodes((ent, ent), index);
- var successorNodeIndices = GetSuccessorNodes((ent, ent), index);
- if (unlockingComp.TriggeredNodeIndexes.Count == 0
- || unlockingComp.TriggeredNodeIndexes.All(
- x => predecessorNodeIndices.Contains(x) || successorNodeIndices.Contains(x)
- )
- )
+ var relatedNodeIndices = GetRelatedNodes((ent, ent), parsedNode.Value.index);
+ partOfRelatedTriggersSet = unlockingComp.TriggeredNodeIndexesRelated.All(x => relatedNodeIndices.Contains(x));
+
+ // Checking for trigger "relatedness" is a much more accurate measurement
+ // of "is this locking phase going to fail" than upstream's predecessor/successor check.
+ // Upstream's version of this check had edge-cases where time would not add, even though the
+ // unlocking phase ends up succeeding.
+ // See definition of GetRelatedNodes() for details on the concept of "relatedness".
+ if (
+ unlockingComp.TriggeredNodeIndexes.Count == unlockingComp.TriggeredNodeIndexesRelated.Count
+ && partOfRelatedTriggersSet
+ )
// we add time on each new trigger, if it is not going to fail us
unlockingComp.EndTime += ent.Comp.UnlockStateIncrementPerNode;
+
+ // DeltaV - end of node scanner overhaul
}
- if (node != null && unlockingComp.TriggeredNodeIndexes.Add(GetIndex(ent, node.Value)))
+ if (parsedNode != null && unlockingComp.TriggeredNodeIndexes.Add(parsedNode.Value.index))
{
- // DeltaV - start of faster unlock effect
+ // DeltaV - start of changes
+ // node scanner overhaul:
+ unlockingComp.TriggeredNodeIndexesOrdered.Add(parsedNode.Value.index);
+ if (partOfRelatedTriggersSet)
+ unlockingComp.TriggeredNodeIndexesRelated.Add(parsedNode.Value.index);
+
+ // faster unlock effect:
if (
ent.Comp.UnlockCompleteDuration is {} completeDuration
&& TryGetNodeFromUnlockState((ent.Owner, unlockingComp, ent.Comp), out var unlockingNode)
@@ -98,7 +120,7 @@ public abstract partial class SharedXenoArtifactSystem
{
unlockingComp.EndTime = _timing.CurTime + completeDuration;
}
- // DeltaV - end of faster unlock effect
+ // DeltaV - end of changes
Dirty(ent, unlockingComp);
}
diff --git a/Content.Shared/_DV/Xenoarchaeology/Artifact/SharedXenoArtifactSystem.DV.cs b/Content.Shared/_DV/Xenoarchaeology/Artifact/SharedXenoArtifactSystem.DV.cs
new file mode 100644
index 0000000000..4795b847ef
--- /dev/null
+++ b/Content.Shared/_DV/Xenoarchaeology/Artifact/SharedXenoArtifactSystem.DV.cs
@@ -0,0 +1,68 @@
+using System.Linq;
+using Content.Shared.Xenoarchaeology.Artifact.Components;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Utility;
+
+namespace Content.Shared.Xenoarchaeology.Artifact;
+
+public abstract partial class SharedXenoArtifactSystem
+{
+
+ ///
+ /// Returns the set of nodes that are "related" to the node with the passed node index.
+ ///
+ ///
+ /// Related nodes are those that might be triggered together with this node, in order to unlock some node.
+ /// Triggering nodes in unrelated parts of the graph (e.g. different segments, or diverging branches)
+ /// causes the unlocking phase to fail. (see TryGetNodeFromUnlockState)
+ ///
+ public HashSet GetRelatedNodes(Entity ent, int nodeIdx)
+ {
+ if (!Resolve(ent, ref ent.Comp))
+ return new();
+
+ var related = GetRelatedNodes(ent, GetNode((ent, ent.Comp), nodeIdx));
+ var output = new HashSet();
+ foreach (var r in related)
+ {
+ output.Add(GetIndex((ent, ent.Comp), r));
+ }
+
+ return output;
+ }
+
+ ///
+ /// Returns set of node entities, that are "related" to passed node entity.
+ ///
+ ///
+ /// Related nodes are those that might be triggered together with this node, in order to unlock some node.
+ /// Triggering nodes in unrelated parts of the graph (e.g. different segments, or diverging branches)
+ /// causes the unlocking phase to fail. (see TryGetNodeFromUnlockState)
+ ///
+ public HashSet> GetRelatedNodes(Entity ent, Entity node)
+ {
+ if (!Resolve(ent, ref ent.Comp))
+ return new();
+
+ var potentialUnlockTargetNodes = GetSuccessorNodes(ent, node);
+ potentialUnlockTargetNodes.Add(node);
+
+ var output = new HashSet>();
+ foreach (var t in potentialUnlockTargetNodes)
+ {
+ var tPredecessors = GetPredecessorNodes(ent, t);
+ // nodes can only be unlocked if all their predecessors are unlocked
+ if (tPredecessors.All(p => !p.Comp.Locked))
+ {
+ output.Add(t);
+ foreach (var p in tPredecessors)
+ {
+ output.Add(p);
+ }
+ }
+ }
+
+ return output;
+ }
+
+}
diff --git a/Resources/Locale/en-US/_DV/xenoarchaeology/node-scanner.ftl b/Resources/Locale/en-US/_DV/xenoarchaeology/node-scanner.ftl
new file mode 100644
index 0000000000..075caff816
--- /dev/null
+++ b/Resources/Locale/en-US/_DV/xenoarchaeology/node-scanner.ftl
@@ -0,0 +1,10 @@
+node-scan-timer = Signal stability:
+
+node-scan-warning-unlock-complete = [color=white][font size=13][bold]NEW HARMONICS DETECTED[/bold][/font][/color]
+node-scan-warning-unlock-complete-subtext = [color=white][font size=11][/font]See analysis console[/color]
+node-scan-warning-partial-complete = [color=white][font size=13][bold]SIGNAL LOST[/bold][/font][/color]
+node-scan-warning-partial-complete-subtext = [color=white][font size=11]Cause: incomplete trigger framework[/font][/color]
+node-scan-warning-failure-imminent = [color=white][font size=13][bold]DIVERGING NODES DETECTED[/bold][/font][/color]
+node-scan-warning-failure-imminent-subtext = [color=white][font size=11]Signal collapse imminent[/font][/color]
+node-scan-warning-failure-complete = [color=white][font size=13][bold]SIGNAL LOST[/bold][/font][/color]
+node-scan-warning-failure-complete-subtext = [color=white][font size=11]Cause: dissonant harmonics[/font][/color]