]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Add DNA injector (#41271)
authorslarticodefast <161409025+slarticodefast@users.noreply.github.com>
Mon, 3 Nov 2025 12:02:48 +0000 (13:02 +0100)
committerGitHub <noreply@github.com>
Mon, 3 Nov 2025 12:02:48 +0000 (12:02 +0000)
* add item

* Update Content.Shared/Changeling/Systems/ChangelingClonerSystem.cs

Co-authored-by: ScarKy0 <106310278+ScarKy0@users.noreply.github.com>
---------

Co-authored-by: ScarKy0 <106310278+ScarKy0@users.noreply.github.com>
Content.Shared/Changeling/Components/ChangelingClonerComponent.cs [new file with mode: 0644]
Content.Shared/Changeling/Systems/ChangelingClonerSystem.cs [new file with mode: 0644]
Content.Shared/Changeling/Systems/SharedChangelingIdentitySystem.cs
Resources/Locale/en-US/changeling/changeling-cloner-component.ftl [new file with mode: 0644]
Resources/Prototypes/Entities/Objects/Devices/Syndicate_Gadgets/dna_injector.yml [new file with mode: 0644]

diff --git a/Content.Shared/Changeling/Components/ChangelingClonerComponent.cs b/Content.Shared/Changeling/Components/ChangelingClonerComponent.cs
new file mode 100644 (file)
index 0000000..20cb690
--- /dev/null
@@ -0,0 +1,100 @@
+using Content.Shared.Charges.Components;
+using Content.Shared.Cloning;
+using Robust.Shared.Audio;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Changeling.Components;
+
+/// <summary>
+/// Changeling transformation in item form!
+/// An entity with this component works like an implanter:
+/// First you use it on a humanoid to make a copy of their identity, along with all species relevant components,
+/// then use it on someone else to tranform them into a clone of them.
+/// Can be used in combination with <see cref="LimitedChargesComponent"/>
+/// </summary>
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+public sealed partial class ChangelingClonerComponent : Component
+{
+    /// <summary>
+    /// A clone of the player you have copied the identity from.
+    /// This is a full humanoid backup, stored on a paused map.
+    /// </summary>
+    /// <remarks>
+    /// Since this entity is stored on a separate map it will be outside PVS range.
+    /// </remarks>
+    [DataField, AutoNetworkedField]
+    public EntityUid? ClonedBackup;
+
+    /// <summary>
+    /// Current state of the item.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public ChangelingClonerState State = ChangelingClonerState.Empty;
+
+    /// <summary>
+    /// The cloning settings to use.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public ProtoId<CloningSettingsPrototype> Settings = "ChangelingCloningSettings";
+
+    /// <summary>
+    /// Doafter time for drawing and injecting.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public TimeSpan DoAfter = TimeSpan.FromSeconds(5);
+
+    /// <summary>
+    /// Can this item be used more than once?
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public bool Reusable = true;
+
+    /// <summary>
+    /// Whether or not to add a reset verb to purge the stored identity,
+    /// allowing you to draw a new one.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public bool CanReset = true;
+
+    /// <summary>
+    /// Raise events when renaming the target?
+    /// This will change their ID card, crew manifest entry, and so on.
+    /// For admeme purposes.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public bool RaiseNameChangeEvents;
+
+    /// <summary>
+    /// The sound to play when taking someone's identity with the item.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public SoundSpecifier? DrawSound;
+
+    /// <summary>
+    /// The sound to play when someone is transformed.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public SoundSpecifier? InjectSound;
+}
+
+/// <summary>
+/// Current state of the item.
+/// </summary>
+[Serializable, NetSerializable]
+public enum ChangelingClonerState : byte
+{
+    /// <summary>
+    /// No sample taken yet.
+    /// </summary>
+    Empty,
+    /// <summary>
+    /// Filled with a DNA sample.
+    /// </summary>
+    Filled,
+    /// <summary>
+    /// Has been used (single use only).
+    /// </summary>
+    Spent,
+}
diff --git a/Content.Shared/Changeling/Systems/ChangelingClonerSystem.cs b/Content.Shared/Changeling/Systems/ChangelingClonerSystem.cs
new file mode 100644 (file)
index 0000000..d65d39c
--- /dev/null
@@ -0,0 +1,308 @@
+using Content.Shared.Administration.Logs;
+using Content.Shared.Changeling.Components;
+using Content.Shared.Cloning;
+using Content.Shared.Database;
+using Content.Shared.DoAfter;
+using Content.Shared.Examine;
+using Content.Shared.Forensics.Systems;
+using Content.Shared.Humanoid;
+using Content.Shared.IdentityManagement;
+using Content.Shared.Interaction;
+using Content.Shared.Popups;
+using Content.Shared.Verbs;
+using Robust.Shared.Audio.Systems;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Changeling.Systems;
+
+public sealed class ChangelingClonerSystem : EntitySystem
+{
+    [Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
+    [Dependency] private readonly SharedHumanoidAppearanceSystem _humanoidAppearance = default!;
+    [Dependency] private readonly MetaDataSystem _metaData = default!;
+    [Dependency] private readonly SharedPopupSystem _popup = default!;
+    [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
+    [Dependency] private readonly SharedAudioSystem _audio = default!;
+    [Dependency] private readonly SharedCloningSystem _cloning = default!;
+    [Dependency] private readonly IPrototypeManager _prototype = default!;
+    [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
+    [Dependency] private readonly SharedChangelingIdentitySystem _changelingIdentity = default!;
+    [Dependency] private readonly SharedForensicsSystem _forensics = default!;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<ChangelingClonerComponent, ExaminedEvent>(OnExamine);
+        SubscribeLocalEvent<ChangelingClonerComponent, GetVerbsEvent<Verb>>(OnGetVerbs);
+        SubscribeLocalEvent<ChangelingClonerComponent, AfterInteractEvent>(OnAfterInteract);
+        SubscribeLocalEvent<ChangelingClonerComponent, ClonerDrawDoAfterEvent>(OnDraw);
+        SubscribeLocalEvent<ChangelingClonerComponent, ClonerInjectDoAfterEvent>(OnInject);
+        SubscribeLocalEvent<ChangelingClonerComponent, ComponentShutdown>(OnShutDown);
+    }
+
+    private void OnShutDown(Entity<ChangelingClonerComponent> ent, ref ComponentShutdown args)
+    {
+        // Delete the stored clone.
+        PredictedQueueDel(ent.Comp.ClonedBackup);
+    }
+
+    private void OnExamine(Entity<ChangelingClonerComponent> ent, ref ExaminedEvent args)
+    {
+        if (!args.IsInDetailsRange)
+            return;
+
+        var msg = ent.Comp.State switch
+        {
+            ChangelingClonerState.Empty => "changeling-cloner-component-empty",
+            ChangelingClonerState.Filled => "changeling-cloner-component-filled",
+            ChangelingClonerState.Spent => "changeling-cloner-component-spent",
+            _ => "error"
+        };
+
+        args.PushMarkup(Loc.GetString(msg));
+
+    }
+
+    private void OnGetVerbs(Entity<ChangelingClonerComponent> ent, ref GetVerbsEvent<Verb> args)
+    {
+        if (!args.CanInteract || !args.CanAccess || args.Hands == null)
+            return;
+
+        if (!ent.Comp.CanReset || ent.Comp.State == ChangelingClonerState.Spent)
+            return;
+
+        var user = args.User;
+        args.Verbs.Add(new Verb
+        {
+            Text = Loc.GetString("changeling-cloner-component-reset-verb"),
+            Disabled = ent.Comp.ClonedBackup == null,
+            Act = () => Reset(ent.AsNullable(), user),
+            DoContactInteraction = true,
+        });
+    }
+
+    private void OnAfterInteract(Entity<ChangelingClonerComponent> ent, ref AfterInteractEvent args)
+    {
+        if (args.Handled || !args.CanReach || args.Target == null)
+            return;
+
+        switch (ent.Comp.State)
+        {
+            case ChangelingClonerState.Empty:
+                args.Handled |= TryDraw(ent.AsNullable(), args.Target.Value, args.User);
+                break;
+            case ChangelingClonerState.Filled:
+                args.Handled |= TryInject(ent.AsNullable(), args.Target.Value, args.User);
+                break;
+            case ChangelingClonerState.Spent:
+            default:
+                break;
+        }
+
+    }
+
+    private void OnDraw(Entity<ChangelingClonerComponent> ent, ref ClonerDrawDoAfterEvent args)
+    {
+        if (args.Handled || args.Cancelled || args.Target == null)
+            return;
+
+        Draw(ent.AsNullable(), args.Target.Value, args.User);
+        args.Handled = true;
+    }
+
+    private void OnInject(Entity<ChangelingClonerComponent> ent, ref ClonerInjectDoAfterEvent args)
+    {
+        if (args.Handled || args.Cancelled || args.Target == null)
+            return;
+
+        Inject(ent.AsNullable(), args.Target.Value, args.User);
+        args.Handled = true;
+    }
+
+    /// <summary>
+    /// Start a DoAfter to draw a DNA sample from the target.
+    /// </summary>
+    public bool TryDraw(Entity<ChangelingClonerComponent?> ent, EntityUid target, EntityUid user)
+    {
+        if (!Resolve(ent, ref ent.Comp))
+            return false;
+
+        if (ent.Comp.State != ChangelingClonerState.Empty)
+            return false;
+
+        if (!HasComp<HumanoidAppearanceComponent>(target))
+            return false; // cloning only works for humanoids at the moment
+
+        var args = new DoAfterArgs(EntityManager, user, ent.Comp.DoAfter, new ClonerDrawDoAfterEvent(), ent, target: target, used: ent)
+        {
+            BreakOnDamage = true,
+            BreakOnMove = true,
+            NeedHand = true,
+        };
+
+        if (!_doAfter.TryStartDoAfter(args))
+            return false;
+
+        var userIdentity = Identity.Entity(user, EntityManager);
+        var targetIdentity = Identity.Entity(target, EntityManager);
+        var userMsg = Loc.GetString("changeling-cloner-component-draw-user", ("user", userIdentity), ("target", targetIdentity));
+        var targetMsg = Loc.GetString("changeling-cloner-component-draw-target", ("user", userIdentity), ("target", targetIdentity));
+        _popup.PopupClient(userMsg, target, user);
+
+        if (user != target) // don't show the warning if using the item on yourself
+            _popup.PopupEntity(targetMsg, user, target, PopupType.LargeCaution);
+
+        return true;
+    }
+
+    /// <summary>
+    /// Start a DoAfter to inject a DNA sample into someone, turning them into a clone of the original.
+    /// </summary>
+    public bool TryInject(Entity<ChangelingClonerComponent?> ent, EntityUid target, EntityUid user)
+    {
+        if (!Resolve(ent, ref ent.Comp))
+            return false;
+
+        if (ent.Comp.State != ChangelingClonerState.Filled)
+            return false;
+
+        if (!HasComp<HumanoidAppearanceComponent>(target))
+            return false; // cloning only works for humanoids at the moment
+
+        var args = new DoAfterArgs(EntityManager, user, ent.Comp.DoAfter, new ClonerInjectDoAfterEvent(), ent, target: target, used: ent)
+        {
+            BreakOnDamage = true,
+            BreakOnMove = true,
+            NeedHand = true,
+        };
+
+        if (!_doAfter.TryStartDoAfter(args))
+            return false;
+
+        var userIdentity = Identity.Entity(user, EntityManager);
+        var targetIdentity = Identity.Entity(target, EntityManager);
+        var userMsg = Loc.GetString("changeling-cloner-component-inject-user", ("user", userIdentity), ("target", targetIdentity));
+        var targetMsg = Loc.GetString("changeling-cloner-component-inject-target", ("user", userIdentity), ("target", targetIdentity));
+        _popup.PopupClient(userMsg, target, user);
+
+        if (user != target) // don't show the warning if using the item on yourself
+            _popup.PopupEntity(targetMsg, user, target, PopupType.LargeCaution);
+
+        return true;
+    }
+
+    /// <summary>
+    /// Draw a DNA sample from the target.
+    /// This will create a clone stored on a paused map for data storage.
+    /// </summary>
+    public void Draw(Entity<ChangelingClonerComponent?> ent, EntityUid target, EntityUid user)
+    {
+        if (!Resolve(ent, ref ent.Comp))
+            return;
+
+        if (ent.Comp.State != ChangelingClonerState.Empty)
+            return;
+
+        if (!HasComp<HumanoidAppearanceComponent>(target))
+            return; // cloning only works for humanoids at the moment
+
+        if (!_prototype.Resolve(ent.Comp.Settings, out var settings))
+            return;
+
+        _adminLogger.Add(LogType.Identity,
+            $"{user} is using {ent.Owner} to draw DNA from {target}.");
+
+        // Make a copy of the target on a paused map, so that we can apply their components later.
+        ent.Comp.ClonedBackup = _changelingIdentity.CloneToPausedMap(settings, target);
+        ent.Comp.State = ChangelingClonerState.Filled;
+        _appearance.SetData(ent.Owner, ChangelingClonerVisuals.State, ChangelingClonerState.Filled);
+        Dirty(ent);
+
+        _audio.PlayPredicted(ent.Comp.DrawSound, target, user);
+        _forensics.TransferDna(ent, target);
+    }
+
+    /// <summary>
+    /// Inject a DNA sample into someone, turning them into a clone of the original.
+    /// </summary>
+    public void Inject(Entity<ChangelingClonerComponent?> ent, EntityUid target, EntityUid user)
+    {
+        if (!Resolve(ent, ref ent.Comp))
+            return;
+
+        if (ent.Comp.State != ChangelingClonerState.Filled)
+            return;
+
+        if (!HasComp<HumanoidAppearanceComponent>(target))
+            return; // cloning only works for humanoids at the moment
+
+        if (!_prototype.Resolve(ent.Comp.Settings, out var settings))
+            return;
+
+        _audio.PlayPredicted(ent.Comp.InjectSound, target, user);
+        _forensics.TransferDna(ent, target); // transfer DNA before overwriting it
+
+        if (!ent.Comp.Reusable)
+        {
+            ent.Comp.State = ChangelingClonerState.Spent;
+            _appearance.SetData(ent.Owner, ChangelingClonerVisuals.State, ChangelingClonerState.Spent);
+            Dirty(ent);
+        }
+
+        if (!Exists(ent.Comp.ClonedBackup))
+            return; // the entity is likely out of PVS range on the client
+
+        _adminLogger.Add(LogType.Identity,
+            $"{user} is using {ent.Owner} to inject DNA into {target} changing their identity to {ent.Comp.ClonedBackup.Value}.");
+
+        // Do the actual transformation.
+        _humanoidAppearance.CloneAppearance(ent.Comp.ClonedBackup.Value, target);
+        _cloning.CloneComponents(ent.Comp.ClonedBackup.Value, target, settings);
+        _metaData.SetEntityName(target, Name(ent.Comp.ClonedBackup.Value), raiseEvents: ent.Comp.RaiseNameChangeEvents);
+
+    }
+
+    /// <summary>
+    /// Purge the stored DNA and allow to draw again.
+    /// </summary>
+    public void Reset(Entity<ChangelingClonerComponent?> ent, EntityUid? user)
+    {
+        if (!Resolve(ent, ref ent.Comp))
+            return;
+
+        // Delete the stored clone.
+        PredictedQueueDel(ent.Comp.ClonedBackup);
+        ent.Comp.ClonedBackup = null;
+        ent.Comp.State = ChangelingClonerState.Empty;
+        _appearance.SetData(ent.Owner, ChangelingClonerVisuals.State, ChangelingClonerState.Empty);
+        Dirty(ent);
+
+        if (user == null)
+            return;
+
+        _popup.PopupClient(Loc.GetString("changeling-cloner-component-reset-popup"), user.Value, user.Value);
+    }
+}
+
+/// <summary>
+/// Doafter event for drawing a DNA sample.
+/// </summary>
+[Serializable, NetSerializable]
+public sealed partial class ClonerDrawDoAfterEvent : SimpleDoAfterEvent;
+
+/// <summary>
+/// DoAfterEvent for injecting a DNA sample, turning a player into someone else.
+/// </summary>
+[Serializable, NetSerializable]
+public sealed partial class ClonerInjectDoAfterEvent : SimpleDoAfterEvent;
+
+/// <summary>
+/// Key for the generic visualizer.
+/// </summary>
+[Serializable, NetSerializable]
+public enum ChangelingClonerVisuals : byte
+{
+    State,
+}
index e7e46d79a1f16a8ecfaf00a478d4b6fed2a20a89..830aed6ab6785492619cc36745bbb03f249788d8 100644 (file)
@@ -83,20 +83,19 @@ public abstract class SharedChangelingIdentitySystem : EntitySystem
     }
 
     /// <summary>
-    /// Clone a target humanoid into nullspace and add it to the Changelings list of identities.
-    /// It creates a perfect copy of the target and can be used to pull components down for future use
+    /// Clone a target humanoid to a paused map.
+    /// It creates a perfect copy of the target and can be used to pull components down for future use.
     /// </summary>
-    /// <param name="ent">the Changeling</param>
-    /// <param name="target">the targets uid</param>
-    public EntityUid? CloneToPausedMap(Entity<ChangelingIdentityComponent> ent, EntityUid target)
+    /// <param name="settings">The settings to use for cloning.</param>
+    /// <param name="target">The target to clone.</param>
+    public EntityUid? CloneToPausedMap(CloningSettingsPrototype settings, EntityUid target)
     {
         // Don't create client side duplicate clones or a clientside map.
         if (_net.IsClient)
             return null;
 
         if (!TryComp<HumanoidAppearanceComponent>(target, out var humanoid)
-            || !_prototype.Resolve(humanoid.Species, out var speciesPrototype)
-            || !_prototype.Resolve(ent.Comp.IdentityCloningSettings, out var settings))
+            || !_prototype.Resolve(humanoid.Species, out var speciesPrototype))
             return null;
 
         EnsurePausedMap();
@@ -117,10 +116,30 @@ public abstract class SharedChangelingIdentitySystem : EntitySystem
 
         var targetName = _nameMod.GetBaseName(target);
         _metaSystem.SetEntityName(clone, targetName);
-        ent.Comp.ConsumedIdentities.Add(clone);
+
+        return clone;
+    }
+
+    /// <summary>
+    /// Clone a target humanoid to a paused map and add it to the Changelings list of identities.
+    /// It creates a perfect copy of the target and can be used to pull components down for future use.
+    /// </summary>
+    /// <param name="ent">The Changeling.</param>
+    /// <param name="target">The target to clone.</param>
+    public EntityUid? CloneToPausedMap(Entity<ChangelingIdentityComponent> ent, EntityUid target)
+    {
+        if (!_prototype.Resolve(ent.Comp.IdentityCloningSettings, out var settings))
+            return null;
+
+        var clone = CloneToPausedMap(settings, target);
+
+        if (clone == null)
+            return null;
+
+        ent.Comp.ConsumedIdentities.Add(clone.Value);
 
         Dirty(ent);
-        HandlePvsOverride(ent, clone);
+        HandlePvsOverride(ent, clone.Value);
 
         return clone;
     }
diff --git a/Resources/Locale/en-US/changeling/changeling-cloner-component.ftl b/Resources/Locale/en-US/changeling/changeling-cloner-component.ftl
new file mode 100644 (file)
index 0000000..ffcab1e
--- /dev/null
@@ -0,0 +1,11 @@
+changeling-cloner-component-empty = It is empty.
+changeling-cloner-component-filled = It has a DNA sample in it.
+changeling-cloner-component-spent = It has been used.
+
+changeling-cloner-component-reset-verb = Reset DNA
+changeling-cloner-component-reset-popup = You purge the injector's DNA storage.
+
+changeling-cloner-component-draw-user = You start drawing DNA from {THE($target)}.
+changeling-cloner-component-draw-target = {CAPITALIZE(THE($user))} starts drawing DNA from you.
+changeling-cloner-component-inject-user = You start injecting DNA into {THE($target)}.
+changeling-cloner-component-inject-target = {CAPITALIZE(THE($user))} starts injecting DNA into you.
diff --git a/Resources/Prototypes/Entities/Objects/Devices/Syndicate_Gadgets/dna_injector.yml b/Resources/Prototypes/Entities/Objects/Devices/Syndicate_Gadgets/dna_injector.yml
new file mode 100644 (file)
index 0000000..16454c2
--- /dev/null
@@ -0,0 +1,47 @@
+- type: entity
+  parent: BaseItem
+  id: DnaInjectorUnlimited
+  suffix: Admeme, unlimited
+  # Should not be a traitor item for several reasons:
+  # - Changeling code is still in development, and copying organs etc does not work yet.
+  # - Giving this to traitors makes them overlap with changelings or paradox clones too much.
+  # - It completely makes the voice mask redundant.
+  # - Unlike when disguising yourself as someone else, there is no way to get caught.
+  name: DNA injector
+  description: Can be used to take a DNA sample from someone and inject it into another person, turning them into a clone of the original.
+  components:
+  - type: Sprite
+    sprite: Objects/Specific/Medical/implanter.rsi
+    state: implanter0
+    layers:
+    - state: implanter0
+      map: [ "injector" ]
+      visible: true
+    - state: implanter1
+      map: [ "fillState" ]
+      visible: false
+  - type: Item
+    sprite: Objects/Specific/Medical/implanter.rsi
+    heldPrefix: implanter
+    size: Small
+  - type: Appearance
+  - type: GenericVisualizer
+    visuals:
+      enum.ChangelingClonerVisuals.State:
+        injector:
+          Empty: {state: implanter0}
+          Filled: {state: implanter0}
+          Spent: {state: broken}
+        fillState:
+          Empty: {visible: false}
+          Filled: {visible: true}
+          Spent: {visible: false}
+  - type: ChangelingCloner
+
+- type: entity
+  parent: DnaInjectorUnlimited
+  id: DnaInjector
+  suffix: Admeme, single use
+  components:
+  - type: ChangelingCloner
+    reusable: false