From d8ed9fe152a674a77bbac42bb57766f73afa02ec Mon Sep 17 00:00:00 2001
From: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
Date: Sun, 16 Mar 2025 23:02:19 +0100
Subject: [PATCH] Paradox clones get all storage items the original has.
(#35838)
* recursive storage copying
* include slime storage
* future proofing
* remove survival box
---
Content.Server/Cloning/CloningSystem.cs | 98 ++++++++++++++++---
.../Cloning/CloningSettingsPrototype.cs | 2 +-
.../en-US/preferences/loadout-groups.ftl | 1 +
.../Prototypes/Entities/Mobs/Player/clone.yml | 1 +
Resources/Prototypes/GameRules/events.yml | 2 +-
.../Prototypes/Loadouts/loadout_groups.yml | 9 ++
.../Prototypes/Loadouts/role_loadouts.yml | 9 ++
7 files changed, 109 insertions(+), 13 deletions(-)
diff --git a/Content.Server/Cloning/CloningSystem.cs b/Content.Server/Cloning/CloningSystem.cs
index 937b311a59..9a77c75f64 100644
--- a/Content.Server/Cloning/CloningSystem.cs
+++ b/Content.Server/Cloning/CloningSystem.cs
@@ -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!;
///
/// 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(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!
///
- public void CopyEquipment(EntityUid original, EntityUid clone, SlotFlags slotFlags, EntityWhitelist? whitelist = null, EntityWhitelist? blacklist = null)
+ public void CopyEquipment(Entity original, Entity clone, SlotFlags slotFlags, EntityWhitelist? whitelist = null, EntityWhitelist? blacklist = null)
{
- if (!TryComp(original, out var originalInventory) || !TryComp(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
+ }
+ }
- var prototype = MetaData(item).EntityPrototype;
- if (prototype != null)
- _inventory.SpawnItemInSlot(clone, slot.Name, prototype.ID, silent: true, inventory: cloneInventory);
+ ///
+ /// 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!
+ ///
+ ///
+ /// 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.
+ ///
+ 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(original, out var originalStack) && TryComp(spawned, out var spawnedStack))
+ _stack.SetCount(spawned, originalStack.Count, spawnedStack);
+
+ // if the original has items inside its storage, copy those as well
+ if (TryComp(original, out var originalStorage) && TryComp(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;
+ }
+
+ ///
+ /// 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.
+ ///
+ public void CopyStorage(Entity original, Entity 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);
}
}
}
diff --git a/Content.Shared/Cloning/CloningSettingsPrototype.cs b/Content.Shared/Cloning/CloningSettingsPrototype.cs
index 3828e6c0cf..fa5f4a35d7 100644
--- a/Content.Shared/Cloning/CloningSettingsPrototype.cs
+++ b/Content.Shared/Cloning/CloningSettingsPrototype.cs
@@ -34,7 +34,7 @@ public sealed partial class CloningSettingsPrototype : IPrototype, IInheritingPr
/// Disabled when null.
///
[DataField]
- public SlotFlags? CopyEquipment = SlotFlags.WITHOUT_POCKET;
+ public SlotFlags? CopyEquipment = SlotFlags.All;
///
/// Whitelist for the equipment allowed to be copied.
diff --git a/Resources/Locale/en-US/preferences/loadout-groups.ftl b/Resources/Locale/en-US/preferences/loadout-groups.ftl
index 79b4914092..ce5a0daf56 100644
--- a/Resources/Locale/en-US/preferences/loadout-groups.ftl
+++ b/Resources/Locale/en-US/preferences/loadout-groups.ftl
@@ -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
diff --git a/Resources/Prototypes/Entities/Mobs/Player/clone.yml b/Resources/Prototypes/Entities/Mobs/Player/clone.yml
index 6ecb751599..cdc273f667 100644
--- a/Resources/Prototypes/Entities/Mobs/Player/clone.yml
+++ b/Resources/Prototypes/Entities/Mobs/Player/clone.yml
@@ -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
diff --git a/Resources/Prototypes/GameRules/events.yml b/Resources/Prototypes/GameRules/events.yml
index 689049f977..444c5e5d09 100644
--- a/Resources/Prototypes/GameRules/events.yml
+++ b/Resources/Prototypes/GameRules/events.yml
@@ -258,7 +258,7 @@
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
diff --git a/Resources/Prototypes/Loadouts/loadout_groups.yml b/Resources/Prototypes/Loadouts/loadout_groups.yml
index bcdd592007..eb36bc3001 100644
--- a/Resources/Prototypes/Loadouts/loadout_groups.yml
+++ b/Resources/Prototypes/Loadouts/loadout_groups.yml
@@ -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
@@ -86,6 +87,14 @@
- 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
diff --git a/Resources/Prototypes/Loadouts/role_loadouts.yml b/Resources/Prototypes/Loadouts/role_loadouts.yml
index a456b54c94..c2e154798b 100644
--- a/Resources/Prototypes/Loadouts/role_loadouts.yml
+++ b/Resources/Prototypes/Loadouts/role_loadouts.yml
@@ -527,12 +527,21 @@
# 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: