xenoarch: node scanner UI overhaul (#4814)

* xenoarch: node scanner UI overhaul

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Update Content.Shared/Xenoarchaeology/Artifact/SharedXenoArtifactSystem.Unlock.cs

Co-authored-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>
Signed-off-by: Charlie Morley <cmorley191@gmail.com>

* Update Content.Shared/Xenoarchaeology/Artifact/SharedXenoArtifactSystem.Unlock.cs

Co-authored-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>
Signed-off-by: Charlie Morley <cmorley191@gmail.com>

---------

Signed-off-by: Charlie Morley <cmorley191@gmail.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>
This commit is contained in:
Charlie Morley 2025-12-08 06:48:49 -08:00 committed by GitHub
parent e7afcda1a8
commit c9ea4aeb77
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 465 additions and 184 deletions

View File

@ -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;
/// </summary>
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
/// <inheritdoc />
protected override void Open()
{
base.Open();
_scannerDisplay = this.CreateWindow<NodeScannerDisplay>();
// DeltaV - start of node scanner overhaul
_scannerDisplay = this.CreateWindow<DVNodeScannerDisplay>();
// DeltaV - end of node scanner overhaul
_scannerDisplay.SetOwner(Owner);
}

View File

@ -1,20 +0,0 @@
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
Title="{Loc 'node-scan-display-title'}"
MinSize="305 180"
SetSize="305 180"
Resizable="False"
>
<BoxContainer Orientation="Vertical" >
<controls:StripeBack>
<Label Name="NodeScannerState" HorizontalAlignment="Center" StyleClasses="LabelSubText" Margin="4 0 0 4" />
</controls:StripeBack>
<BoxContainer Orientation="Horizontal">
<Label Name="NoActiveNodeDataLabel" Text="{Loc 'node-scan-no-data'}" Margin="45 25 0 0" MinHeight="47" />
<GridContainer Name="ActiveNodesList" Columns="4" Rows="2" Visible="True" MinHeight="72" />
</BoxContainer>
<controls:StripeBack>
<Label Name="ArtifactStateLabel" HorizontalAlignment="Center" StyleClasses="LabelSubText" Margin="4 0 0 4" />
</controls:StripeBack>
</BoxContainer>
</controls:FancyWindow>

View File

@ -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<string> _triggeredNodeNames = new();
public NodeScannerDisplay()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_artifact = _ent.System<SharedXenoArtifactSystem>();
}
/// <summary>
/// Sets entity that represents hand-held xeno artifact node scanner for which window is opened.
/// Closes window if <see cref="NodeScannerComponent"/> is not present on entity.
/// </summary>
public void SetOwner(EntityUid scannerEntityUid)
{
if (!_ent.TryGetComponent<NodeScannerComponent>(scannerEntityUid, out var scannerComponent))
{
Close();
return;
}
_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>
private void Update(bool isConnected, ArtifactState artifactState, HashSet<string>? 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"),
};
}
}

View File

@ -0,0 +1,39 @@
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
Title="{Loc 'node-scan-display-title'}"
MinSize="350 400"
SetSize="350 400"
Resizable="False"
>
<BoxContainer Orientation="Vertical" >
<controls:StripeBack>
<Label Name="NodeScannerState" HorizontalAlignment="Center" StyleClasses="LabelSubText" Margin="4 0 0 4" />
</controls:StripeBack>
<BoxContainer Name="ProgressBarContainer" Orientation="Horizontal" Margin="0 5 0 0" HorizontalExpand="True" Visible="False" >
<Label Name="ProgressBarLabel" Text="{Loc 'node-scan-timer'}" Margin="20 0 0 0" />
<ProgressBar
Name="ProgressBar"
MinValue="0"
MaxValue="1"
SetHeight="20"
Margin="10 0 20 0"
HorizontalExpand="True" >
</ProgressBar>
</BoxContainer>
<BoxContainer Orientation="Horizontal" VerticalExpand="True" >
<Label Name="NoActiveNodeDataLabel" Text="{Loc 'node-scan-no-data'}" Margin="15 15 0 0" MinHeight="47" HorizontalExpand="True" HorizontalAlignment="Center" />
<BoxContainer Name="ActiveNodesList" Orientation="Vertical" HorizontalExpand="True" Visible="False" MinHeight="72" />
</BoxContainer>
<PanelContainer Name="SystemWarningPanel"
HorizontalAlignment="Center"
VerticalAlignment="Bottom"
HorizontalExpand="True"
Margin="10 0 10 5"
Visible="False" >
<BoxContainer Orientation="Vertical" HorizontalExpand="True" >
<RichTextLabel Name="SystemWarningLabel" Margin="5 8 5 0" HorizontalAlignment="Center" />
<RichTextLabel Name="SystemWarningSublabel" Margin="5 3 5 8" HorizontalAlignment="Center" />
</BoxContainer>
</PanelContainer>
</BoxContainer>
</controls:FancyWindow>

View File

@ -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;
/// <summary>
/// DeltaV replacement for NodeScannerDisplay, the UI of the artifact node scanner.
/// Displays extra information about the artifact unlocking phase.
/// </summary>
[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<SharedXenoArtifactSystem>();
_spriteSystem = _ent.System<SpriteSystem>();
}
/// <summary>
/// Sets entity that represents hand-held xeno artifact node scanner for which window is opened.
/// Closes window if <see cref="NodeScannerComponent"/> is not present on entity.
/// </summary>
public void SetOwner(EntityUid scannerEntityUid)
{
if (!_ent.TryGetComponent<NodeScannerComponent>(scannerEntityUid, out var scannerComponent))
{
Close();
return;
}
_owner = scannerEntityUid;
_blipTexture = _spriteSystem.Frame0(new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_circle.png")));
}
/// <inheritdoc />
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<int> triggeredIndexesOrdered, HashSet<int> 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<int> triggeredIndexesOrdered, HashSet<int> 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<NameIdentifierComponent>(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));
}
}
}
}

View File

@ -91,6 +91,31 @@ public sealed partial class XenoArtifactComponent : Component
/// </summary>
[DataField, AutoPausedField]
public TimeSpan NextUnlockTime;
/// <summary>
/// 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.
/// </summary>
[DataField, AutoNetworkedField]
public TimeSpan? LastUnlockingEndTime = null;
/// <summary>
/// DeltaV - The set of related nodes that were triggered during the last completed XenoArtifactUnlockingComponent phase.
/// </summary>
[DataField, AutoNetworkedField]
public HashSet<int> LastUnlockingTriggeredNodeIndexesRelated = new();
/// <summary>
/// DeltaV - The list of nodes that were triggered during the last completed XenoArtifactUnlockingComponent phase.
/// </summary>
[DataField, AutoNetworkedField]
public List<int> LastUnlockingTriggeredNodeIndexesOrdered = new();
/// <summary>
/// DeltaV - Whether the last completed unlocking phase resulted in a newly-unlocked node.
/// </summary>
[DataField, AutoNetworkedField]
public bool LastUnlockingSuccessful = false;
#endregion
// NOTE: you should not be accessing any of these values directly. Use the methods in SharedXenoArtifactSystem.Graph

View File

@ -15,6 +15,21 @@ public sealed partial class XenoArtifactUnlockingComponent : Component
[DataField, AutoNetworkedField]
public HashSet<int> TriggeredNodeIndexes = new();
/// <summary>
/// DeltaV - Ordered list of triggered nodes.
/// </summary>
[DataField, AutoNetworkedField]
public List<int> TriggeredNodeIndexesOrdered = new();
/// <summary>
/// DeltaV - A subset of TriggeredNodeIndexes whose elements are "related" to eachother.
/// </summary>
/// <remarks>
/// See definition of relatedness in GetRelatedNodes() of SharedXenoArtifactSystem.DV.cs
/// </remarks
[DataField, AutoNetworkedField]
public HashSet<int> TriggeredNodeIndexesRelated = new();
/// <summary>
/// The time at which the unlocking state ends.
/// </summary>

View File

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

View File

@ -63,6 +63,15 @@ public abstract partial class SharedXenoArtifactSystem
if (!force && _timing.CurTime < ent.Comp.NextUnlockTime)
return;
// DeltaV - start of node scanner overhaul
(Entity<XenoArtifactNodeComponent> 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<XenoArtifactUnlockingComponent>(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);
}

View File

@ -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
{
/// <summary>
/// Returns the set of nodes that are "related" to the node with the passed node index.
/// </summary>
/// <remarks>
/// 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)
/// </remarks>
public HashSet<int> GetRelatedNodes(Entity<XenoArtifactComponent?> ent, int nodeIdx)
{
if (!Resolve(ent, ref ent.Comp))
return new();
var related = GetRelatedNodes(ent, GetNode((ent, ent.Comp), nodeIdx));
var output = new HashSet<int>();
foreach (var r in related)
{
output.Add(GetIndex((ent, ent.Comp), r));
}
return output;
}
/// <summary>
/// Returns set of node entities, that are "related" to passed node entity.
/// </summary>
/// <remarks>
/// 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)
/// </remarks>
public HashSet<Entity<XenoArtifactNodeComponent>> GetRelatedNodes(Entity<XenoArtifactComponent?> ent, Entity<XenoArtifactNodeComponent> node)
{
if (!Resolve(ent, ref ent.Comp))
return new();
var potentialUnlockTargetNodes = GetSuccessorNodes(ent, node);
potentialUnlockTargetNodes.Add(node);
var output = new HashSet<Entity<XenoArtifactNodeComponent>>();
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;
}
}

View File

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