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: