Repository: impstation/imp-station-14
Pull request #2213: Admin-facing: knockdown instant effect suffixes & VVWrite-able artifact triggers/effects
Author: login: TheGrimbeeper
Created: 2025-04-06T01:35:49Z | Merged: 2025-04-08T11:48:36Z

Three minor tweaks,
1. Added suffixes to the knockdown instant effects used for artifacts - admins can now distinguish between rather than 3 'instant effect' in the spawn menu
2. Enabled VVWrite on artifact triggers and effects. This will both improve ease of testing artifacts, as well as provide more strings for admins to pull their puppets with.  
3. To assist with 2, added a 'StoredTrigger' and 'StoredEffect' value in the artifact node component. The artifact will save the trigger & effect values to this when adding components upon node entry, and look in here when removing. This will prevent components from getting 'stuck' if an admin edits a current node.

NOTE TO ADMINS: Triggers/Effects are only updated on node entry and exit. Editing the current node will have no effect until artifact re-enters the edited node.

No CL, not player facing.


Diff:
diff --git a/Content.Server/Xenoarchaeology/XenoArtifacts/ArtifactComponent.cs b/Content.Server/Xenoarchaeology/XenoArtifacts/ArtifactComponent.cs
index 4afd8af21cfdf..760c0c58fb4b3 100644
--- a/Content.Server/Xenoarchaeology/XenoArtifacts/ArtifactComponent.cs
+++ b/Content.Server/Xenoarchaeology/XenoArtifacts/ArtifactComponent.cs
@@ -127,8 +127,9 @@ public sealed partial class ArtifactNode : ICloneable
 
     /// <summary>
     /// The trigger for the node
+    /// #IMP: added VVAccess.ReadWrite
     /// </summary>
-    [DataField("trigger", customTypeSerializer: typeof(PrototypeIdSerializer<ArtifactTriggerPrototype>), required: true), ViewVariables]
+    [DataField("trigger", customTypeSerializer: typeof(PrototypeIdSerializer<ArtifactTriggerPrototype>), required: true)]
     public string Trigger = default!;
 
     /// <summary>
@@ -139,10 +140,21 @@ public sealed partial class ArtifactNode : ICloneable
 
     /// <summary>
     /// The effect when the node is activated
+    /// #IMP: added VVAccess.ReadWrite
     /// </summary>
-    [DataField("effect", customTypeSerializer: typeof(PrototypeIdSerializer<ArtifactEffectPrototype>), required: true), ViewVariables]
+    [DataField("effect", customTypeSerializer: typeof(PrototypeIdSerializer<ArtifactEffectPrototype>), required: true)]
     public string Effect = default!;
 
+    /// <summary>
+    /// #IMP Save the trigger when node is entered, so if trigger for node changes (due to admin interference), the node can be properly exited
+    /// </summary>
+    public string StoredTrigger = default!;
+
+    /// <summary>
+    /// #IMP Save the effect when node is entered, so if effect for node changes (due to admin interference), the node can be properly exited
+    /// </summary>
+    public string StoredEffect = default!;
+
     /// <summary>
     /// Used for storing cumulative information about nodes
     /// </summary>
@@ -160,7 +172,9 @@ public object Clone()
             Trigger = Trigger,
             Triggered = Triggered,
             Effect = Effect,
-            NodeData = NodeData
+            NodeData = NodeData,
+            StoredTrigger = StoredTrigger,
+            StoredEffect = StoredEffect
         };
     }
 }
diff --git a/Content.Server/Xenoarchaeology/XenoArtifacts/ArtifactSystem.Nodes.cs b/Content.Server/Xenoarchaeology/XenoArtifacts/ArtifactSystem.Nodes.cs
index cb2c1af08d35a..b5dfdd1c57b4a 100644
--- a/Content.Server/Xenoarchaeology/XenoArtifacts/ArtifactSystem.Nodes.cs
+++ b/Content.Server/Xenoarchaeology/XenoArtifacts/ArtifactSystem.Nodes.cs
@@ -232,8 +232,31 @@ private void EnterNode(EntityUid uid, ref ArtifactNode node, ArtifactComponent?
 
         component.CurrentNodeId = node.Id;
 
-        var trigger = _prototype.Index<ArtifactTriggerPrototype>(node.Trigger);
-        var effect = _prototype.Index<ArtifactEffectPrototype>(node.Effect);
+        //#IMP Attempt to get trigger/effect from string, fall back to defaults if not in prototype
+        // Putting defaults in here rather than components because artifact component isn't guaranteed to exist,
+        // and these should never be modified in-game
+        var defaultTrigger = "TriggerExamine";
+        var defaultEffect = "EffectBadFeeling";
+        _prototype.TryIndex<ArtifactTriggerPrototype>(node.Trigger, out var maybeTrigger);
+        _prototype.TryIndex<ArtifactEffectPrototype>(node.Effect, out var maybeEffect);
+
+        var trigger = _prototype.Index<ArtifactTriggerPrototype>(defaultTrigger);
+        var effect = _prototype.Index<ArtifactEffectPrototype>(defaultEffect);
+        if (maybeTrigger is null)
+            Log.Debug($"Trigger prototype {node.Trigger} not found for artifact entity {ToPrettyString(uid)}, falling back to default");
+        else
+            trigger = maybeTrigger;
+
+        if (maybeEffect is null)
+            Log.Debug($"Effect prototype {node.Effect} not found for artifact entity {ToPrettyString(uid)}, falling back to default");
+        else
+            effect = maybeEffect;
+
+        // #IMP: Save trigger & effect to allow proper exiting in case admin edits between entry and exit.
+        node.StoredTrigger = maybeTrigger != null ? node.Trigger : defaultTrigger;
+        node.StoredEffect = maybeEffect != null ? node.Effect : defaultEffect;
+
+        //#END IMP
 
         var allComponents = effect.Components.Concat(effect.PermanentComponents).Concat(trigger.Components);
         foreach (var (name, entry) in allComponents)
@@ -271,8 +294,8 @@ private void ExitNode(EntityUid uid, ArtifactComponent? component = null)
             return;
         var currentNode = GetNodeFromId(component.CurrentNodeId.Value, component);
 
-        var trigger = _prototype.Index<ArtifactTriggerPrototype>(currentNode.Trigger);
-        var effect = _prototype.Index<ArtifactEffectPrototype>(currentNode.Effect);
+        var trigger = _prototype.Index<ArtifactTriggerPrototype>(currentNode.StoredTrigger);
+        var effect = _prototype.Index<ArtifactEffectPrototype>(currentNode.StoredEffect);
 
         var entityPrototype = MetaData(uid).EntityPrototype;
         var toRemove = effect.Components.Keys.Concat(trigger.Components.Keys).ToList();
diff --git a/Resources/Prototypes/_Impstation/XenoArch/Effects/normal_effects.yml b/Resources/Prototypes/_Impstation/XenoArch/Effects/normal_effects.yml
index e9c6699543b9a..822743b03a8d1 100644
--- a/Resources/Prototypes/_Impstation/XenoArch/Effects/normal_effects.yml
+++ b/Resources/Prototypes/_Impstation/XenoArch/Effects/normal_effects.yml
@@ -1,5 +1,6 @@
 - type: entity
   id: InstantEffectVeryLocalKnockdown
+  suffix: "Very local knockdown"
   parent: AdminInstantEffectBase
   components:
   - type: StunOnTrigger
@@ -28,6 +29,7 @@
 
 - type: entity
   id: InstantEffectLocalKnockdown
+  suffix: "Local knockdown"
   parent: AdminInstantEffectBase
   components:
   - type: StunOnTrigger
@@ -458,6 +460,7 @@
 
 - type: entity
   id: InstantEffectGridKnockdown
+  suffix: "Gridwide knockdown"
   parent: AdminInstantEffectBase
   components:
   - type: StunOnTrigger


Reviews:
user: login: Darkmajia
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-07T15:49:24Z
user: login: Darkmajia
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-07T15:49:47Z
user: login: Darkmajia
body: this is pretty scary and i fear we may be about to find out why Trigger was read-only, but you are our strongest artifact warrior
state: CHANGES_REQUESTED
author_association: NONE
submitted_at: 2025-04-07T15:51:34Z
user: login: Darkmajia
body: may god punish us for our sins
state: APPROVED
author_association: NONE
submitted_at: 2025-04-08T11:48:24Z

Review comments:
diff_hunk: @@ -127,22 +127,34 @@ public sealed partial class ArtifactNode : ICloneable
 
     /// <summary>
     /// The trigger for the node
+    /// #IMP: added VVAccess.ReadWrite
     /// </summary>
-    [DataField("trigger", customTypeSerializer: typeof(PrototypeIdSerializer<ArtifactTriggerPrototype>), required: true), ViewVariables]
+    [DataField("trigger", customTypeSerializer: typeof(PrototypeIdSerializer<ArtifactTriggerPrototype>), required: true), ViewVariables(VVAccess.ReadWrite)]
path: Content.Server/Xenoarchaeology/XenoArtifacts/ArtifactComponent.cs
user: login: Darkmajia
body: `VVAccess.ReadWrite` is the default, we can save a tiny bit of space by just deleting `ViewVariables` outright and letting default handle it. same for `Effect` below
created_at: 2025-04-07T15:49:24Z
author_association: NONE
diff_hunk: @@ -127,22 +127,34 @@ public sealed partial class ArtifactNode : ICloneable
 
     /// <summary>
     /// The trigger for the node
+    /// #IMP: added VVAccess.ReadWrite
     /// </summary>
-    [DataField("trigger", customTypeSerializer: typeof(PrototypeIdSerializer<ArtifactTriggerPrototype>), required: true), ViewVariables]
+    [DataField("trigger", customTypeSerializer: typeof(PrototypeIdSerializer<ArtifactTriggerPrototype>), required: true), ViewVariables(VVAccess.ReadWrite)]
     public string Trigger = default!;
 
     /// <summary>
     /// Whether or not the node has been triggered
+    /// #IMP: added VVAccess.ReadWrite
path: Content.Server/Xenoarchaeology/XenoArtifacts/ArtifactComponent.cs
user: login: Darkmajia
body: this already had `ReadWrite` so this comment is inaccurate
created_at: 2025-04-07T15:49:47Z
author_association: NONE

PR comments:
user: login: mqole
created_at: 2025-04-06T14:44:20Z
body: >        NOTE TO ADMINS: Do NOT edit the trigger or effect of the CURRENT node an artifact is on, it will make the artifact forget to remove the components so the old trigger/effect will remain forever (or until a duplicate shows up).

this scares me. is there any way to idiot-proof this (asking for a friend)
author_association: MEMBER
user: login: TheGrimbeeper
created_at: 2025-04-07T08:21:31Z
body: > > ```
> >    NOTE TO ADMINS: Do NOT edit the trigger or effect of the CURRENT node an artifact is on, it will make the artifact forget to remove the components so the old trigger/effect will remain forever (or until a duplicate shows up).
> > ```
> 
> this scares me. is there any way to idiot-proof this (asking for a friend)

Done :)
author_association: NONE
user: login: TheGrimbeeper
created_at: 2025-04-08T08:41:50Z
body: Just realised there is no protection for an incorrect trigger/effect prototype! Gotta whip that up before its safe. Whoopsie
author_association: NONE
user: login: TheGrimbeeper
created_at: 2025-04-08T09:08:25Z
body: yay it should be safe now, sorry for the big changes but its safeproofing
author_association: NONE