From c9ea4aeb77a8906e90ed115b6095bb35cdf25755 Mon Sep 17 00:00:00 2001 From: Charlie Morley Date: Mon, 8 Dec 2025 06:48:49 -0800 Subject: [PATCH] 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 * Update Content.Shared/Xenoarchaeology/Artifact/SharedXenoArtifactSystem.Unlock.cs Co-authored-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com> Signed-off-by: Charlie Morley --------- Signed-off-by: Charlie Morley 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> --- .../Ui/NodeScannerBoundUserInterface.cs | 9 +- .../Ui/NodeScannerDisplay.xaml | 20 -- .../Ui/NodeScannerDisplay.xaml.cs | 150 ----------- .../Ui/DVNodeScannerDisplay.xaml | 39 +++ .../Ui/DVNodeScannerDisplay.xaml.cs | 250 ++++++++++++++++++ .../Components/XenoArtifactComponent.cs | 25 ++ .../XenoArtifactUnlockingComponent.cs | 15 ++ .../SharedXenoArtifactSystem.Unlock.cs | 17 ++ .../Artifact/SharedXenoArtifactSystem.XAT.cs | 46 +++- .../Artifact/SharedXenoArtifactSystem.DV.cs | 68 +++++ .../_DV/xenoarchaeology/node-scanner.ftl | 10 + 11 files changed, 465 insertions(+), 184 deletions(-) delete mode 100644 Content.Client/Xenoarchaeology/Ui/NodeScannerDisplay.xaml delete mode 100644 Content.Client/Xenoarchaeology/Ui/NodeScannerDisplay.xaml.cs create mode 100644 Content.Client/_DV/Xenoarchaeology/Ui/DVNodeScannerDisplay.xaml create mode 100644 Content.Client/_DV/Xenoarchaeology/Ui/DVNodeScannerDisplay.xaml.cs create mode 100644 Content.Shared/_DV/Xenoarchaeology/Artifact/SharedXenoArtifactSystem.DV.cs create mode 100644 Resources/Locale/en-US/_DV/xenoarchaeology/node-scanner.ftl 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]