]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Cloning Refactor and bugfixes (#35555)
authorslarticodefast <161409025+slarticodefast@users.noreply.github.com>
Sun, 2 Mar 2025 15:50:12 +0000 (16:50 +0100)
committerGitHub <noreply@github.com>
Sun, 2 Mar 2025 15:50:12 +0000 (16:50 +0100)
* cloning refactor

* cleanup and fixes

* don't pick from 0

* give dwarves the correct species

* fix dna and bloodstream reagent data cloning

* don't copy helmets

* be less redundant

25 files changed:
Content.Server/Body/Systems/BloodstreamSystem.cs
Content.Server/Cloning/AcceptCloningEui.cs
Content.Server/Cloning/CloningConsoleSystem.cs
Content.Server/Cloning/CloningPodSystem.cs [new file with mode: 0644]
Content.Server/Cloning/CloningSystem.cs
Content.Server/Cloning/Components/RandomCloneSpawnerComponent.cs [new file with mode: 0644]
Content.Server/Cloning/RandomCloneSpawnerSystem.cs [new file with mode: 0644]
Content.Server/Forensics/Systems/ForensicsSystem.cs
Content.Server/Implants/SubdermalImplantSystem.cs
Content.Server/Speech/EntitySystems/AddAccentClothingSystem.cs
Content.Server/Traits/Assorted/UnrevivableSystem.cs [new file with mode: 0644]
Content.Server/Zombies/ZombieSystem.Transform.cs
Content.Server/Zombies/ZombieSystem.cs
Content.Shared/Cloning/CloningEvents.cs [new file with mode: 0644]
Content.Shared/Cloning/CloningPodComponent.cs
Content.Shared/Cloning/CloningSettingsPrototype.cs [new file with mode: 0644]
Content.Shared/Forensics/Components/DnaComponent.cs
Content.Shared/Forensics/Events.cs
Content.Shared/Traits/Assorted/UnrevivableComponent.cs
Resources/Locale/en-US/medical/components/cloning-console-component.ftl
Resources/Prototypes/DeviceLinking/source_ports.yml
Resources/Prototypes/Entities/Mobs/Player/clone.yml [new file with mode: 0644]
Resources/Prototypes/Entities/Mobs/Species/dwarf.yml
Resources/Textures/Markers/paradox_clone.rsi/meta.json [new file with mode: 0644]
Resources/Textures/Markers/paradox_clone.rsi/preview.png [new file with mode: 0644]

index d04a993226753dced59794b977766145ce1e7816..6dc03fed744c4c050d76db582f83e33ff8647ab9 100644 (file)
@@ -1,7 +1,6 @@
 using Content.Server.Body.Components;
 using Content.Server.EntityEffects.Effects;
 using Content.Server.Fluids.EntitySystems;
-using Content.Server.Forensics;
 using Content.Server.Popups;
 using Content.Shared.Alert;
 using Content.Shared.Chemistry.Components;
@@ -40,7 +39,6 @@ public sealed class BloodstreamSystem : EntitySystem
     [Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
     [Dependency] private readonly SharedStutteringSystem _stutteringSystem = default!;
     [Dependency] private readonly AlertsSystem _alertsSystem = default!;
-    [Dependency] private readonly ForensicsSystem _forensicsSystem = default!;
 
     public override void Initialize()
     {
@@ -193,17 +191,8 @@ public sealed class BloodstreamSystem : EntitySystem
         bloodSolution.MaxVolume = entity.Comp.BloodMaxVolume;
         tempSolution.MaxVolume = entity.Comp.BleedPuddleThreshold * 4; // give some leeway, for chemstream as well
 
-        // Ensure blood that should have DNA has it; must be run here, in case DnaComponent has not yet been initialized
-
-        if (TryComp<DnaComponent>(entity.Owner, out var donorComp) && donorComp.DNA == String.Empty)
-        {
-            donorComp.DNA = _forensicsSystem.GenerateDNA();
-
-            var ev = new GenerateDnaEvent { Owner = entity.Owner, DNA = donorComp.DNA };
-            RaiseLocalEvent(entity.Owner, ref ev);
-        }
-
         // Fill blood solution with BLOOD
+        // The DNA string might not be initialized yet, but the reagent data gets updated in the GenerateDnaEvent subscription
         bloodSolution.AddReagent(new ReagentId(entity.Comp.BloodReagent, GetEntityBloodData(entity.Owner)), entity.Comp.BloodMaxVolume - bloodSolution.Volume);
     }
 
@@ -492,6 +481,8 @@ public sealed class BloodstreamSystem : EntitySystem
                 reagentData.AddRange(GetEntityBloodData(entity.Owner));
             }
         }
+        else
+            Log.Error("Unable to set bloodstream DNA, solution entity could not be resolved");
     }
 
     /// <summary>
@@ -502,13 +493,10 @@ public sealed class BloodstreamSystem : EntitySystem
         var bloodData = new List<ReagentData>();
         var dnaData = new DnaData();
 
-        if (TryComp<DnaComponent>(uid, out var donorComp))
-        {
+        if (TryComp<DnaComponent>(uid, out var donorComp) && donorComp.DNA != null)
             dnaData.DNA = donorComp.DNA;
-        } else
-        {
+        else
             dnaData.DNA = Loc.GetString("forensics-dna-unknown");
-        }
 
         bloodData.Add(dnaData);
 
index 3d4356f8ca07c17eb9ef253a1e96c8d9eb7cc1c4..2d1ea93fdb218e62fe1e63cec01e8cc7042209cc 100644 (file)
@@ -9,13 +9,13 @@ namespace Content.Server.Cloning
     {
         private readonly EntityUid _mindId;
         private readonly MindComponent _mind;
-        private readonly CloningSystem _cloningSystem;
+        private readonly CloningPodSystem _cloningPodSystem;
 
-        public AcceptCloningEui(EntityUid mindId, MindComponent mind, CloningSystem cloningSys)
+        public AcceptCloningEui(EntityUid mindId, MindComponent mind, CloningPodSystem cloningPodSys)
         {
             _mindId = mindId;
             _mind = mind;
-            _cloningSystem = cloningSys;
+            _cloningPodSystem = cloningPodSys;
         }
 
         public override void HandleMessage(EuiMessageBase msg)
@@ -29,7 +29,7 @@ namespace Content.Server.Cloning
                 return;
             }
 
-            _cloningSystem.TransferMindToClone(_mindId, _mind);
+            _cloningPodSystem.TransferMindToClone(_mindId, _mind);
             Close();
         }
     }
index 050e2b7f0647fbff29fa71c339995270561fb4da..39eac842f0a57e7e2855f9986f43c6d5479238e5 100644 (file)
@@ -3,7 +3,6 @@ using Content.Server.Administration.Logs;
 using Content.Server.Cloning.Components;
 using Content.Server.DeviceLinking.Systems;
 using Content.Server.Medical.Components;
-using Content.Server.Power.Components;
 using Content.Server.Power.EntitySystems;
 using Content.Shared.UserInterface;
 using Content.Shared.Cloning;
@@ -16,19 +15,17 @@ using Content.Shared.Mind;
 using Content.Shared.Mobs.Components;
 using Content.Shared.Mobs.Systems;
 using Content.Shared.Power;
-using JetBrains.Annotations;
 using Robust.Server.GameObjects;
 using Robust.Server.Player;
 
 namespace Content.Server.Cloning
 {
-    [UsedImplicitly]
     public sealed class CloningConsoleSystem : EntitySystem
     {
         [Dependency] private readonly DeviceLinkSystem _signalSystem = default!;
         [Dependency] private readonly IAdminLogManager _adminLogger = default!;
         [Dependency] private readonly IPlayerManager _playerManager = default!;
-        [Dependency] private readonly CloningSystem _cloningSystem = default!;
+        [Dependency] private readonly CloningPodSystem _cloningPodSystem = default!;
         [Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
         [Dependency] private readonly MobStateSystem _mobStateSystem = default!;
         [Dependency] private readonly PowerReceiverSystem _powerReceiverSystem = default!;
@@ -171,7 +168,7 @@ namespace Content.Server.Cloning
             if (mind.UserId.HasValue == false || mind.Session == null)
                 return;
 
-            if (_cloningSystem.TryCloning(cloningPodUid, body.Value, (mindId, mind), cloningPod, scannerComp.CloningFailChanceMultiplier))
+            if (_cloningPodSystem.TryCloning(cloningPodUid, body.Value, (mindId, mind), cloningPod, scannerComp.CloningFailChanceMultiplier))
                 _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(uid)} successfully cloned {ToPrettyString(body.Value)}.");
         }
 
diff --git a/Content.Server/Cloning/CloningPodSystem.cs b/Content.Server/Cloning/CloningPodSystem.cs
new file mode 100644 (file)
index 0000000..594c5eb
--- /dev/null
@@ -0,0 +1,323 @@
+using Content.Server.Atmos.EntitySystems;
+using Content.Server.Chat.Systems;
+using Content.Server.Cloning.Components;
+using Content.Server.DeviceLinking.Systems;
+using Content.Server.EUI;
+using Content.Server.Fluids.EntitySystems;
+using Content.Server.Materials;
+using Content.Server.Popups;
+using Content.Server.Power.EntitySystems;
+using Content.Shared.Atmos;
+using Content.Shared.CCVar;
+using Content.Shared.Chemistry.Components;
+using Content.Shared.Cloning;
+using Content.Shared.Damage;
+using Content.Shared.DeviceLinking.Events;
+using Content.Shared.Emag.Components;
+using Content.Shared.Emag.Systems;
+using Content.Shared.Examine;
+using Content.Shared.GameTicking;
+using Content.Shared.Mind;
+using Content.Shared.Mind.Components;
+using Content.Shared.Mobs.Systems;
+using Robust.Server.Containers;
+using Robust.Server.Player;
+using Robust.Shared.Audio.Systems;
+using Robust.Shared.Configuration;
+using Robust.Shared.Containers;
+using Robust.Shared.Physics.Components;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+
+namespace Content.Server.Cloning;
+
+public sealed class CloningPodSystem : EntitySystem
+{
+    [Dependency] private readonly DeviceLinkSystem _signalSystem = default!;
+    [Dependency] private readonly IPlayerManager _playerManager = null!;
+    [Dependency] private readonly EuiManager _euiManager = null!;
+    [Dependency] private readonly CloningConsoleSystem _cloningConsoleSystem = default!;
+    [Dependency] private readonly ContainerSystem _containerSystem = default!;
+    [Dependency] private readonly MobStateSystem _mobStateSystem = default!;
+    [Dependency] private readonly PowerReceiverSystem _powerReceiverSystem = default!;
+    [Dependency] private readonly IRobustRandom _robustRandom = default!;
+    [Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
+    [Dependency] private readonly SharedTransformSystem _transformSystem = default!;
+    [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
+    [Dependency] private readonly PuddleSystem _puddleSystem = default!;
+    [Dependency] private readonly ChatSystem _chatSystem = default!;
+    [Dependency] private readonly SharedAudioSystem _audio = default!;
+    [Dependency] private readonly IConfigurationManager _configManager = default!;
+    [Dependency] private readonly MaterialStorageSystem _material = default!;
+    [Dependency] private readonly PopupSystem _popupSystem = default!;
+    [Dependency] private readonly SharedMindSystem _mindSystem = default!;
+    [Dependency] private readonly CloningSystem _cloning = default!;
+    [Dependency] private readonly EmagSystem _emag = default!;
+
+    public readonly Dictionary<MindComponent, EntityUid> ClonesWaitingForMind = new();
+    public readonly ProtoId<CloningSettingsPrototype> SettingsId = "CloningPod";
+    public const float EasyModeCloningCost = 0.7f;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<RoundRestartCleanupEvent>(Reset);
+        SubscribeLocalEvent<BeingClonedComponent, MindAddedMessage>(HandleMindAdded);
+        SubscribeLocalEvent<CloningPodComponent, ComponentInit>(OnComponentInit);
+        SubscribeLocalEvent<CloningPodComponent, PortDisconnectedEvent>(OnPortDisconnected);
+        SubscribeLocalEvent<CloningPodComponent, AnchorStateChangedEvent>(OnAnchor);
+        SubscribeLocalEvent<CloningPodComponent, ExaminedEvent>(OnExamined);
+        SubscribeLocalEvent<CloningPodComponent, GotEmaggedEvent>(OnEmagged);
+    }
+
+    private void OnComponentInit(Entity<CloningPodComponent> ent, ref ComponentInit args)
+    {
+        ent.Comp.BodyContainer = _containerSystem.EnsureContainer<ContainerSlot>(ent.Owner, "clonepod-bodyContainer");
+        _signalSystem.EnsureSinkPorts(ent.Owner, ent.Comp.PodPort);
+    }
+
+    internal void TransferMindToClone(EntityUid mindId, MindComponent mind)
+    {
+        if (!ClonesWaitingForMind.TryGetValue(mind, out var entity) ||
+            !EntityManager.EntityExists(entity) ||
+            !TryComp<MindContainerComponent>(entity, out var mindComp) ||
+            mindComp.Mind != null)
+            return;
+
+        _mindSystem.TransferTo(mindId, entity, ghostCheckOverride: true, mind: mind);
+        _mindSystem.UnVisit(mindId, mind);
+        ClonesWaitingForMind.Remove(mind);
+    }
+
+    private void HandleMindAdded(EntityUid uid, BeingClonedComponent clonedComponent, MindAddedMessage message)
+    {
+        if (clonedComponent.Parent == EntityUid.Invalid ||
+            !EntityManager.EntityExists(clonedComponent.Parent) ||
+            !TryComp<CloningPodComponent>(clonedComponent.Parent, out var cloningPodComponent) ||
+            uid != cloningPodComponent.BodyContainer.ContainedEntity)
+        {
+            EntityManager.RemoveComponent<BeingClonedComponent>(uid);
+            return;
+        }
+        UpdateStatus(clonedComponent.Parent, CloningPodStatus.Cloning, cloningPodComponent);
+    }
+    private void OnPortDisconnected(Entity<CloningPodComponent> ent, ref PortDisconnectedEvent args)
+    {
+        ent.Comp.ConnectedConsole = null;
+    }
+
+    private void OnAnchor(Entity<CloningPodComponent> ent, ref AnchorStateChangedEvent args)
+    {
+        if (ent.Comp.ConnectedConsole == null || !TryComp<CloningConsoleComponent>(ent.Comp.ConnectedConsole, out var console))
+            return;
+
+        if (args.Anchored)
+        {
+            _cloningConsoleSystem.RecheckConnections(ent.Comp.ConnectedConsole.Value, ent.Owner, console.GeneticScanner, console);
+            return;
+        }
+        _cloningConsoleSystem.UpdateUserInterface(ent.Comp.ConnectedConsole.Value, console);
+    }
+
+    private void OnExamined(Entity<CloningPodComponent> ent, ref ExaminedEvent args)
+    {
+        if (!args.IsInDetailsRange || !_powerReceiverSystem.IsPowered(ent.Owner))
+            return;
+
+        args.PushMarkup(Loc.GetString("cloning-pod-biomass", ("number", _material.GetMaterialAmount(ent.Owner, ent.Comp.RequiredMaterial))));
+    }
+
+    public bool TryCloning(EntityUid uid, EntityUid bodyToClone, Entity<MindComponent> mindEnt, CloningPodComponent? clonePod, float failChanceModifier = 1)
+    {
+        if (!Resolve(uid, ref clonePod))
+            return false;
+
+        if (HasComp<ActiveCloningPodComponent>(uid))
+            return false;
+
+        var mind = mindEnt.Comp;
+        if (ClonesWaitingForMind.TryGetValue(mind, out var clone))
+        {
+            if (EntityManager.EntityExists(clone) &&
+                !_mobStateSystem.IsDead(clone) &&
+                TryComp<MindContainerComponent>(clone, out var cloneMindComp) &&
+                (cloneMindComp.Mind == null || cloneMindComp.Mind == mindEnt))
+                return false; // Mind already has clone
+
+            ClonesWaitingForMind.Remove(mind);
+        }
+
+        if (mind.OwnedEntity != null && !_mobStateSystem.IsDead(mind.OwnedEntity.Value))
+            return false; // Body controlled by mind is not dead
+
+        // Yes, we still need to track down the client because we need to open the Eui
+        if (mind.UserId == null || !_playerManager.TryGetSessionById(mind.UserId.Value, out var client))
+            return false; // If we can't track down the client, we can't offer transfer. That'd be quite bad.
+
+        if (!TryComp<PhysicsComponent>(bodyToClone, out var physics))
+            return false;
+
+        var cloningCost = (int)Math.Round(physics.FixturesMass);
+
+        if (_configManager.GetCVar(CCVars.BiomassEasyMode))
+            cloningCost = (int)Math.Round(cloningCost * EasyModeCloningCost);
+
+        // biomass checks
+        var biomassAmount = _material.GetMaterialAmount(uid, clonePod.RequiredMaterial);
+
+        if (biomassAmount < cloningCost)
+        {
+            if (clonePod.ConnectedConsole != null)
+                _chatSystem.TrySendInGameICMessage(clonePod.ConnectedConsole.Value, Loc.GetString("cloning-console-chat-error", ("units", cloningCost)), InGameICChatType.Speak, false);
+            return false;
+        }
+
+        // end of biomass checks
+
+        // genetic damage checks
+        if (TryComp<DamageableComponent>(bodyToClone, out var damageable) &&
+            damageable.Damage.DamageDict.TryGetValue("Cellular", out var cellularDmg))
+        {
+            var chance = Math.Clamp((float)(cellularDmg / 100), 0, 1);
+            chance *= failChanceModifier;
+
+            if (cellularDmg > 0 && clonePod.ConnectedConsole != null)
+                _chatSystem.TrySendInGameICMessage(clonePod.ConnectedConsole.Value, Loc.GetString("cloning-console-cellular-warning", ("percent", Math.Round(100 - chance * 100))), InGameICChatType.Speak, false);
+
+            if (_robustRandom.Prob(chance))
+            {
+                clonePod.FailedClone = true;
+                UpdateStatus(uid, CloningPodStatus.Gore, clonePod);
+                AddComp<ActiveCloningPodComponent>(uid);
+                _material.TryChangeMaterialAmount(uid, clonePod.RequiredMaterial, -cloningCost);
+                clonePod.UsedBiomass = cloningCost;
+                return true;
+            }
+        }
+        // end of genetic damage checks
+
+        if (!_cloning.TryCloning(bodyToClone, _transformSystem.GetMapCoordinates(bodyToClone), SettingsId, out var mob)) // spawn a new body
+        {
+            if (clonePod.ConnectedConsole != null)
+                _chatSystem.TrySendInGameICMessage(clonePod.ConnectedConsole.Value, Loc.GetString("cloning-console-uncloneable-trait-error"), InGameICChatType.Speak, false);
+            return false;
+        }
+
+        var cloneMindReturn = EntityManager.AddComponent<BeingClonedComponent>(mob.Value);
+        cloneMindReturn.Mind = mind;
+        cloneMindReturn.Parent = uid;
+        _containerSystem.Insert(mob.Value, clonePod.BodyContainer);
+        ClonesWaitingForMind.Add(mind, mob.Value);
+        _euiManager.OpenEui(new AcceptCloningEui(mindEnt, mind, this), client);
+
+        UpdateStatus(uid, CloningPodStatus.NoMind, clonePod);
+        AddComp<ActiveCloningPodComponent>(uid);
+        _material.TryChangeMaterialAmount(uid, clonePod.RequiredMaterial, -cloningCost);
+        clonePod.UsedBiomass = cloningCost;
+        return true;
+    }
+
+    public void UpdateStatus(EntityUid podUid, CloningPodStatus status, CloningPodComponent cloningPod)
+    {
+        cloningPod.Status = status;
+        _appearance.SetData(podUid, CloningPodVisuals.Status, cloningPod.Status);
+    }
+
+    public override void Update(float frameTime)
+    {
+        var query = EntityQueryEnumerator<ActiveCloningPodComponent, CloningPodComponent>();
+        while (query.MoveNext(out var uid, out var _, out var cloning))
+        {
+            if (!_powerReceiverSystem.IsPowered(uid))
+                continue;
+
+            if (cloning.BodyContainer.ContainedEntity == null && !cloning.FailedClone)
+                continue;
+
+            cloning.CloningProgress += frameTime;
+            if (cloning.CloningProgress < cloning.CloningTime)
+                continue;
+
+            if (cloning.FailedClone)
+                EndFailedCloning(uid, cloning);
+            else
+                Eject(uid, cloning);
+        }
+    }
+
+    /// <summary>
+    /// On emag, spawns a failed clone when cloning process fails which attacks nearby crew.
+    /// </summary>
+    private void OnEmagged(Entity<CloningPodComponent> ent, ref GotEmaggedEvent args)
+    {
+        if (!_emag.CompareFlag(args.Type, EmagType.Interaction))
+            return;
+
+        if (_emag.CheckFlag(ent.Owner, EmagType.Interaction))
+            return;
+
+        if (!this.IsPowered(ent.Owner, EntityManager))
+            return;
+
+        _popupSystem.PopupEntity(Loc.GetString("cloning-pod-component-upgrade-emag-requirement"), ent.Owner);
+        args.Handled = true;
+    }
+
+    public void Eject(EntityUid uid, CloningPodComponent? clonePod)
+    {
+        if (!Resolve(uid, ref clonePod))
+            return;
+
+        if (clonePod.BodyContainer.ContainedEntity is not { Valid: true } entity || clonePod.CloningProgress < clonePod.CloningTime)
+            return;
+
+        EntityManager.RemoveComponent<BeingClonedComponent>(entity);
+        _containerSystem.Remove(entity, clonePod.BodyContainer);
+        clonePod.CloningProgress = 0f;
+        clonePod.UsedBiomass = 0;
+        UpdateStatus(uid, CloningPodStatus.Idle, clonePod);
+        RemCompDeferred<ActiveCloningPodComponent>(uid);
+    }
+
+    private void EndFailedCloning(EntityUid uid, CloningPodComponent clonePod)
+    {
+        clonePod.FailedClone = false;
+        clonePod.CloningProgress = 0f;
+        UpdateStatus(uid, CloningPodStatus.Idle, clonePod);
+        var transform = Transform(uid);
+        var indices = _transformSystem.GetGridTilePositionOrDefault((uid, transform));
+        var tileMix = _atmosphereSystem.GetTileMixture(transform.GridUid, null, indices, true);
+
+        if (HasComp<EmaggedComponent>(uid))
+        {
+            _audio.PlayPvs(clonePod.ScreamSound, uid);
+            Spawn(clonePod.MobSpawnId, transform.Coordinates);
+        }
+
+        Solution bloodSolution = new();
+
+        var i = 0;
+        while (i < 1)
+        {
+            tileMix?.AdjustMoles(Gas.Ammonia, 6f);
+            bloodSolution.AddReagent("Blood", 50);
+            if (_robustRandom.Prob(0.2f))
+                i++;
+        }
+        _puddleSystem.TrySpillAt(uid, bloodSolution, out _);
+
+        if (!HasComp<EmaggedComponent>(uid))
+        {
+            _material.SpawnMultipleFromMaterial(_robustRandom.Next(1, (int)(clonePod.UsedBiomass / 2.5)), clonePod.RequiredMaterial, Transform(uid).Coordinates);
+        }
+
+        clonePod.UsedBiomass = 0;
+        RemCompDeferred<ActiveCloningPodComponent>(uid);
+    }
+
+    public void Reset(RoundRestartCleanupEvent ev)
+    {
+        ClonesWaitingForMind.Clear();
+    }
+}
index d8aac565159988ac9b0cc564cdd93aa23d1f10fb..937b311a59f6b6121a5a5778dc2ec0042f1c706c 100644 (file)
-using Content.Server.Atmos.EntitySystems;
-using Content.Server.Chat.Systems;
-using Content.Server.Cloning.Components;
-using Content.Server.DeviceLinking.Systems;
-using Content.Server.EUI;
-using Content.Server.Fluids.EntitySystems;
 using Content.Server.Humanoid;
-using Content.Server.Jobs;
-using Content.Server.Materials;
-using Content.Server.Popups;
-using Content.Server.Power.EntitySystems;
-using Content.Shared.Atmos;
-using Content.Shared.CCVar;
-using Content.Shared.Chemistry.Components;
+using Content.Shared.Administration.Logs;
 using Content.Shared.Cloning;
-using Content.Shared.Damage;
-using Content.Shared.DeviceLinking.Events;
-using Content.Shared.Emag.Components;
-using Content.Shared.Emag.Systems;
-using Content.Shared.Examine;
-using Content.Shared.GameTicking;
+using Content.Shared.Cloning.Events;
+using Content.Shared.Database;
 using Content.Shared.Humanoid;
-using Content.Shared.Mind;
-using Content.Shared.Mind.Components;
-using Content.Shared.Mobs.Systems;
-using Content.Shared.Roles.Jobs;
-using Robust.Server.Containers;
-using Robust.Server.GameObjects;
-using Robust.Server.Player;
-using Robust.Shared.Audio.Systems;
-using Robust.Shared.Configuration;
-using Robust.Shared.Containers;
-using Robust.Shared.Physics.Components;
+using Content.Shared.Inventory;
+using Content.Shared.NameModifier.Components;
+using Content.Shared.StatusEffect;
+using Content.Shared.Whitelist;
+using Robust.Shared.Map;
 using Robust.Shared.Prototypes;
-using Robust.Shared.Random;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
 
-namespace Content.Server.Cloning
+namespace Content.Server.Cloning;
+
+/// <summary>
+///     System responsible for making a copy of a humanoid's body.
+///     For the cloning machines themselves look at CloningPodSystem, CloningConsoleSystem and MedicalScannerSystem instead.
+/// </summary>
+public sealed class CloningSystem : EntitySystem
 {
-    public sealed class CloningSystem : EntitySystem
+    [Dependency] private readonly IComponentFactory _componentFactory = default!;
+    [Dependency] private readonly HumanoidAppearanceSystem _humanoidSystem = default!;
+    [Dependency] private readonly InventorySystem _inventory = default!;
+    [Dependency] private readonly MetaDataSystem _metaData = default!;
+    [Dependency] private readonly IPrototypeManager _prototype = default!;
+    [Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
+    [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
+
+    /// <summary>
+    ///     Spawns a clone of the given humanoid mob at the specified location or in nullspace.
+    /// </summary>
+    public bool TryCloning(EntityUid original, MapCoordinates? coords, ProtoId<CloningSettingsPrototype> settingsId, [NotNullWhen(true)] out EntityUid? clone)
     {
-        [Dependency] private readonly DeviceLinkSystem _signalSystem = default!;
-        [Dependency] private readonly IPlayerManager _playerManager = null!;
-        [Dependency] private readonly IPrototypeManager _prototype = default!;
-        [Dependency] private readonly EuiManager _euiManager = null!;
-        [Dependency] private readonly CloningConsoleSystem _cloningConsoleSystem = default!;
-        [Dependency] private readonly HumanoidAppearanceSystem _humanoidSystem = default!;
-        [Dependency] private readonly ContainerSystem _containerSystem = default!;
-        [Dependency] private readonly MobStateSystem _mobStateSystem = default!;
-        [Dependency] private readonly PowerReceiverSystem _powerReceiverSystem = default!;
-        [Dependency] private readonly IRobustRandom _robustRandom = default!;
-        [Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
-        [Dependency] private readonly TransformSystem _transformSystem = default!;
-        [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
-        [Dependency] private readonly PuddleSystem _puddleSystem = default!;
-        [Dependency] private readonly ChatSystem _chatSystem = default!;
-        [Dependency] private readonly SharedAudioSystem _audio = default!;
-        [Dependency] private readonly IConfigurationManager _configManager = default!;
-        [Dependency] private readonly MaterialStorageSystem _material = default!;
-        [Dependency] private readonly PopupSystem _popupSystem = default!;
-        [Dependency] private readonly SharedMindSystem _mindSystem = default!;
-        [Dependency] private readonly MetaDataSystem _metaSystem = default!;
-        [Dependency] private readonly SharedJobSystem _jobs = default!;
-        [Dependency] private readonly EmagSystem _emag = default!;
+        clone = null;
+        if (!_prototype.TryIndex(settingsId, out var settings))
+            return false; // invalid settings
 
-        public readonly Dictionary<MindComponent, EntityUid> ClonesWaitingForMind = new();
-        public const float EasyModeCloningCost = 0.7f;
+        if (!TryComp<HumanoidAppearanceComponent>(original, out var humanoid))
+            return false; // whatever body was to be cloned, was not a humanoid
 
-        public override void Initialize()
-        {
-            base.Initialize();
+        if (!_prototype.TryIndex(humanoid.Species, out var speciesPrototype))
+            return false; // invalid species
 
-            SubscribeLocalEvent<CloningPodComponent, ComponentInit>(OnComponentInit);
-            SubscribeLocalEvent<RoundRestartCleanupEvent>(Reset);
-            SubscribeLocalEvent<BeingClonedComponent, MindAddedMessage>(HandleMindAdded);
-            SubscribeLocalEvent<CloningPodComponent, PortDisconnectedEvent>(OnPortDisconnected);
-            SubscribeLocalEvent<CloningPodComponent, AnchorStateChangedEvent>(OnAnchor);
-            SubscribeLocalEvent<CloningPodComponent, ExaminedEvent>(OnExamined);
-            SubscribeLocalEvent<CloningPodComponent, GotEmaggedEvent>(OnEmagged);
-        }
+        var attemptEv = new CloningAttemptEvent(settings);
+        RaiseLocalEvent(original, ref attemptEv);
+        if (attemptEv.Cancelled && !settings.ForceCloning)
+            return false; // cannot clone, for example due to the unrevivable trait
 
-        private void OnComponentInit(EntityUid uid, CloningPodComponent clonePod, ComponentInit args)
-        {
-            clonePod.BodyContainer = _containerSystem.EnsureContainer<ContainerSlot>(uid, "clonepod-bodyContainer");
-            _signalSystem.EnsureSinkPorts(uid, CloningPodComponent.PodPort);
-        }
+        clone = coords == null ? Spawn(speciesPrototype.Prototype) : Spawn(speciesPrototype.Prototype, coords.Value);
+        _humanoidSystem.CloneAppearance(original, clone.Value);
 
-        internal void TransferMindToClone(EntityUid mindId, MindComponent mind)
-        {
-            if (!ClonesWaitingForMind.TryGetValue(mind, out var entity) ||
-                !EntityManager.EntityExists(entity) ||
-                !TryComp<MindContainerComponent>(entity, out var mindComp) ||
-                mindComp.Mind != null)
-                return;
+        var componentsToCopy = settings.Components;
 
-            _mindSystem.TransferTo(mindId, entity, ghostCheckOverride: true, mind: mind);
-            _mindSystem.UnVisit(mindId, mind);
-            ClonesWaitingForMind.Remove(mind);
-        }
-
-        private void HandleMindAdded(EntityUid uid, BeingClonedComponent clonedComponent, MindAddedMessage message)
-        {
-            if (clonedComponent.Parent == EntityUid.Invalid ||
-                !EntityManager.EntityExists(clonedComponent.Parent) ||
-                !TryComp<CloningPodComponent>(clonedComponent.Parent, out var cloningPodComponent) ||
-                uid != cloningPodComponent.BodyContainer.ContainedEntity)
-            {
-                EntityManager.RemoveComponent<BeingClonedComponent>(uid);
-                return;
-            }
-            UpdateStatus(clonedComponent.Parent, CloningPodStatus.Cloning, cloningPodComponent);
-        }
-
-        private void OnPortDisconnected(EntityUid uid, CloningPodComponent pod, PortDisconnectedEvent args)
-        {
-            pod.ConnectedConsole = null;
-        }
-
-        private void OnAnchor(EntityUid uid, CloningPodComponent component, ref AnchorStateChangedEvent args)
-        {
-            if (component.ConnectedConsole == null || !TryComp<CloningConsoleComponent>(component.ConnectedConsole, out var console))
-                return;
-
-            if (args.Anchored)
-            {
-                _cloningConsoleSystem.RecheckConnections(component.ConnectedConsole.Value, uid, console.GeneticScanner, console);
-                return;
-            }
-            _cloningConsoleSystem.UpdateUserInterface(component.ConnectedConsole.Value, console);
-        }
+        // don't make status effects permanent
+        if (TryComp<StatusEffectsComponent>(original, out var statusComp))
+            componentsToCopy.ExceptWith(statusComp.ActiveEffects.Values.Select(s => s.RelevantComponent).Where(s => s != null)!);
 
-        private void OnExamined(EntityUid uid, CloningPodComponent component, ExaminedEvent args)
+        foreach (var componentName in componentsToCopy)
         {
-            if (!args.IsInDetailsRange || !_powerReceiverSystem.IsPowered(uid))
-                return;
-
-            args.PushMarkup(Loc.GetString("cloning-pod-biomass", ("number", _material.GetMaterialAmount(uid, component.RequiredMaterial))));
-        }
-
-        public bool TryCloning(EntityUid uid, EntityUid bodyToClone, Entity<MindComponent> mindEnt, CloningPodComponent? clonePod, float failChanceModifier = 1)
-        {
-            if (!Resolve(uid, ref clonePod))
-                return false;
-
-            if (HasComp<ActiveCloningPodComponent>(uid))
-                return false;
-
-            var mind = mindEnt.Comp;
-            if (ClonesWaitingForMind.TryGetValue(mind, out var clone))
-            {
-                if (EntityManager.EntityExists(clone) &&
-                    !_mobStateSystem.IsDead(clone) &&
-                    TryComp<MindContainerComponent>(clone, out var cloneMindComp) &&
-                    (cloneMindComp.Mind == null || cloneMindComp.Mind == mindEnt))
-                    return false; // Mind already has clone
-
-                ClonesWaitingForMind.Remove(mind);
-            }
-
-            if (mind.OwnedEntity != null && !_mobStateSystem.IsDead(mind.OwnedEntity.Value))
-                return false; // Body controlled by mind is not dead
-
-            // Yes, we still need to track down the client because we need to open the Eui
-            if (mind.UserId == null || !_playerManager.TryGetSessionById(mind.UserId.Value, out var client))
-                return false; // If we can't track down the client, we can't offer transfer. That'd be quite bad.
-
-            if (!TryComp<HumanoidAppearanceComponent>(bodyToClone, out var humanoid))
-                return false; // whatever body was to be cloned, was not a humanoid
-
-            if (!_prototype.TryIndex(humanoid.Species, out var speciesPrototype))
-                return false;
-
-            if (!TryComp<PhysicsComponent>(bodyToClone, out var physics))
-                return false;
-
-            var cloningCost = (int) Math.Round(physics.FixturesMass);
-
-            if (_configManager.GetCVar(CCVars.BiomassEasyMode))
-                cloningCost = (int) Math.Round(cloningCost * EasyModeCloningCost);
-
-            // biomass checks
-            var biomassAmount = _material.GetMaterialAmount(uid, clonePod.RequiredMaterial);
-
-            if (biomassAmount < cloningCost)
-            {
-                if (clonePod.ConnectedConsole != null)
-                    _chatSystem.TrySendInGameICMessage(clonePod.ConnectedConsole.Value, Loc.GetString("cloning-console-chat-error", ("units", cloningCost)), InGameICChatType.Speak, false);
-                return false;
-            }
-
-            _material.TryChangeMaterialAmount(uid, clonePod.RequiredMaterial, -cloningCost);
-            clonePod.UsedBiomass = cloningCost;
-            // end of biomass checks
-
-            // genetic damage checks
-            if (TryComp<DamageableComponent>(bodyToClone, out var damageable) &&
-                damageable.Damage.DamageDict.TryGetValue("Cellular", out var cellularDmg))
+            if (!_componentFactory.TryGetRegistration(componentName, out var componentRegistration))
             {
-                var chance = Math.Clamp((float) (cellularDmg / 100), 0, 1);
-                chance *= failChanceModifier;
-
-                if (cellularDmg > 0 && clonePod.ConnectedConsole != null)
-                    _chatSystem.TrySendInGameICMessage(clonePod.ConnectedConsole.Value, Loc.GetString("cloning-console-cellular-warning", ("percent", Math.Round(100 - chance * 100))), InGameICChatType.Speak, false);
-
-                if (_robustRandom.Prob(chance))
-                {
-                    UpdateStatus(uid, CloningPodStatus.Gore, clonePod);
-                    clonePod.FailedClone = true;
-                    AddComp<ActiveCloningPodComponent>(uid);
-                    return true;
-                }
+                Log.Error($"Tried to use invalid component registration for cloning: {componentName}");
+                continue;
             }
-            // end of genetic damage checks
-
-            var mob = Spawn(speciesPrototype.Prototype, _transformSystem.GetMapCoordinates(uid));
-            _humanoidSystem.CloneAppearance(bodyToClone, mob);
-
-            var ev = new CloningEvent(bodyToClone, mob);
-            RaiseLocalEvent(bodyToClone, ref ev);
 
-            if (!ev.NameHandled)
-                _metaSystem.SetEntityName(mob, MetaData(bodyToClone).EntityName);
-
-            var cloneMindReturn = EntityManager.AddComponent<BeingClonedComponent>(mob);
-            cloneMindReturn.Mind = mind;
-            cloneMindReturn.Parent = uid;
-            _containerSystem.Insert(mob, clonePod.BodyContainer);
-            ClonesWaitingForMind.Add(mind, mob);
-            UpdateStatus(uid, CloningPodStatus.NoMind, clonePod);
-            _euiManager.OpenEui(new AcceptCloningEui(mindEnt, mind, this), client);
-
-            AddComp<ActiveCloningPodComponent>(uid);
-
-            // TODO: Ideally, components like this should be components on the mind entity so this isn't necessary.
-            // Add on special job components to the mob.
-            if (_jobs.MindTryGetJob(mindEnt, out var prototype))
+            if (EntityManager.TryGetComponent(original, componentRegistration.Type, out var sourceComp)) // Does the original have this component?
             {
-                foreach (var special in prototype.Special)
-                {
-                    if (special is AddComponentSpecial)
-                        special.AfterEquip(mob);
-                }
+                if (HasComp(clone.Value, componentRegistration.Type)) // CopyComp cannot overwrite existing components
+                    RemComp(clone.Value, componentRegistration.Type);
+                CopyComp(original, clone.Value, sourceComp);
             }
-
-            return true;
         }
 
-        public void UpdateStatus(EntityUid podUid, CloningPodStatus status, CloningPodComponent cloningPod)
-        {
-            cloningPod.Status = status;
-            _appearance.SetData(podUid, CloningPodVisuals.Status, cloningPod.Status);
-        }
-
-        public override void Update(float frameTime)
-        {
-            var query = EntityQueryEnumerator<ActiveCloningPodComponent, CloningPodComponent>();
-            while (query.MoveNext(out var uid, out var _, out var cloning))
-            {
-                if (!_powerReceiverSystem.IsPowered(uid))
-                    continue;
-
-                if (cloning.BodyContainer.ContainedEntity == null && !cloning.FailedClone)
-                    continue;
+        var cloningEv = new CloningEvent(settings, clone.Value);
+        RaiseLocalEvent(original, ref cloningEv); // used for datafields that cannot be directly copied
 
-                cloning.CloningProgress += frameTime;
-                if (cloning.CloningProgress < cloning.CloningTime)
-                    continue;
-
-                if (cloning.FailedClone)
-                    EndFailedCloning(uid, cloning);
-                else
-                    Eject(uid, cloning);
-            }
-        }
-
-        /// <summary>
-        /// On emag, spawns a failed clone when cloning process fails which attacks nearby crew.
-        /// </summary>
-        private void OnEmagged(EntityUid uid, CloningPodComponent clonePod, ref GotEmaggedEvent args)
-        {
-            if (!_emag.CompareFlag(args.Type, EmagType.Interaction))
-                return;
+        // Add equipment first so that SetEntityName also renames the ID card.
+        if (settings.CopyEquipment != null)
+            CopyEquipment(original, clone.Value, settings.CopyEquipment.Value, settings.Whitelist, settings.Blacklist);
 
-            if (_emag.CheckFlag(uid, EmagType.Interaction))
-                return;
+        var originalName = Name(original);
+        if (TryComp<NameModifierComponent>(original, out var nameModComp)) // if the originals name was modified, use the unmodified name
+            originalName = nameModComp.BaseName;
 
-            if (!this.IsPowered(uid, EntityManager))
-                return;
+        // This will properly set the BaseName and EntityName for the clone.
+        // Adding the component first before renaming will make sure RefreshNameModifers is called.
+        // Without this the name would get reverted to Urist.
+        // If the clone has no name modifiers, NameModifierComponent will be removed again.
+        EnsureComp<NameModifierComponent>(clone.Value);
+        _metaData.SetEntityName(clone.Value, originalName);
 
-            _popupSystem.PopupEntity(Loc.GetString("cloning-pod-component-upgrade-emag-requirement"), uid);
-            args.Handled = true;
-        }
-
-        public void Eject(EntityUid uid, CloningPodComponent? clonePod)
-        {
-            if (!Resolve(uid, ref clonePod))
-                return;
-
-            if (clonePod.BodyContainer.ContainedEntity is not { Valid: true } entity || clonePod.CloningProgress < clonePod.CloningTime)
-                return;
-
-            EntityManager.RemoveComponent<BeingClonedComponent>(entity);
-            _containerSystem.Remove(entity, clonePod.BodyContainer);
-            clonePod.CloningProgress = 0f;
-            clonePod.UsedBiomass = 0;
-            UpdateStatus(uid, CloningPodStatus.Idle, clonePod);
-            RemCompDeferred<ActiveCloningPodComponent>(uid);
-        }
+        _adminLogger.Add(LogType.Chat, LogImpact.Medium, $"The body of {original:player} was cloned as {clone.Value:player}");
+        return true;
+    }
 
-        private void EndFailedCloning(EntityUid uid, CloningPodComponent clonePod)
+    /// <summary>
+    ///     Copies the equipment the original has to the clone.
+    ///     This uses the original prototype of the items, so any changes to components that are done after spawning are lost!
+    /// </summary>
+    public void CopyEquipment(EntityUid original, EntityUid clone, SlotFlags slotFlags, EntityWhitelist? whitelist = null, EntityWhitelist? blacklist = null)
+    {
+        if (!TryComp<InventoryComponent>(original, out var originalInventory) || !TryComp<InventoryComponent>(clone, out var cloneInventory))
+            return;
+        // Iterate over all inventory slots
+        var slotEnumerator = _inventory.GetSlotEnumerator((original, originalInventory), slotFlags);
+        while (slotEnumerator.NextItem(out var item, out var slot))
         {
-            clonePod.FailedClone = false;
-            clonePod.CloningProgress = 0f;
-            UpdateStatus(uid, CloningPodStatus.Idle, clonePod);
-            var transform = Transform(uid);
-            var indices = _transformSystem.GetGridTilePositionOrDefault((uid, transform));
-            var tileMix = _atmosphereSystem.GetTileMixture(transform.GridUid, null, indices, true);
-
-            if (_emag.CheckFlag(uid, EmagType.Interaction))
-            {
-                _audio.PlayPvs(clonePod.ScreamSound, uid);
-                Spawn(clonePod.MobSpawnId, transform.Coordinates);
-            }
-
-            Solution bloodSolution = new();
-
-            var i = 0;
-            while (i < 1)
-            {
-                tileMix?.AdjustMoles(Gas.Ammonia, 6f);
-                bloodSolution.AddReagent("Blood", 50);
-                if (_robustRandom.Prob(0.2f))
-                    i++;
-            }
-            _puddleSystem.TrySpillAt(uid, bloodSolution, out _);
+            // Spawn a copy of the item using the original prototype.
+            // This means any changes done to the item after spawning will be reset, but that should not be a problem for simple items like clothing etc.
+            // we use a whitelist and blacklist to be sure to exclude any problematic entities
 
-            if (!_emag.CheckFlag(uid, EmagType.Interaction))
-            {
-                _material.SpawnMultipleFromMaterial(_robustRandom.Next(1, (int) (clonePod.UsedBiomass / 2.5)), clonePod.RequiredMaterial, Transform(uid).Coordinates);
-            }
+            if (_whitelist.IsWhitelistFail(whitelist, item) || _whitelist.IsBlacklistPass(blacklist, item))
+                continue;
 
-            clonePod.UsedBiomass = 0;
-            RemCompDeferred<ActiveCloningPodComponent>(uid);
-        }
-
-        public void Reset(RoundRestartCleanupEvent ev)
-        {
-            ClonesWaitingForMind.Clear();
+            var prototype = MetaData(item).EntityPrototype;
+            if (prototype != null)
+                _inventory.SpawnItemInSlot(clone, slot.Name, prototype.ID, silent: true, inventory: cloneInventory);
         }
     }
 }
diff --git a/Content.Server/Cloning/Components/RandomCloneSpawnerComponent.cs b/Content.Server/Cloning/Components/RandomCloneSpawnerComponent.cs
new file mode 100644 (file)
index 0000000..ee06a53
--- /dev/null
@@ -0,0 +1,17 @@
+using Content.Shared.Cloning;
+using Robust.Shared.Prototypes;
+
+namespace Content.Server.Cloning.Components;
+
+/// <summary>
+///     This is added to a marker entity in order to spawn a clone of a random player.
+/// </summary>
+[RegisterComponent, EntityCategory("Spawner")]
+public sealed partial class RandomCloneSpawnerComponent : Component
+{
+    /// <summary>
+    ///     Cloning settings to be used.
+    /// </summary>
+    [DataField]
+    public ProtoId<CloningSettingsPrototype> Settings = "BaseClone";
+}
diff --git a/Content.Server/Cloning/RandomCloneSpawnerSystem.cs b/Content.Server/Cloning/RandomCloneSpawnerSystem.cs
new file mode 100644 (file)
index 0000000..a645a10
--- /dev/null
@@ -0,0 +1,47 @@
+using Content.Server.Cloning.Components;
+using Content.Shared.Mind;
+using Content.Shared.Mobs.Systems;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+
+namespace Content.Server.Cloning;
+
+/// <summary>
+///     This deals with spawning and setting up a clone of a random crew member.
+/// </summary>
+public sealed class RandomCloneSpawnerSystem : EntitySystem
+{
+    [Dependency] private readonly CloningSystem _cloning = default!;
+    [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+    [Dependency] private readonly IRobustRandom _random = default!;
+    [Dependency] private readonly SharedTransformSystem _transformSystem = default!;
+    [Dependency] private readonly SharedMindSystem _mind = default!;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<RandomCloneSpawnerComponent, MapInitEvent>(OnMapInit);
+    }
+
+    private void OnMapInit(Entity<RandomCloneSpawnerComponent> ent, ref MapInitEvent args)
+    {
+        QueueDel(ent.Owner);
+
+        if (!_prototypeManager.TryIndex(ent.Comp.Settings, out var settings))
+        {
+            Log.Error($"Used invalid cloning settings {ent.Comp.Settings} for RandomCloneSpawner");
+            return;
+        }
+
+        var allHumans = _mind.GetAliveHumans();
+
+        if (allHumans.Count == 0)
+            return;
+
+        var bodyToClone = _random.Pick(allHumans).Comp.OwnedEntity;
+
+        if (bodyToClone != null)
+            _cloning.TryCloning(bodyToClone.Value, _transformSystem.GetMapCoordinates(ent.Owner), settings, out _);
+    }
+}
index f811bede7b65718e2c6132d2e6fc2a859cd5101e..a52b06039135be1153b96e591ecce2b572b228a4 100644 (file)
@@ -1,4 +1,5 @@
 using Content.Server.Body.Components;
+using Content.Server.Body.Systems;
 using Content.Server.DoAfter;
 using Content.Server.Fluids.EntitySystems;
 using Content.Server.Forensics.Components;
@@ -32,8 +33,9 @@ namespace Content.Server.Forensics
         public override void Initialize()
         {
             SubscribeLocalEvent<FingerprintComponent, ContactInteractionEvent>(OnInteract);
-            SubscribeLocalEvent<FingerprintComponent, MapInitEvent>(OnFingerprintInit);
-            SubscribeLocalEvent<DnaComponent, MapInitEvent>(OnDNAInit);
+            SubscribeLocalEvent<FingerprintComponent, MapInitEvent>(OnFingerprintInit, after: new[] { typeof(BloodstreamSystem) });
+            // The solution entities are spawned on MapInit as well, so we have to wait for that to be able to set the DNA in the bloodstream correctly without ResolveSolution failing
+            SubscribeLocalEvent<DnaComponent, MapInitEvent>(OnDNAInit, after: new[] { typeof(BloodstreamSystem) });
 
             SubscribeLocalEvent<ForensicsComponent, BeingGibbedEvent>(OnBeingGibbed);
             SubscribeLocalEvent<ForensicsComponent, MeleeHitEvent>(OnMeleeHit);
@@ -65,18 +67,20 @@ namespace Content.Server.Forensics
 
         private void OnFingerprintInit(Entity<FingerprintComponent> ent, ref MapInitEvent args)
         {
-            ent.Comp.Fingerprint = GenerateFingerprint();
-            Dirty(ent);
+            if (ent.Comp.Fingerprint == null)
+                RandomizeFingerprint((ent.Owner, ent.Comp));
         }
 
-        private void OnDNAInit(EntityUid uid, DnaComponent component, MapInitEvent args)
+        private void OnDNAInit(Entity<DnaComponent> ent, ref MapInitEvent args)
         {
-            if (component.DNA == String.Empty)
+            Log.Debug($"Init DNA {Name(ent.Owner)} {ent.Comp.DNA}");
+            if (ent.Comp.DNA == null)
+                RandomizeDNA((ent.Owner, ent.Comp));
+            else
             {
-                component.DNA = GenerateDNA();
-
-                var ev = new GenerateDnaEvent { Owner = uid, DNA = component.DNA };
-                RaiseLocalEvent(uid, ref ev);
+                // If set manually (for example by cloning) we also need to inform the bloodstream of the correct DNA string so it can be updated
+                var ev = new GenerateDnaEvent { Owner = ent.Owner, DNA = ent.Comp.DNA };
+                RaiseLocalEvent(ent.Owner, ref ev);
             }
         }
 
@@ -84,7 +88,7 @@ namespace Content.Server.Forensics
         {
             string dna = Loc.GetString("forensics-dna-unknown");
 
-            if (TryComp(uid, out DnaComponent? dnaComp))
+            if (TryComp(uid, out DnaComponent? dnaComp) && dnaComp.DNA != null)
                 dna = dnaComp.DNA;
 
             foreach (EntityUid part in args.GibbedParts)
@@ -103,7 +107,7 @@ namespace Content.Server.Forensics
             {
                 foreach (EntityUid hitEntity in args.HitEntities)
                 {
-                    if (TryComp<DnaComponent>(hitEntity, out var hitEntityComp))
+                    if (TryComp<DnaComponent>(hitEntity, out var hitEntityComp) && hitEntityComp.DNA != null)
                         component.DNAs.Add(hitEntityComp.DNA);
                 }
             }
@@ -301,6 +305,9 @@ namespace Content.Server.Forensics
 
         private void OnTransferDnaEvent(EntityUid uid, DnaComponent component, ref TransferDnaEvent args)
         {
+            if (component.DNA == null)
+                return;
+
             var recipientComp = EnsureComp<ForensicsComponent>(args.Recipient);
             recipientComp.DNAs.Add(component.DNA);
             recipientComp.CanDnaBeCleaned = args.CanDnaBeCleaned;
@@ -308,6 +315,36 @@ namespace Content.Server.Forensics
 
         #region Public API
 
+        /// <summary>
+        /// Give the entity a new, random DNA string and call an event to notify other systems like the bloodstream that it has been changed.
+        /// Does nothing if it does not have the DnaComponent.
+        /// </summary>
+        public void RandomizeDNA(Entity<DnaComponent?> ent)
+        {
+            if (!Resolve(ent, ref ent.Comp, false))
+                return;
+
+            ent.Comp.DNA = GenerateDNA();
+            Dirty(ent);
+
+            Log.Debug($"Randomize DNA {Name(ent.Owner)} {ent.Comp.DNA}");
+            var ev = new GenerateDnaEvent { Owner = ent.Owner, DNA = ent.Comp.DNA };
+            RaiseLocalEvent(ent.Owner, ref ev);
+        }
+
+        /// <summary>
+        /// Give the entity a new, random fingerprint string.
+        /// Does nothing if it does not have the FingerprintComponent.
+        /// </summary>
+        public void RandomizeFingerprint(Entity<FingerprintComponent?> ent)
+        {
+            if (!Resolve(ent, ref ent.Comp, false))
+                return;
+
+            ent.Comp.Fingerprint = GenerateFingerprint();
+            Dirty(ent);
+        }
+
         /// <summary>
         /// Transfer DNA from one entity onto the forensics of another
         /// </summary>
@@ -316,7 +353,7 @@ namespace Content.Server.Forensics
         /// <param name="canDnaBeCleaned">If this DNA be cleaned off of the recipient. e.g. cleaning a knife vs cleaning a puddle of blood</param>
         public void TransferDna(EntityUid recipient, EntityUid donor, bool canDnaBeCleaned = true)
         {
-            if (TryComp<DnaComponent>(donor, out var donorComp))
+            if (TryComp<DnaComponent>(donor, out var donorComp) && donorComp.DNA != null)
             {
                 EnsureComp<ForensicsComponent>(recipient, out var recipientComp);
                 recipientComp.DNAs.Add(donorComp.DNA);
index c306e406a1b8972c0ee52c5f7c4f1383feb50779..bd6ffe375c44bdeff800bfb4e86759e836584f92 100644 (file)
@@ -216,18 +216,12 @@ public sealed class SubdermalImplantSystem : SharedSubdermalImplantSystem
             var newProfile = HumanoidCharacterProfile.RandomWithSpecies(humanoid.Species);
             _humanoidAppearance.LoadProfile(ent, newProfile, humanoid);
             _metaData.SetEntityName(ent, newProfile.Name, raiseEvents: false); // raising events would update ID card, station record, etc.
-            if (TryComp<DnaComponent>(ent, out var dna))
-            {
-                dna.DNA = _forensicsSystem.GenerateDNA();
 
-                var ev = new GenerateDnaEvent { Owner = ent, DNA = dna.DNA };
-                RaiseLocalEvent(ent, ref ev);
-            }
-            if (TryComp<FingerprintComponent>(ent, out var fingerprint))
-            {
-                fingerprint.Fingerprint = _forensicsSystem.GenerateFingerprint();
-            }
-            RemComp<DetailExaminableComponent>(ent); // remove MRP+ custom description if one exists 
+            // If the entity has the respecive components, then scramble the dna and fingerprint strings
+            _forensicsSystem.RandomizeDNA(ent);
+            _forensicsSystem.RandomizeFingerprint(ent);
+
+            RemComp<DetailExaminableComponent>(ent); // remove MRP+ custom description if one exists
             _identity.QueueIdentityUpdate(ent); // manually queue identity update since we don't raise the event
             _popup.PopupEntity(Loc.GetString("scramble-implant-activated-popup"), ent, ent);
         }
index 897cd061f426398dfad0b5af64e062c368172774..d55c6e6764d25c7d7bfad70225d0fa59b1e3da0a 100644 (file)
@@ -14,6 +14,8 @@ public sealed class AddAccentClothingSystem : EntitySystem
         SubscribeLocalEvent<AddAccentClothingComponent, ClothingGotUnequippedEvent>(OnGotUnequipped);
     }
 
+
+//  TODO: Turn this into a relay event.
     private void OnGotEquipped(EntityUid uid, AddAccentClothingComponent component, ref ClothingGotEquippedEvent args)
     {
         // does the user already has this accent?
diff --git a/Content.Server/Traits/Assorted/UnrevivableSystem.cs b/Content.Server/Traits/Assorted/UnrevivableSystem.cs
new file mode 100644 (file)
index 0000000..c2c8ee9
--- /dev/null
@@ -0,0 +1,20 @@
+using Content.Shared.Cloning.Events;
+using Content.Shared.Traits.Assorted;
+
+namespace Content.Server.Traits.Assorted;
+
+public sealed class UnrevivableSystem : EntitySystem
+{
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<UnrevivableComponent, CloningAttemptEvent>(OnCloningAttempt);
+    }
+
+    private void OnCloningAttempt(Entity<UnrevivableComponent> ent, ref CloningAttemptEvent args)
+    {
+        if (!ent.Comp.Cloneable)
+            args.Cancelled = true;
+    }
+}
index 82c9e2dacce838563cbb2eb4e8743da475807e80..b393850497053f4db362b58bb8a91d6211b457fd 100644 (file)
@@ -24,11 +24,11 @@ using Content.Shared.Mobs;
 using Content.Shared.Mobs.Components;
 using Content.Shared.Movement.Pulling.Components;
 using Content.Shared.Movement.Systems;
+using Content.Shared.NameModifier.EntitySystems;
 using Content.Shared.NPC.Systems;
 using Content.Shared.Nutrition.AnimalHusbandry;
 using Content.Shared.Nutrition.Components;
 using Content.Shared.Popups;
-using Content.Shared.Roles;
 using Content.Shared.Weapons.Melee;
 using Content.Shared.Zombies;
 using Content.Shared.Prying.Components;
@@ -58,8 +58,8 @@ public sealed partial class ZombieSystem
     [Dependency] private readonly MindSystem _mind = default!;
     [Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifier = default!;
     [Dependency] private readonly NPCSystem _npc = default!;
-    [Dependency] private readonly SharedRoleSystem _roles = default!;
     [Dependency] private readonly TagSystem _tag = default!;
+    [Dependency] private readonly NameModifierSystem _nameMod = default!;
 
     /// <summary>
     /// Handles an entity turning into a zombie when they die or go into crit
@@ -235,7 +235,7 @@ public sealed partial class ZombieSystem
         if (hasMind && _mind.TryGetSession(mindId, out var session))
         {
             //Zombie role for player manifest
-            _roles.MindAddRole(mindId, "MindRoleZombie", mind: null, silent: true);
+            _role.MindAddRole(mindId, "MindRoleZombie", mind: null, silent: true);
 
             //Greeting message for new bebe zombers
             _chatMan.DispatchServerMessage(session, Loc.GetString("zombie-infection-greeting"));
index ec50e11a0caf9b43da18cb00447f2099d0fba752..aa5c2682bc7ef6bc6799e7bf2c24e32271f00d72 100644 (file)
@@ -5,18 +5,20 @@ using Content.Server.Chat;
 using Content.Server.Chat.Systems;
 using Content.Server.Emoting.Systems;
 using Content.Server.Speech.EntitySystems;
+using Content.Server.Roles;
 using Content.Shared.Anomaly.Components;
 using Content.Shared.Bed.Sleep;
-using Content.Shared.Cloning;
+using Content.Shared.Cloning.Events;
 using Content.Shared.Damage;
 using Content.Shared.Humanoid;
 using Content.Shared.Inventory;
 using Content.Shared.Mind;
+using Content.Shared.Mind.Components;
 using Content.Shared.Mobs;
 using Content.Shared.Mobs.Components;
 using Content.Shared.Mobs.Systems;
-using Content.Shared.NameModifier.EntitySystems;
 using Content.Shared.Popups;
+using Content.Shared.Roles;
 using Content.Shared.Weapons.Melee.Events;
 using Content.Shared.Zombies;
 using Robust.Shared.Prototypes;
@@ -38,7 +40,7 @@ namespace Content.Server.Zombies
         [Dependency] private readonly EmoteOnDamageSystem _emoteOnDamage = default!;
         [Dependency] private readonly MobStateSystem _mobState = default!;
         [Dependency] private readonly SharedPopupSystem _popup = default!;
-        [Dependency] private readonly NameModifierSystem _nameMod = default!;
+        [Dependency] private readonly SharedRoleSystem _role = default!;
 
         public const SlotFlags ProtectiveSlots =
             SlotFlags.FEET |
@@ -63,6 +65,8 @@ namespace Content.Server.Zombies
             SubscribeLocalEvent<ZombieComponent, CloningEvent>(OnZombieCloning);
             SubscribeLocalEvent<ZombieComponent, TryingToSleepEvent>(OnSleepAttempt);
             SubscribeLocalEvent<ZombieComponent, GetCharactedDeadIcEvent>(OnGetCharacterDeadIC);
+            SubscribeLocalEvent<ZombieComponent, MindAddedMessage>(OnMindAdded);
+            SubscribeLocalEvent<ZombieComponent, MindRemovedMessage>(OnMindRemoved);
 
             SubscribeLocalEvent<PendingZombieComponent, MapInitEvent>(OnPendingMapInit);
             SubscribeLocalEvent<PendingZombieComponent, BeforeRemoveAnomalyOnDeathEvent>(OnBeforeRemoveAnomalyOnDeath);
@@ -272,7 +276,7 @@ namespace Content.Server.Zombies
         /// <param name="target">the entity you want to unzombify (different from source in case of cloning, for example)</param>
         /// <param name="zombiecomp"></param>
         /// <remarks>
-        ///     this currently only restore the name and skin/eye color from before zombified
+        ///     this currently only restore the skin/eye color from before zombified
         ///     TODO: completely rethink how zombies are done to allow reversal.
         /// </remarks>
         public bool UnZombify(EntityUid source, EntityUid target, ZombieComponent? zombiecomp)
@@ -292,14 +296,25 @@ namespace Content.Server.Zombies
             _humanoidAppearance.SetSkinColor(target, zombiecomp.BeforeZombifiedSkinColor, false);
             _bloodstream.ChangeBloodReagent(target, zombiecomp.BeforeZombifiedBloodReagent);
 
-            _nameMod.RefreshNameModifiers(target);
             return true;
         }
 
-        private void OnZombieCloning(EntityUid uid, ZombieComponent zombiecomp, ref CloningEvent args)
+        private void OnZombieCloning(Entity<ZombieComponent> ent, ref CloningEvent args)
         {
-            if (UnZombify(args.Source, args.Target, zombiecomp))
-                args.NameHandled = true;
+            UnZombify(ent.Owner, args.CloneUid, ent.Comp);
+        }
+
+        // Make sure players that enter a zombie (for example via a ghost role or the mind swap spell) count as an antagonist.
+        private void OnMindAdded(Entity<ZombieComponent> ent, ref MindAddedMessage args)
+        {
+            if (!_role.MindHasRole<ZombieRoleComponent>(args.Mind))
+                _role.MindAddRole(args.Mind, "MindRoleZombie", mind: args.Mind.Comp);
+        }
+
+        // Remove the role when getting cloned, getting gibbed and borged, or leaving the body via any other method.
+        private void OnMindRemoved(Entity<ZombieComponent> ent, ref MindRemovedMessage args)
+        {
+            _role.MindTryRemoveRole<ZombieRoleComponent>(args.Mind);
         }
     }
 }
diff --git a/Content.Shared/Cloning/CloningEvents.cs b/Content.Shared/Cloning/CloningEvents.cs
new file mode 100644 (file)
index 0000000..bd66454
--- /dev/null
@@ -0,0 +1,13 @@
+namespace Content.Shared.Cloning.Events;
+
+/// <summary>
+///    Raised before a mob is cloned. Cancel to prevent cloning.
+/// </summary>
+[ByRefEvent]
+public record struct CloningAttemptEvent(CloningSettingsPrototype Settings, bool Cancelled = false);
+
+/// <summary>
+///    Raised after a new mob got spawned when cloning a humanoid.
+/// </summary>
+[ByRefEvent]
+public record struct CloningEvent(CloningSettingsPrototype Settings, EntityUid CloneUid);
index d588b62eb37f0bdf705cc8e5f50c15b43b0d8c7d..17f733c8f369a1ef101c6ff544616a9b49e58e81 100644 (file)
@@ -10,8 +10,8 @@ namespace Content.Shared.Cloning;
 [RegisterComponent]
 public sealed partial class CloningPodComponent : Component
 {
-    [ValidatePrototypeId<SinkPortPrototype>]
-    public const string PodPort = "CloningPodReceiver";
+    [DataField]
+    public ProtoId<SinkPortPrototype> PodPort = "CloningPodReceiver";
 
     [ViewVariables]
     public ContainerSlot BodyContainer = default!;
@@ -31,23 +31,25 @@ public sealed partial class CloningPodComponent : Component
     /// <summary>
     /// The material that is used to clone entities.
     /// </summary>
-    [DataField("requiredMaterial"), ViewVariables(VVAccess.ReadWrite)]
+    [DataField]
     public ProtoId<MaterialPrototype> RequiredMaterial = "Biomass";
 
     /// <summary>
-    /// The current amount of time it takes to clone a body
+    /// The current amount of time it takes to clone a body.
     /// </summary>
-    [DataField, ViewVariables(VVAccess.ReadWrite)]
+    [DataField]
     public float CloningTime = 30f;
 
     /// <summary>
-    /// The mob to spawn on emag
+    /// The mob to spawn on emag.
     /// </summary>
-    [DataField("mobSpawnId"), ViewVariables(VVAccess.ReadWrite)]
+    [DataField]
     public EntProtoId MobSpawnId = "MobAbomination";
 
-    // TODO: Remove this from here when cloning and/or zombies are refactored
-    [DataField("screamSound")]
+    /// <summary>
+    /// The sound played when a mob is spawned from an emagged cloning pod.
+    /// </summary>
+    [DataField]
     public SoundSpecifier ScreamSound = new SoundCollectionSpecifier("ZombieScreams")
     {
         Params = AudioParams.Default.WithVolume(4),
@@ -74,21 +76,3 @@ public enum CloningPodStatus : byte
     Gore,
     NoMind
 }
-
-/// <summary>
-/// Raised after a new mob got spawned when cloning a humanoid
-/// </summary>
-[ByRefEvent]
-public struct CloningEvent
-{
-    public bool NameHandled = false;
-
-    public readonly EntityUid Source;
-    public readonly EntityUid Target;
-
-    public CloningEvent(EntityUid source, EntityUid target)
-    {
-        Source = source;
-        Target = target;
-    }
-}
diff --git a/Content.Shared/Cloning/CloningSettingsPrototype.cs b/Content.Shared/Cloning/CloningSettingsPrototype.cs
new file mode 100644 (file)
index 0000000..3828e6c
--- /dev/null
@@ -0,0 +1,60 @@
+using Content.Shared.Inventory;
+using Content.Shared.Whitelist;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Array;
+
+namespace Content.Shared.Cloning;
+
+/// <summary>
+///     Settings for cloning a humanoid.
+///     Used to decide which components should be copied.
+/// </summary>
+[Prototype]
+public sealed partial class CloningSettingsPrototype : IPrototype, IInheritingPrototype
+{
+    /// <inheritdoc/>
+    [IdDataField]
+    public string ID { get; private set; } = default!;
+
+    [ParentDataField(typeof(PrototypeIdArraySerializer<CloningSettingsPrototype>))]
+    public string[]? Parents { get; }
+
+    [AbstractDataField]
+    [NeverPushInheritance]
+    public bool Abstract { get; }
+
+    /// <summary>
+    ///     Determines if cloning can be prevented by traits etc.
+    /// </summary>
+    [DataField]
+    public bool ForceCloning = true;
+
+    /// <summary>
+    ///     Which inventory slots will receive a copy of the original's clothing.
+    ///     Disabled when null.
+    /// </summary>
+    [DataField]
+    public SlotFlags? CopyEquipment = SlotFlags.WITHOUT_POCKET;
+
+    /// <summary>
+    ///     Whitelist for the equipment allowed to be copied.
+    /// </summary>
+    [DataField]
+    public EntityWhitelist? Whitelist;
+
+    /// <summary>
+    ///     Blacklist for the equipment allowed to be copied.
+    /// </summary>
+    [DataField]
+    public EntityWhitelist? Blacklist;
+
+    /// TODO: Make this not a string https://github.com/space-wizards/RobustToolbox/issues/5709
+    /// <summary>
+    ///     Components to copy from the original to the clone.
+    ///     This only makes a shallow copy of datafields!
+    ///     If you need a deep copy or additional component initialization, then subscribe to CloningEvent instead!
+    /// </summary>
+    [DataField]
+    [AlwaysPushInheritance]
+    public HashSet<string> Components = new();
+}
index 0dfa92146b36d823c0edf66619b2ceb07dcd884a..a9d2f7ea8b8d7757f9b8bb6027a36a9f24362027 100644 (file)
@@ -9,5 +9,5 @@ namespace Content.Shared.Forensics.Components;
 public sealed partial class DnaComponent : Component
 {
     [DataField("dna"), AutoNetworkedField]
-    public string DNA = String.Empty;
+    public string? DNA;
 }
index c346d08536d754b25b23f19eb0e045d80150845b..0506f48a3d5572f795134107bcce567a43c62dc8 100644 (file)
@@ -53,7 +53,7 @@ public record struct TransferDnaEvent()
 }
 
 /// <summary>
-/// An event to generate and act upon new DNA for an entity.
+/// Raised on an entity when its DNA has been changed.
 /// </summary>
 [ByRefEvent]
 public record struct GenerateDnaEvent()
index 19b4cac5e0d4be2fdb11953d04507b68e82bbd87..44af27f5ea3db710e33d7620d94a7e59793a555b 100644 (file)
@@ -14,6 +14,12 @@ public sealed partial class UnrevivableComponent : Component
     [DataField, AutoNetworkedField]
     public bool Analyzable = true;
 
+    /// <summary>
+    /// Can this player be cloned using a cloning pod?
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public bool Cloneable = false;
+
     /// <summary>
     /// The loc string used to provide a reason for being unrevivable
     /// </summary>
index b801f9258057694de5cc379606054a2077b0714f..c01cc8b5c6a4d081aa4c1a4c478d413ff460e25f 100644 (file)
@@ -26,5 +26,5 @@ cloning-console-component-msg-no-cloner = Not Ready: No Cloner Detected
 cloning-console-component-msg-no-mind = Not Ready: No Soul Activity Detected
 
 cloning-console-chat-error = ERROR: INSUFFICIENT BIOMASS. CLONING THIS BODY REQUIRES {$units} UNITS OF BIOMASS.
-cloning-console-uncloneable-trait-error = ERROR: SOUL IS ABSENT, CLONING IS IMPOSSIBLE.
+cloning-console-uncloneable-trait-error = ERROR: CLONING IS IMPOSSIBLE DUE TO ABNORMAL BODY COMPOSITION.
 cloning-console-cellular-warning = WARNING: GENEFSCK CONFIDENCE SCORE IS {$percent}%. CLONING MAY HAVE UNEXPECTED RESULTS.
index 5c327347268493a5823824b33bdba7c63c342051..adbf4df134f18c9a7179a01d3d1e050818c8de6c 100644 (file)
   id: CloningPodSender
   name: signal-port-name-pod-receiver
   description: signal-port-description-pod-sender
+  defaultLinks: [ CloningPodReceiver ]
 
 - type: sourcePort
   id: MedicalScannerSender
   name: signal-port-name-med-scanner-sender
   description: signal-port-description-med-scanner-sender
+  defaultLinks: [ MedicalScannerReceiver ]
 
 - type: sourcePort
   id: ArtifactAnalyzerSender
diff --git a/Resources/Prototypes/Entities/Mobs/Player/clone.yml b/Resources/Prototypes/Entities/Mobs/Player/clone.yml
new file mode 100644 (file)
index 0000000..64ae4f9
--- /dev/null
@@ -0,0 +1,84 @@
+# Settings for cloning bodies
+# If you add a new trait, job specific component or a component doing visual/examination changes for humanoids
+# then add it here to the correct prototype.
+# The datafields of the components are only shallow copied using CopyComp.
+# Subscribe to CloningEvent instead if that is not enough.
+
+- type: cloningSettings
+  id: BaseClone
+  components:
+  # general
+  - DetailExaminable
+  - Dna
+  - Fingerprint
+  - NpcFactionMember
+  # traits
+  # - LegsParalyzed (you get healed)
+  - LightweightDrunk
+  - Narcolepsy
+  - Pacified
+  - PainNumbness
+  - Paracusia
+  - PermanentBlindness
+  - Unrevivable
+  # job specific
+  - BibleUser
+  - CommandStaff
+  - Clumsy
+  - MindShield
+  - MimePowers
+  # accents
+  - Accentless
+  - BackwardsAccent
+  - BarkAccent
+  - BleatingAccent
+  - FrenchAccent
+  - GermanAccent
+  - LizardAccent
+  - MobsterAccent
+  - MonkeyAccent
+  - MothAccent
+  - MumbleAccent
+  - OwOAccent
+  - ParrotAccent
+  - PirateAccent
+  # - ReplacementAccent
+  # Not supported at the moment because AddAccentClothingComponent will make it permanent when cloned.
+  # TODO: AddAccentClothingComponent should use an inventory relay event.
+  # Also ZombieComponent overwrites the old replacement accent, because you can only have one at a time.
+  - RussianAccent
+  - ScrambledAccent
+  - SkeletonAccent
+  - SlurredAccent
+  - SouthernAccent
+  - SpanishAccent
+  - StutteringAccent
+  blacklist:
+    components:
+    - AttachedClothing # helmets, which are part of the suit
+
+- type: cloningSettings
+  id: Antag
+  parent: BaseClone
+  components:
+  - HeadRevolutionary
+  - Revolutionary
+
+- type: cloningSettings
+  id: CloningPod
+  parent: Antag
+  forceCloning: false
+  copyEquipment: null
+
+# spawner
+
+- type: entity
+  id: RandomCloneSpawner
+  name: Random Clone
+  suffix: Non-Antag
+  components:
+  - type: Sprite
+    sprite: Markers/paradox_clone.rsi
+    state: preview
+  - type: RandomCloneSpawner
+    settings: BaseClone
index 6ce9c80a250f82467cf4ec4f18983c46f91aa5ed..e3f0e5a5c125d354c7f0134d43b2e206c903c84b 100644 (file)
@@ -8,7 +8,7 @@
   - type: Hunger
   - type: Thirst
   - type: Icon
-    sprite: Mobs/Species/Slime/parts.rsi # It was like this beforehand, no idea why.
+    sprite: Mobs/Species/Human/parts.rsi
     state: full
   - type: Respirator
     damage:
@@ -52,7 +52,7 @@
   - type: Speech
     speechSounds: Bass
   - type: HumanoidAppearance
-    species: Human
+    species: Dwarf
     hideLayersOnEquip:
     - Hair
     - Snout
diff --git a/Resources/Textures/Markers/paradox_clone.rsi/meta.json b/Resources/Textures/Markers/paradox_clone.rsi/meta.json
new file mode 100644 (file)
index 0000000..e586347
--- /dev/null
@@ -0,0 +1,14 @@
+{
+  "version": 1,
+  "license": "CC-BY-SA-3.0",
+  "copyright": "preview combined from Mobs/Species/Human/parts.rsi, Clothing/Uniforms/Jumpsuit/janitor.rsi, Clothing/Shoes/Specific/galoshes.rsi, Clothing/Belt/janitor.rsi, Clothing/Hands/Gloves/janitor.rsi and Clothing/Head/Soft/purplesoft.rsi by slarticodefast",
+  "size": {
+    "x": 32,
+    "y": 32
+  },
+  "states": [
+    {
+      "name": "preview"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/Resources/Textures/Markers/paradox_clone.rsi/preview.png b/Resources/Textures/Markers/paradox_clone.rsi/preview.png
new file mode 100644 (file)
index 0000000..8b83969
Binary files /dev/null and b/Resources/Textures/Markers/paradox_clone.rsi/preview.png differ