]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Make artifacts support saving (#14784)
authorNemanja <98561806+EmoGarbage404@users.noreply.github.com>
Fri, 24 Mar 2023 02:50:24 +0000 (22:50 -0400)
committerGitHub <noreply@github.com>
Fri, 24 Mar 2023 02:50:24 +0000 (13:50 +1100)
Content.Server/Xenoarchaeology/Equipment/Systems/ArtifactAnalyzerSystem.cs
Content.Server/Xenoarchaeology/Equipment/Systems/NodeScannerSystem.cs
Content.Server/Xenoarchaeology/XenoArtifacts/ArtifactComponent.cs
Content.Server/Xenoarchaeology/XenoArtifacts/ArtifactSystem.Actions.cs
Content.Server/Xenoarchaeology/XenoArtifacts/ArtifactSystem.Commands.cs
Content.Server/Xenoarchaeology/XenoArtifacts/ArtifactSystem.Nodes.cs
Content.Server/Xenoarchaeology/XenoArtifacts/ArtifactSystem.cs

index 5197f2ca36f58269707e8c37afb2a08fd16d1cd2..6c1bb31833dc3f84031d3c392908bbdc6ea78d02 100644 (file)
@@ -14,11 +14,13 @@ using Content.Shared.MachineLinking.Events;
 using Content.Shared.Popups;
 using Content.Shared.Research.Components;
 using Content.Shared.Xenoarchaeology.Equipment;
+using Content.Shared.Xenoarchaeology.XenoArtifacts;
 using JetBrains.Annotations;
 using Robust.Server.GameObjects;
 using Robust.Server.Player;
 using Robust.Shared.Audio;
 using Robust.Shared.Physics.Events;
+using Robust.Shared.Prototypes;
 using Robust.Shared.Timing;
 using Robust.Shared.Utility;
 
@@ -31,6 +33,7 @@ namespace Content.Server.Xenoarchaeology.Equipment.Systems;
 public sealed class ArtifactAnalyzerSystem : EntitySystem
 {
     [Dependency] private readonly IGameTiming _timing = default!;
+    [Dependency] private readonly IPrototypeManager _prototype = default!;
     [Dependency] private readonly SharedAudioSystem _audio = default!;
     [Dependency] private readonly SharedAmbientSoundSystem _ambienntSound = default!;
     [Dependency] private readonly SharedPopupSystem _popup = default!;
@@ -74,7 +77,8 @@ public sealed class ArtifactAnalyzerSystem : EntitySystem
     {
         base.Update(frameTime);
 
-        foreach (var (active, scan) in EntityQuery<ActiveArtifactAnalyzerComponent, ArtifactAnalyzerComponent>())
+        var query = EntityQueryEnumerator<ActiveArtifactAnalyzerComponent, ArtifactAnalyzerComponent>();
+        while (query.MoveNext(out var uid, out var active, out var scan))
         {
             if (scan.Console != null)
                 UpdateUserInterface(scan.Console.Value);
@@ -82,7 +86,7 @@ public sealed class ArtifactAnalyzerSystem : EntitySystem
             if (_timing.CurTime - active.StartTime < (scan.AnalysisDuration * scan.AnalysisDurationMulitplier))
                 continue;
 
-            FinishScan(scan.Owner, scan, active);
+            FinishScan(uid, scan, active);
         }
     }
 
@@ -139,7 +143,9 @@ public sealed class ArtifactAnalyzerSystem : EntitySystem
         }
         else if (TryComp<ArtifactComponent>(component.LastAnalyzedArtifact, out var artifact))
         {
-            var lastNode = (ArtifactNode?) artifact.CurrentNode?.Clone();
+            var lastNode = artifact.CurrentNodeId == null
+                ? null
+                : (ArtifactNode?) _artifact.GetNodeFromId(artifact.CurrentNodeId.Value, artifact).Clone();
             component.LastAnalyzedNode = lastNode;
             component.LastAnalyzerPointValue = _artifact.GetResearchPointValue(component.LastAnalyzedArtifact.Value, artifact);
         }
@@ -298,17 +304,20 @@ public sealed class ArtifactAnalyzerSystem : EntitySystem
 
         msg.PushNewline();
         var needSecondNewline = false;
-        if (n.Trigger.TriggerHint != null)
+
+        var triggerProto = _prototype.Index<ArtifactTriggerPrototype>(n.Trigger);
+        if (triggerProto.TriggerHint != null)
         {
             msg.AddMarkup(Loc.GetString("analysis-console-info-trigger",
-                ("trigger", Loc.GetString(n.Trigger.TriggerHint))) + "\n");
+                ("trigger", Loc.GetString(triggerProto.TriggerHint))) + "\n");
             needSecondNewline = true;
         }
 
-        if (n.Effect.EffectHint != null)
+        var effectproto = _prototype.Index<ArtifactEffectPrototype>(n.Effect);
+        if (effectproto.EffectHint != null)
         {
             msg.AddMarkup(Loc.GetString("analysis-console-info-effect",
-                ("effect", Loc.GetString(n.Effect.EffectHint))) + "\n");
+                ("effect", Loc.GetString(effectproto.EffectHint))) + "\n");
             needSecondNewline = true;
         }
 
index 1b08696956e48ff749deb2cb47d644d00ab491c3..928de357114584670a4f1198fd4ca1c4c6fd6a50 100644 (file)
@@ -23,7 +23,7 @@ public sealed class NodeScannerSystem : EntitySystem
         if (!args.CanReach || args.Target == null)
             return;
 
-        if (!TryComp<ArtifactComponent>(args.Target, out var artifact) || artifact.CurrentNode == null)
+        if (!TryComp<ArtifactComponent>(args.Target, out var artifact) || artifact.CurrentNodeId == null)
             return;
 
         if (args.Handled)
@@ -33,6 +33,6 @@ public sealed class NodeScannerSystem : EntitySystem
         var target = args.Target.Value;
         _useDelay.BeginDelay(uid);
         _popupSystem.PopupEntity(Loc.GetString("node-scan-popup",
-            ("id", $"{artifact.CurrentNode.Id}")), target);
+            ("id", $"{artifact.CurrentNodeId}")), target);
     }
 }
index 7a10d855324ed65faaa97574fcac6f1cb3284a15..21dfb084e9510c910a0bfce886bf8eb2fce49ea5 100644 (file)
@@ -1,5 +1,6 @@
 using Content.Shared.Xenoarchaeology.XenoArtifacts;
-using Robust.Shared.Serialization.TypeSerializers.Implementations;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
 
 namespace Content.Server.Xenoarchaeology.XenoArtifacts;
 
@@ -7,16 +8,16 @@ namespace Content.Server.Xenoarchaeology.XenoArtifacts;
 public sealed class ArtifactComponent : Component
 {
     /// <summary>
-    /// The artifact's node tree.
+    /// Every node contained in the tree
     /// </summary>
-    [ViewVariables]
-    public ArtifactTree? NodeTree;
+    [DataField("nodeTree"), ViewVariables]
+    public List<ArtifactNode> NodeTree = new();
 
     /// <summary>
     /// The current node the artifact is on.
     /// </summary>
-    [ViewVariables]
-    public ArtifactNode? CurrentNode;
+    [DataField("currentNodeId"), ViewVariables]
+    public int? CurrentNodeId;
 
     #region Node Tree Gen
     /// <summary>
@@ -35,21 +36,20 @@ public sealed class ArtifactComponent : Component
     /// <summary>
     /// Cooldown time between artifact activations (in seconds).
     /// </summary>
-    [DataField("timer", customTypeSerializer: typeof(TimespanSerializer))]
-    [ViewVariables(VVAccess.ReadWrite)]
+    [DataField("timer"), ViewVariables(VVAccess.ReadWrite)]
     public TimeSpan CooldownTime = TimeSpan.FromSeconds(5);
 
     /// <summary>
     /// Is this artifact under some suppression device?
     /// f true, will ignore all trigger activations attempts.
     /// </summary>
-    [ViewVariables(VVAccess.ReadWrite)]
+    [DataField("isSuppressed"), ViewVariables(VVAccess.ReadWrite)]
     public bool IsSuppressed;
 
     /// <summary>
     /// The last time the artifact was activated.
     /// </summary>
-    [DataField("lastActivationTime", customTypeSerializer: typeof(TimespanSerializer))]
+    [DataField("lastActivationTime", customTypeSerializer: typeof(TimeOffsetSerializer))]
     public TimeSpan LastActivationTime;
 
     /// <summary>
@@ -72,25 +72,6 @@ public sealed class ArtifactComponent : Component
     public float PointDangerMultiplier = 1.35f;
 }
 
-/// <summary>
-/// A tree of nodes.
-/// </summary>
-[DataDefinition]
-public sealed class ArtifactTree
-{
-    /// <summary>
-    /// The first node of the tree
-    /// </summary>
-    [ViewVariables]
-    public ArtifactNode StartNode = default!;
-
-    /// <summary>
-    /// Every node contained in the tree
-    /// </summary>
-    [ViewVariables]
-    public readonly List<ArtifactNode> AllNodes = new();
-}
-
 /// <summary>
 /// A single "node" of an artifact that contains various data about it.
 /// </summary>
@@ -98,51 +79,51 @@ public sealed class ArtifactTree
 public sealed class ArtifactNode : ICloneable
 {
     /// <summary>
-    /// A numeric id corresponding to each node. used for display purposes
+    /// A numeric id corresponding to each node.
     /// </summary>
-    [ViewVariables]
+    [DataField("id"), ViewVariables]
     public int Id;
 
     /// <summary>
     /// how "deep" into the node tree. used for generation and price/value calculations
     /// </summary>
-    [ViewVariables]
-    public int Depth = 0;
+    [DataField("depth"), ViewVariables]
+    public int Depth;
 
     /// <summary>
     /// A list of surrounding nodes. Used for tree traversal
     /// </summary>
-    [ViewVariables]
-    public List<ArtifactNode> Edges = new();
+    [DataField("edges"), ViewVariables]
+    public HashSet<int> Edges = new();
 
     /// <summary>
     /// Whether or not the node has been entered
     /// </summary>
-    [ViewVariables(VVAccess.ReadWrite)]
-    public bool Discovered = false;
+    [DataField("discovered"), ViewVariables(VVAccess.ReadWrite)]
+    public bool Discovered;
 
     /// <summary>
     /// The trigger for the node
     /// </summary>
-    [ViewVariables]
-    public ArtifactTriggerPrototype Trigger = default!;
+    [DataField("trigger", customTypeSerializer: typeof(PrototypeIdSerializer<ArtifactTriggerPrototype>), required: true), ViewVariables]
+    public string Trigger = default!;
 
     /// <summary>
     /// Whether or not the node has been triggered
     /// </summary>
-    [ViewVariables(VVAccess.ReadWrite)]
-    public bool Triggered = false;
+    [DataField("triggered"), ViewVariables(VVAccess.ReadWrite)]
+    public bool Triggered;
 
     /// <summary>
     /// The effect when the node is activated
     /// </summary>
-    [ViewVariables]
-    public ArtifactEffectPrototype Effect = default!;
+    [DataField("effect", customTypeSerializer: typeof(PrototypeIdSerializer<ArtifactEffectPrototype>), required: true), ViewVariables]
+    public string Effect = default!;
 
     /// <summary>
     /// Used for storing cumulative information about nodes
     /// </summary>
-    [ViewVariables]
+    [DataField("nodeData"), ViewVariables]
     public Dictionary<string, object> NodeData = new();
 
     public object Clone()
index 3f84fcc6ac1a2e99240ffd402131bd3047b1ea92..69f9450a4fc0f04ae08f0ddb228e4711f7fa633c 100644 (file)
@@ -42,10 +42,10 @@ public partial class ArtifactSystem
 
     private void OnSelfActivate(EntityUid uid, ArtifactComponent component, ArtifactSelfActivateEvent args)
     {
-        if (component.CurrentNode == null)
+        if (component.CurrentNodeId == null)
             return;
 
-        var curNode = component.CurrentNode.Id;
+        var curNode = GetNodeFromId(component.CurrentNodeId.Value, component).Id;
         _popup.PopupEntity(Loc.GetString("activate-artifact-popup-self", ("node", curNode)), uid, uid);
         TryActivateArtifact(uid, uid, component);
 
index ddb1573b32d6d7d75858a829a3a832fe19b2c32a..37f974f6b0a12519d64e12961887e8993166a51d 100644 (file)
@@ -28,10 +28,10 @@ public partial class ArtifactSystem
         if (!EntityUid.TryParse(args[0], out var uid) || ! int.TryParse(args[1], out var id))
             return;
 
-        if (!TryComp<ArtifactComponent>(uid, out var artifact) || artifact.NodeTree == null)
+        if (!TryComp<ArtifactComponent>(uid, out var artifact))
             return;
 
-        if (artifact.NodeTree.AllNodes.FirstOrDefault(n => n.Id == id) is { } node)
+        if (artifact.NodeTree.FirstOrDefault(n => n.Id == id) is { } node)
         {
             EnterNode(uid, ref node);
         }
@@ -41,9 +41,9 @@ public partial class ArtifactSystem
     {
         if (args.Length == 2 && EntityUid.TryParse(args[0], out var uid))
         {
-            if (TryComp<ArtifactComponent>(uid, out var artifact) && artifact.NodeTree != null)
+            if (TryComp<ArtifactComponent>(uid, out var artifact))
             {
-                return CompletionResult.FromHintOptions(artifact.NodeTree.AllNodes.Select(s => s.Id.ToString()), "<node id>");
+                return CompletionResult.FromHintOptions(artifact.NodeTree.Select(s => s.Id.ToString()), "<node id>");
             }
         }
 
@@ -59,10 +59,10 @@ public partial class ArtifactSystem
         if (!EntityUid.TryParse(args[0], out var uid))
             return;
 
-        if (!TryComp<ArtifactComponent>(uid, out var artifact) || artifact.NodeTree == null)
+        if (!TryComp<ArtifactComponent>(uid, out var artifact))
             return;
 
         var pointSum = GetResearchPointValue(uid, artifact, true);
-        shell.WriteLine($"Max point value for {ToPrettyString(uid)} with {artifact.NodeTree.AllNodes.Count} nodes: {pointSum}");
+        shell.WriteLine($"Max point value for {ToPrettyString(uid)} with {artifact.NodeTree.Count} nodes: {pointSum}");
     }
 }
index 5c83440e5f5f4ff882e5c8384af83bf1a3aae5eb..54b83f25143c14d3aef0364c1e55c0a5cf240066 100644 (file)
@@ -1,6 +1,7 @@
 using System.Linq;
 using Content.Server.Xenoarchaeology.XenoArtifacts.Events;
 using Content.Shared.Xenoarchaeology.XenoArtifacts;
+using JetBrains.Annotations;
 using Robust.Shared.Prototypes;
 using Robust.Shared.Random;
 using Robust.Shared.Serialization.Manager;
@@ -15,13 +16,15 @@ public sealed partial class ArtifactSystem
 
     private const int MaxEdgesPerNode = 4;
 
+    private readonly HashSet<int> _usedNodeIds = new();
+
     /// <summary>
     /// Generate an Artifact tree with fully developed nodes.
     /// </summary>
     /// <param name="artifact"></param>
-    /// <param name="tree">The tree being generated.</param>
+    /// <param name="allNodes"></param>
     /// <param name="nodeAmount">The amount of nodes it has.</param>
-    private void GenerateArtifactNodeTree(EntityUid artifact, ref ArtifactTree tree, int nodeAmount)
+    private void GenerateArtifactNodeTree(EntityUid artifact, ref List<ArtifactNode> allNodes, int nodeAmount)
     {
         if (nodeAmount < 1)
         {
@@ -29,19 +32,36 @@ public sealed partial class ArtifactSystem
             return;
         }
 
-        var uninitializedNodes = new List<ArtifactNode> { new() };
-        tree.StartNode = uninitializedNodes.First(); //the first node
+        _usedNodeIds.Clear();
+
 
+        var rootNode = new ArtifactNode
+        {
+            Id = GetValidNodeId()
+        };
+        var uninitializedNodes = new List<ArtifactNode> { rootNode };
         while (uninitializedNodes.Any())
         {
-            GenerateNode(artifact, ref uninitializedNodes, ref tree, nodeAmount);
+            GenerateNode(artifact, ref uninitializedNodes, ref allNodes, nodeAmount);
         }
     }
 
+    private int GetValidNodeId()
+    {
+        var id = _random.Next(10000, 100000);
+        while (_usedNodeIds.Contains(id))
+        {
+            id = _random.Next(10000, 100000);
+        }
+
+        _usedNodeIds.Add(id);
+        return id;
+    }
+
     /// <summary>
     /// Generate an individual node on the tree.
     /// </summary>
-    private void GenerateNode(EntityUid artifact, ref List<ArtifactNode> uninitializedNodes, ref ArtifactTree tree, int targetNodeAmount)
+    private void GenerateNode(EntityUid artifact, ref List<ArtifactNode> uninitializedNodes, ref List<ArtifactNode> allNodes, int targetNodeAmount)
     {
         if (!uninitializedNodes.Any())
             return;
@@ -49,13 +69,10 @@ public sealed partial class ArtifactSystem
         var node = uninitializedNodes.First();
         uninitializedNodes.Remove(node);
 
-        //random 5-digit number
-        node.Id = _random.Next(10000, 100000);
-
         //Generate the connected nodes
-        var maxEdges = Math.Max(1, targetNodeAmount - tree.AllNodes.Count - uninitializedNodes.Count - 1);
+        var maxEdges = Math.Max(1, targetNodeAmount - allNodes.Count - uninitializedNodes.Count - 1);
         maxEdges = Math.Min(maxEdges, MaxEdgesPerNode);
-        var minEdges = Math.Clamp(targetNodeAmount - tree.AllNodes.Count - uninitializedNodes.Count - 1, 0, 1);
+        var minEdges = Math.Clamp(targetNodeAmount - allNodes.Count - uninitializedNodes.Count - 1, 0, 1);
 
         var edgeAmount = _random.Next(minEdges, maxEdges);
 
@@ -63,10 +80,11 @@ public sealed partial class ArtifactSystem
         {
             var neighbor = new ArtifactNode
             {
-                Depth = node.Depth + 1
+                Depth = node.Depth + 1,
+                Id = GetValidNodeId()
             };
-            node.Edges.Add(neighbor);
-            neighbor.Edges.Add(node);
+            node.Edges.Add(neighbor.Id);
+            neighbor.Edges.Add(node.Id);
 
             uninitializedNodes.Add(neighbor);
         }
@@ -74,13 +92,13 @@ public sealed partial class ArtifactSystem
         node.Trigger = GetRandomTrigger(artifact, ref node);
         node.Effect = GetRandomEffect(artifact, ref node);
 
-        tree.AllNodes.Add(node);
+        allNodes.Add(node);
     }
 
     //yeah these two functions are near duplicates but i don't
     //want to implement an interface or abstract parent
 
-    private ArtifactTriggerPrototype GetRandomTrigger(EntityUid artifact, ref ArtifactNode node)
+    private string GetRandomTrigger(EntityUid artifact, ref ArtifactNode node)
     {
         var allTriggers = _prototype.EnumeratePrototypes<ArtifactTriggerPrototype>()
             .Where(x => (x.Whitelist?.IsValid(artifact, EntityManager) ?? true) && (!x.Blacklist?.IsValid(artifact, EntityManager) ?? true)).ToList();
@@ -91,10 +109,10 @@ public sealed partial class ArtifactSystem
         var targetTriggers = allTriggers
             .Where(x => x.TargetDepth == selectedRandomTargetDepth).ToList();
 
-        return _random.Pick(targetTriggers);
+        return _random.Pick(targetTriggers).ID;
     }
 
-    private ArtifactEffectPrototype GetRandomEffect(EntityUid artifact, ref ArtifactNode node)
+    private string GetRandomEffect(EntityUid artifact, ref ArtifactNode node)
     {
         var allEffects = _prototype.EnumeratePrototypes<ArtifactEffectPrototype>()
             .Where(x => (x.Whitelist?.IsValid(artifact, EntityManager) ?? true) && (!x.Blacklist?.IsValid(artifact, EntityManager) ?? true)).ToList();
@@ -105,7 +123,7 @@ public sealed partial class ArtifactSystem
         var targetEffects = allEffects
             .Where(x => x.TargetDepth == selectedRandomTargetDepth).ToList();
 
-        return _random.Pick(targetEffects);
+        return _random.Pick(targetEffects).ID;
     }
 
     /// <remarks>
@@ -156,14 +174,17 @@ public sealed partial class ArtifactSystem
         if (!Resolve(uid, ref component))
             return;
 
-        if (component.CurrentNode != null)
+        if (component.CurrentNodeId != null)
         {
             ExitNode(uid, component);
         }
 
-        component.CurrentNode = node;
+        component.CurrentNodeId = node.Id;
+
+        var trigger = _prototype.Index<ArtifactTriggerPrototype>(node.Trigger);
+        var effect = _prototype.Index<ArtifactEffectPrototype>(node.Effect);
 
-        var allComponents = node.Effect.Components.Concat(node.Effect.PermanentComponents).Concat(node.Trigger.Components);
+        var allComponents = effect.Components.Concat(effect.PermanentComponents).Concat(trigger.Components);
         foreach (var (name, entry) in allComponents)
         {
             var reg = _componentFactory.GetRegistration(name);
@@ -171,7 +192,7 @@ public sealed partial class ArtifactSystem
             if (node.Discovered && EntityManager.HasComponent(uid, reg.Type))
             {
                 // Don't re-add permanent components unless this is the first time you've entered this node
-                if (node.Effect.PermanentComponents.ContainsKey(name))
+                if (effect.PermanentComponents.ContainsKey(name))
                     continue;
 
                 EntityManager.RemoveComponent(uid, reg.Type);
@@ -187,7 +208,7 @@ public sealed partial class ArtifactSystem
         }
 
         node.Discovered = true;
-        RaiseLocalEvent(uid, new ArtifactNodeEnteredEvent(component.CurrentNode.Id));
+        RaiseLocalEvent(uid, new ArtifactNodeEnteredEvent(component.CurrentNodeId.Value));
     }
 
     /// <summary>
@@ -198,16 +219,31 @@ public sealed partial class ArtifactSystem
         if (!Resolve(uid, ref component))
             return;
 
-        var node = component.CurrentNode;
-        if (node == null)
+        if (component.CurrentNodeId == null)
             return;
+        var currentNode = GetNodeFromId(component.CurrentNodeId.Value, component);
+
+        var trigger = _prototype.Index<ArtifactTriggerPrototype>(currentNode.Trigger);
+        var effect = _prototype.Index<ArtifactEffectPrototype>(currentNode.Effect);
 
-        foreach (var name in node.Effect.Components.Keys.Concat(node.Trigger.Components.Keys))
+        foreach (var name in effect.Components.Keys.Concat(trigger.Components.Keys))
         {
             var comp = _componentFactory.GetRegistration(name);
             EntityManager.RemoveComponentDeferred(uid, comp.Type);
         }
 
-        component.CurrentNode = null;
+        component.CurrentNodeId = null;
+    }
+
+    [PublicAPI]
+    public ArtifactNode GetNodeFromId(int id, ArtifactComponent component)
+    {
+        return component.NodeTree.First(x => x.Id == id);
+    }
+
+    [PublicAPI]
+    public ArtifactNode GetNodeFromId(int id, IEnumerable<ArtifactNode> nodes)
+    {
+        return nodes.First(x => x.Id == id);
     }
 }
index b0a12c9509db8c89e90cb154abe7a18d29b3ea77..d11f83507992ef1e3a7e9ec1111a243df6ef72da 100644 (file)
@@ -6,6 +6,7 @@ using Content.Server.Power.EntitySystems;
 using Content.Server.Xenoarchaeology.Equipment.Components;
 using Content.Server.Xenoarchaeology.XenoArtifacts.Events;
 using Content.Server.Xenoarchaeology.XenoArtifacts.Triggers.Components;
+using Content.Shared.Xenoarchaeology.XenoArtifacts;
 using JetBrains.Annotations;
 using Robust.Shared.Random;
 using Robust.Shared.Timing;
@@ -46,13 +47,10 @@ public sealed partial class ArtifactSystem : EntitySystem
     /// </remarks>
     private void GetPrice(EntityUid uid, ArtifactComponent component, ref PriceCalculationEvent args)
     {
-        if (component.NodeTree == null)
-            return;
-
-        var price = component.NodeTree.AllNodes.Sum(x => GetNodePrice(x, component));
+        var price = component.NodeTree.Sum(x => GetNodePrice(x, component));
 
         // 25% bonus for fully exploring every node.
-        var fullyExploredBonus = component.NodeTree.AllNodes.Any(x => !x.Triggered) ? 1 : 1.25f;
+        var fullyExploredBonus = component.NodeTree.Any(x => !x.Triggered) ? 1 : 1.25f;
 
         args.Price =+ price * fullyExploredBonus;
     }
@@ -62,10 +60,13 @@ public sealed partial class ArtifactSystem : EntitySystem
         if (!node.Discovered) //no money for undiscovered nodes.
             return 0;
 
+        var triggerProto = _prototype.Index<ArtifactTriggerPrototype>(node.Trigger);
+        var effectProto = _prototype.Index<ArtifactEffectPrototype>(node.Effect);
+
         //quarter price if not triggered
         var priceMultiplier = node.Triggered ? 1f : 0.25f;
         //the danger is the average of node depth, effect danger, and trigger danger.
-        var nodeDanger = (node.Depth + node.Effect.TargetDepth + node.Trigger.TargetDepth) / 3;
+        var nodeDanger = (node.Depth + effectProto.TargetDepth + triggerProto.TargetDepth) / 3;
 
         var price = MathF.Pow(2f, nodeDanger) * component.PricePerNode * priceMultiplier;
         return price;
@@ -86,11 +87,11 @@ public sealed partial class ArtifactSystem : EntitySystem
     /// </remarks>
     public int GetResearchPointValue(EntityUid uid, ArtifactComponent? component = null, bool getMaxPrice = false)
     {
-        if (!Resolve(uid, ref component) || component.NodeTree == null)
+        if (!Resolve(uid, ref component))
             return 0;
 
-        var sumValue = component.NodeTree.AllNodes.Sum(n => GetNodePointValue(n, component, getMaxPrice));
-        var fullyExploredBonus = component.NodeTree.AllNodes.All(x => x.Triggered) || getMaxPrice ? 1.25f : 1;
+        var sumValue = component.NodeTree.Sum(n => GetNodePointValue(n, component, getMaxPrice));
+        var fullyExploredBonus = component.NodeTree.All(x => x.Triggered) || getMaxPrice ? 1.25f : 1;
 
         var pointValue = (int) (sumValue * fullyExploredBonus);
         return pointValue;
@@ -109,7 +110,11 @@ public sealed partial class ArtifactSystem : EntitySystem
 
             valueDeduction = !node.Triggered ? 0.25f : 1;
         }
-        var nodeDanger = (node.Depth + node.Effect.TargetDepth + node.Trigger.TargetDepth) / 3;
+
+        var triggerProto = _prototype.Index<ArtifactTriggerPrototype>(node.Trigger);
+        var effectProto = _prototype.Index<ArtifactEffectPrototype>(node.Effect);
+
+        var nodeDanger = (node.Depth + effectProto.TargetDepth + triggerProto.TargetDepth) / 3;
         return component.PointsPerNode * MathF.Pow(component.PointDangerMultiplier, nodeDanger) * valueDeduction;
     }
 
@@ -121,10 +126,9 @@ public sealed partial class ArtifactSystem : EntitySystem
     {
         var nodeAmount = _random.Next(component.NodesMin, component.NodesMax);
 
-        component.NodeTree = new ArtifactTree();
-
         GenerateArtifactNodeTree(uid, ref component.NodeTree, nodeAmount);
-        EnterNode(uid, ref component.NodeTree.StartNode, component);
+        var firstNode = GetRootNode(component.NodeTree);
+        EnterNode(uid, ref firstNode, component);
     }
 
     /// <summary>
@@ -162,7 +166,7 @@ public sealed partial class ArtifactSystem : EntitySystem
     {
         if (!Resolve(uid, ref component))
             return;
-        if (component.CurrentNode == null)
+        if (component.CurrentNodeId == null)
             return;
 
         component.LastActivationTime = _gameTiming.CurTime;
@@ -173,8 +177,10 @@ public sealed partial class ArtifactSystem : EntitySystem
         };
         RaiseLocalEvent(uid, ev, true);
 
-        component.CurrentNode.Triggered = true;
-        if (component.CurrentNode.Edges.Any())
+        var currentNode = GetNodeFromId(component.CurrentNodeId.Value, component);
+
+        currentNode.Triggered = true;
+        if (currentNode.Edges.Any())
         {
             var newNode = GetNewNode(uid, component);
             if (newNode == null)
@@ -185,10 +191,18 @@ public sealed partial class ArtifactSystem : EntitySystem
 
     private ArtifactNode? GetNewNode(EntityUid uid, ArtifactComponent component)
     {
-        if (component.CurrentNode == null)
+        if (component.CurrentNodeId == null)
             return null;
 
-        var allNodes = component.CurrentNode.Edges;
+        var currentNode = GetNodeFromId(component.CurrentNodeId.Value, component);
+
+        var allNodes = currentNode.Edges;
+        Logger.Debug($"our node: {currentNode.Id}");
+        Logger.Debug("other nodes:");
+        foreach (var other in allNodes)
+        {
+            Logger.Debug($"{other}");
+        }
 
         if (TryComp<BiasedArtifactComponent>(uid, out var bias) &&
             TryComp<TraversalDistorterComponent>(bias.Provider, out var trav) &&
@@ -198,26 +212,26 @@ public sealed partial class ArtifactSystem : EntitySystem
             switch (trav.BiasDirection)
             {
                 case BiasDirection.In:
-                    var foo = allNodes.Where(x => x.Depth < component.CurrentNode.Depth).ToList();
+                    var foo = allNodes.Where(x => GetNodeFromId(x, component).Depth < currentNode.Depth).ToHashSet();
                     if (foo.Any())
                         allNodes = foo;
                     break;
                 case BiasDirection.Out:
-                    var bar = allNodes.Where(x => x.Depth > component.CurrentNode.Depth).ToList();
+                    var bar = allNodes.Where(x => GetNodeFromId(x, component).Depth > currentNode.Depth).ToHashSet();
                     if (bar.Any())
                         allNodes = bar;
                     break;
             }
         }
 
-        var undiscoveredNodes = allNodes.Where(x => !x.Discovered).ToList();
+        var undiscoveredNodes = allNodes.Where(x => GetNodeFromId(x, component).Discovered).ToList();
         var newNode = _random.Pick(allNodes);
         if (undiscoveredNodes.Any() && _random.Prob(0.75f))
         {
             newNode = _random.Pick(undiscoveredNodes);
         }
 
-        return newNode;
+        return GetNodeFromId(newNode, component);
     }
 
     /// <summary>
@@ -236,10 +250,11 @@ public sealed partial class ArtifactSystem : EntitySystem
         if (!Resolve(uid, ref component))
             return false;
 
-        if (component.CurrentNode == null)
+        if (component.CurrentNodeId == null)
             return false;
+        var currentNode = GetNodeFromId(component.CurrentNodeId.Value, component);
 
-        if (component.CurrentNode.NodeData.TryGetValue(key, out var dat) && dat is T value)
+        if (currentNode.NodeData.TryGetValue(key, out var dat) && dat is T value)
         {
             data = value;
             return true;
@@ -260,10 +275,21 @@ public sealed partial class ArtifactSystem : EntitySystem
         if (!Resolve(uid, ref component))
             return;
 
-        if (component.CurrentNode == null)
+        if (component.CurrentNodeId == null)
             return;
+        var currentNode = GetNodeFromId(component.CurrentNodeId.Value, component);
+
+        currentNode.NodeData[key] = value;
+    }
 
-        component.CurrentNode.NodeData[key] = value;
+    /// <summary>
+    /// Gets the base node (depth 0) of an artifact's node graph
+    /// </summary>
+    /// <param name="allNodes"></param>
+    /// <returns></returns>
+    public ArtifactNode GetRootNode(List<ArtifactNode> allNodes)
+    {
+        return allNodes.First(n => n.Depth == 0);
     }
 
     /// <summary>
@@ -271,10 +297,11 @@ public sealed partial class ArtifactSystem : EntitySystem
     /// </summary>
     private void OnRoundEnd(RoundEndTextAppendEvent ev)
     {
-        foreach (var artifactComp in EntityQuery<ArtifactComponent>())
+        var query = EntityQueryEnumerator<ArtifactComponent>();
+        while (query.MoveNext(out var ent, out var artifactComp))
         {
             artifactComp.CooldownTime = TimeSpan.Zero;
-            var timerTrigger = EnsureComp<ArtifactTimerTriggerComponent>(artifactComp.Owner);
+            var timerTrigger = EnsureComp<ArtifactTimerTriggerComponent>(ent);
             timerTrigger.ActivationRate = TimeSpan.FromSeconds(0.5); //HAHAHAHAHAHAHAHAHAH -emo
         }
     }