]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Paradox clones get all storage items the original has. (#35838)
authorslarticodefast <161409025+slarticodefast@users.noreply.github.com>
Sun, 16 Mar 2025 22:02:19 +0000 (23:02 +0100)
committerGitHub <noreply@github.com>
Sun, 16 Mar 2025 22:02:19 +0000 (15:02 -0700)
* recursive storage copying

* include slime storage

* future proofing

* remove survival box

Content.Server/Cloning/CloningSystem.cs
Content.Shared/Cloning/CloningSettingsPrototype.cs
Resources/Locale/en-US/preferences/loadout-groups.ftl
Resources/Prototypes/Entities/Mobs/Player/clone.yml
Resources/Prototypes/GameRules/events.yml
Resources/Prototypes/Loadouts/loadout_groups.yml
Resources/Prototypes/Loadouts/role_loadouts.yml

index 937b311a59f6b6121a5a5778dc2ec0042f1c706c..9a77c75f6455f2844898cd897cb6ab9ad9749d5b 100644 (file)
@@ -7,7 +7,11 @@ using Content.Shared.Humanoid;
 using Content.Shared.Inventory;
 using Content.Shared.NameModifier.Components;
 using Content.Shared.StatusEffect;
+using Content.Shared.Stacks;
+using Content.Shared.Storage;
+using Content.Shared.Storage.EntitySystems;
 using Content.Shared.Whitelist;
+using Robust.Shared.Containers;
 using Robust.Shared.Map;
 using Robust.Shared.Prototypes;
 using System.Diagnostics.CodeAnalysis;
@@ -28,6 +32,9 @@ public sealed class CloningSystem : EntitySystem
     [Dependency] private readonly IPrototypeManager _prototype = default!;
     [Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
     [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
+    [Dependency] private readonly SharedContainerSystem _container = default!;
+    [Dependency] private readonly SharedStorageSystem _storage = default!;
+    [Dependency] private readonly SharedStackSystem _stack = default!;
 
     /// <summary>
     ///     Spawns a clone of the given humanoid mob at the specified location or in nullspace.
@@ -81,6 +88,10 @@ public sealed class CloningSystem : EntitySystem
         if (settings.CopyEquipment != null)
             CopyEquipment(original, clone.Value, settings.CopyEquipment.Value, settings.Whitelist, settings.Blacklist);
 
+        // Copy storage on the mob itself as well.
+        // This is needed for slime storage.
+        CopyStorage(original, clone.Value, settings.Whitelist, settings.Blacklist);
+
         var originalName = Name(original);
         if (TryComp<NameModifierComponent>(original, out var nameModComp)) // if the originals name was modified, use the unmodified name
             originalName = nameModComp.BaseName;
@@ -100,24 +111,89 @@ public sealed class CloningSystem : EntitySystem
     ///     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)
+    public void CopyEquipment(Entity<InventoryComponent?> original, Entity<InventoryComponent?> clone, SlotFlags slotFlags, EntityWhitelist? whitelist = null, EntityWhitelist? blacklist = null)
     {
-        if (!TryComp<InventoryComponent>(original, out var originalInventory) || !TryComp<InventoryComponent>(clone, out var cloneInventory))
+        if (!Resolve(original, ref original.Comp) || !Resolve(clone, ref clone.Comp))
             return;
+
+        var coords = Transform(clone).Coordinates;
+
         // Iterate over all inventory slots
-        var slotEnumerator = _inventory.GetSlotEnumerator((original, originalInventory), slotFlags);
+        var slotEnumerator = _inventory.GetSlotEnumerator(original, slotFlags);
         while (slotEnumerator.NextItem(out var item, out var slot))
         {
-            // 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
+            var cloneItem = CopyItem(item, coords, whitelist, blacklist);
 
-            if (_whitelist.IsWhitelistFail(whitelist, item) || _whitelist.IsBlacklistPass(blacklist, item))
-                continue;
+            if (cloneItem != null && !_inventory.TryEquip(clone, cloneItem.Value, slot.Name, silent: true, inventory: clone.Comp))
+                Del(cloneItem); // delete it again if the clone cannot equip it
+        }
+    }
+
+    /// <summary>
+    ///     Copies an item and its storage recursively, placing all items at the same position in grid storage.
+    ///     This uses the original prototype of the items, so any changes to components that are done after spawning are lost!
+    /// </summary>
+    /// <remarks>
+    ///     This is not perfect and only considers item in storage containers.
+    ///     Some components have their own additional spawn logic on map init, so we cannot just copy all containers.
+    /// </remarks>
+    public EntityUid? CopyItem(EntityUid original, EntityCoordinates coords, EntityWhitelist? whitelist = null, EntityWhitelist? blacklist = null)
+    {
+        // we use a whitelist and blacklist to be sure to exclude any problematic entities
+        if (!_whitelist.CheckBoth(original, blacklist, whitelist))
+            return null;
+
+        var prototype = MetaData(original).EntityPrototype?.ID;
+        if (prototype == null)
+            return null;
+
+        var spawned = EntityManager.SpawnAtPosition(prototype, coords);
+
+        // if the original is a stack, adjust the count of the copy
+        if (TryComp<StackComponent>(original, out var originalStack) && TryComp<StackComponent>(spawned, out var spawnedStack))
+            _stack.SetCount(spawned, originalStack.Count, spawnedStack);
+
+        // if the original has items inside its storage, copy those as well
+        if (TryComp<StorageComponent>(original, out var originalStorage) && TryComp<StorageComponent>(spawned, out var spawnedStorage))
+        {
+            // remove all items that spawned with the entity inside its storage
+            // this ignores other containers, but this should be good enough for our purposes
+            _container.CleanContainer(spawnedStorage.Container);
+
+            // recursively replace them
+            // surely no one will ever create two items that contain each other causing an infinite loop, right?
+            foreach ((var itemUid, var itemLocation) in originalStorage.StoredItems)
+            {
+                var copy = CopyItem(itemUid, coords, whitelist, blacklist);
+                if (copy != null)
+                    _storage.InsertAt((spawned, spawnedStorage), copy.Value, itemLocation, out _, playSound: false);
+            }
+        }
+
+        return spawned;
+    }
 
-            var prototype = MetaData(item).EntityPrototype;
-            if (prototype != null)
-                _inventory.SpawnItemInSlot(clone, slot.Name, prototype.ID, silent: true, inventory: cloneInventory);
+    /// <summary>
+    ///     Copies an item's storage recursively to another storage.
+    ///     The storage grids should have the same shape or it will drop on the floor.
+    ///     Basically the same as CopyItem, but we don't copy the outermost container.
+    /// </summary>
+    public void CopyStorage(Entity<StorageComponent?> original, Entity<StorageComponent?> target, EntityWhitelist? whitelist = null, EntityWhitelist? blacklist = null)
+    {
+        if (!Resolve(original, ref original.Comp, false) || !Resolve(target, ref target.Comp, false))
+            return;
+
+        var coords = Transform(target).Coordinates;
+
+        // delete all items in the target storage
+        _container.CleanContainer(target.Comp.Container);
+
+        // recursively replace them
+        foreach ((var itemUid, var itemLocation) in original.Comp.StoredItems)
+        {
+            var copy = CopyItem(itemUid, coords, whitelist, blacklist);
+            if (copy != null)
+                _storage.InsertAt(target, copy.Value, itemLocation, out _, playSound: false);
         }
     }
 }
index 3828e6c0cffc4a3921bad0003250401063ed2963..fa5f4a35d70dcea4dd27c1d677e826651de10def 100644 (file)
@@ -34,7 +34,7 @@ public sealed partial class CloningSettingsPrototype : IPrototype, IInheritingPr
     ///     Disabled when null.
     /// </summary>
     [DataField]
-    public SlotFlags? CopyEquipment = SlotFlags.WITHOUT_POCKET;
+    public SlotFlags? CopyEquipment = SlotFlags.All;
 
     /// <summary>
     ///     Whitelist for the equipment allowed to be copied.
index 79b49140923303aa144c4ef8fa4c1f286c8328e2..ce5a0daf562c021c594da272e5faecf48268cce9 100644 (file)
@@ -15,6 +15,7 @@ loadout-group-survival-syndicate = Github is forcing me to write text that is li
 loadout-group-breath-tool = Species-dependent breath tools
 loadout-group-tank-harness = Species-specific survival equipment
 loadout-group-EVA-tank = Species-specific gas tank
+loadout-group-vox-tank = Vox-specific gas tank
 loadout-group-pocket-tank-double = Species-specific double emergency tank in pocket
 loadout-group-survival-mime = Mime Survival Box
 
index 6ecb75159902a248ac6dc8bcb847c40c53c3e4c2..cdc273f667650f0bf98661957fee9b189fabcfb5 100644 (file)
@@ -57,6 +57,7 @@
   blacklist:
     components:
     - AttachedClothing # helmets, which are part of the suit
+    - HumanoidAppearance # will cause problems for downstream felinids getting cloned as Urists
     - VirtualItem
 
 - type: cloningSettings
index 689049f977a73b36a5457329586304bb770b8818..444c5e5d09a971c1ae3851c08e74ecdd91ceb311 100644 (file)
       pickPlayer: false
       startingGear: ParadoxCloneGear
       roleLoadout:
-      - RoleSurvivalStandard # give vox something to breath in case they don't get a copy
+      - RoleSurvivalVoxTank # give vox something to breath in case they don't get a copy
       briefing:
         text: paradox-clone-role-greeting
         color: lightblue
index bcdd592007e1ca240d0a62d8fce41f0c553f7d33..eb36bc30017ce37e2cb6d76d6ce9ac5176431ba3 100644 (file)
@@ -78,6 +78,7 @@
   - EmergencyOxygen
   - LoadoutSpeciesVoxNitrogen
 
+# nitrogen or oxygen tank, depending on what your species needs
 - type: loadoutGroup
   id: GroupEVATank
   name: loadout-group-EVA-tank
   - LoadoutSpeciesEVANitrogen
   - LoadoutSpeciesEVAOxygen
 
+# vox get a nitrogen tank, other species get nothing
+- type: loadoutGroup
+  id: GroupEVATankVox
+  name: loadout-group-vox-tank
+  hidden: true
+  loadouts:
+  - LoadoutSpeciesVoxNitrogen
+
 - type: loadoutGroup
   id: GroupPocketTankDouble
   name: loadout-group-pocket-tank-double
index a456b54c94ed324a363125d4b0af88ef79577b6f..c2e154798bae076187b33df08f7c476d9144efe0 100644 (file)
 # These loadouts are used for non-crew spawns, like off-station antags and event mobs
 # They will be used without player configuration, thus they will only ever apply what is forced by MinLimit
 
+# gives vox a harness and breathing mask
+# nitrogen tank not included
 - type: roleLoadout
   id: RoleSurvivalVoxSupport
   groups:
   - GroupSpeciesBreathTool
   - GroupTankHarness
 
+# gives vox a nitrogen breathing tank
+# other species get nothing
+- type: roleLoadout
+  id: RoleSurvivalVoxTank
+  groups:
+  - GroupEVATankVox
+
 - type: roleLoadout
   id: RoleSurvivalStandard
   groups: