]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Make parcelwrap able to wrap humanoids (#40911)
authorslarticodefast <161409025+slarticodefast@users.noreply.github.com>
Wed, 15 Oct 2025 20:19:34 +0000 (22:19 +0200)
committerGitHub <noreply@github.com>
Wed, 15 Oct 2025 20:19:34 +0000 (20:19 +0000)
* parcel

* help, I'm being forced to add admin abuse features

* review

Content.Shared/ParcelWrap/Components/ParcelWrapComponent.cs
Content.Shared/ParcelWrap/Components/ParcelWrapOverrideComponent.cs [new file with mode: 0644]
Content.Shared/ParcelWrap/Components/WrappedParcelComponent.cs
Content.Shared/ParcelWrap/Systems/ParcelWrappingSystem.ParcelWrap.cs
Content.Shared/ParcelWrap/Systems/ParcelWrappingSystem.WrappedParcel.cs
Resources/Locale/en-US/parcel-wrap.ftl [moved from Resources/Locale/en-US/pacel-wrap.ftl with 71% similarity]
Resources/Prototypes/Entities/Mobs/Species/base.yml
Resources/Prototypes/Entities/Objects/Misc/parcel_wrap.yml
Resources/Textures/Objects/Misc/ParcelWrap/wrapped_parcel.rsi/human.png [new file with mode: 0644]
Resources/Textures/Objects/Misc/ParcelWrap/wrapped_parcel.rsi/meta.json

index b5afb49dca3119f6539fbb992d102cdaf5468dfb..41375e5c2ac255a0dacae464e93607b06a8a9e9d 100644 (file)
@@ -10,58 +10,64 @@ namespace Content.Shared.ParcelWrap.Components;
 /// This component gives its owning entity the ability to wrap items into parcels.
 /// </summary>
 /// <seealso cref="Components.WrappedParcelComponent"/>
-[RegisterComponent, NetworkedComponent]
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
 [Access] // Readonly, except for VV editing
 public sealed partial class ParcelWrapComponent : Component
 {
     /// <summary>
     /// The <see cref="EntityPrototype"/> of the parcel created by using this component.
     /// </summary>
-    [DataField(required: true)]
+    [DataField(required: true), AutoNetworkedField]
     public EntProtoId ParcelPrototype;
 
     /// <summary>
     /// If true, parcels created by this will have the same <see cref="ItemSizePrototype">size</see> as the item they
     /// contain. If false, parcels created by this will always have the size specified by <see cref="FallbackItemSize"/>.
     /// </summary>
-    [DataField]
+    [DataField, AutoNetworkedField]
     public bool WrappedItemsMaintainSize = true;
 
     /// <summary>
     /// The <see cref="ItemSizePrototype">size</see> of parcels created by this component's entity. This is used if
     /// <see cref="WrappedItemsMaintainSize"/> is false, or if the item being wrapped somehow doesn't have a size.
     /// </summary>
-    [DataField]
+    [DataField, AutoNetworkedField]
     public ProtoId<ItemSizePrototype> FallbackItemSize = "Ginormous";
 
     /// <summary>
     /// If true, parcels created by this will have the same shape as the item they contain. If false, parcels created by
     /// this will have the default shape for their size.
     /// </summary>
-    [DataField]
+    [DataField, AutoNetworkedField]
     public bool WrappedItemsMaintainShape;
 
     /// <summary>
     /// How long it takes to use this to wrap something.
     /// </summary>
-    [DataField(required: true)]
+    [DataField(required: true), AutoNetworkedField]
     public TimeSpan WrapDelay = TimeSpan.FromSeconds(1);
 
     /// <summary>
     /// Sound played when this is used to wrap something.
     /// </summary>
-    [DataField]
+    [DataField, AutoNetworkedField]
     public SoundSpecifier? WrapSound;
 
     /// <summary>
     /// Defines the set of things which can be wrapped (unless it fails the <see cref="Blacklist"/>).
     /// </summary>
-    [DataField]
+    [DataField, AutoNetworkedField]
     public EntityWhitelist? Whitelist;
 
     /// <summary>
     /// Defines the set of things which cannot be wrapped (even if it passes the <see cref="Whitelist"/>).
     /// </summary>
-    [DataField]
+    [DataField, AutoNetworkedField]
     public EntityWhitelist? Blacklist;
+
+    /// <summary>
+    /// If a player trapped inside this parcel can escape from it by unwrapping it.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public bool CanSelfUnwrap = true;
 }
diff --git a/Content.Shared/ParcelWrap/Components/ParcelWrapOverrideComponent.cs b/Content.Shared/ParcelWrap/Components/ParcelWrapOverrideComponent.cs
new file mode 100644 (file)
index 0000000..e401d30
--- /dev/null
@@ -0,0 +1,24 @@
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.ParcelWrap.Components;
+
+/// <summary>
+/// Added to an entity to override the datafields in <see cref="ParcelWrapComponent"/> when it is being wrapped.
+/// Use this for special, non-item parcel types, for example Urist-shaped parcels.
+/// </summary>
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+public sealed partial class ParcelWrapOverrideComponent : Component
+{
+    /// <summary>
+    /// The <see cref="EntityPrototype"/> of the parcel created by wrapping this entity.
+    /// </summary>
+    [DataField(required: true), AutoNetworkedField]
+    public EntProtoId? ParcelPrototype;
+
+    /// <summary>
+    /// How long it takes to use this to wrap something.
+    /// </summary>
+    [DataField(required: true), AutoNetworkedField]
+    public TimeSpan? WrapDelay;
+}
index e0ceaeae49c5bfb5c504a4fbe1c5d9c945b7a8fa..2ed018e34470eeb1f9a1c0548c25e9a6c7e9fc6f 100644 (file)
@@ -11,7 +11,8 @@ namespace Content.Shared.ParcelWrap.Components;
 /// destroying this entity and releasing <see cref="Contents"/>.
 /// </summary>
 /// <seealso cref="ParcelWrapComponent"/>
-[RegisterComponent, NetworkedComponent, Access(typeof(ParcelWrappingSystem))]
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+[Access(typeof(ParcelWrappingSystem))]
 public sealed partial class WrappedParcelComponent : Component
 {
     /// <summary>
@@ -23,19 +24,19 @@ public sealed partial class WrappedParcelComponent : Component
     /// <summary>
     /// Specifies the entity to spawn when this parcel is unwrapped.
     /// </summary>
-    [DataField]
+    [DataField, AutoNetworkedField]
     public EntProtoId? UnwrapTrash;
 
     /// <summary>
     /// How long it takes to unwrap this parcel.
     /// </summary>
-    [DataField(required: true)]
+    [DataField(required: true), AutoNetworkedField]
     public TimeSpan UnwrapDelay = TimeSpan.FromSeconds(1);
 
     /// <summary>
     /// Sound played when unwrapping this parcel.
     /// </summary>
-    [DataField]
+    [DataField, AutoNetworkedField]
     public SoundSpecifier? UnwrapSound;
 
     /// <summary>
@@ -43,4 +44,11 @@ public sealed partial class WrappedParcelComponent : Component
     /// </summary>
     [DataField, ViewVariables(VVAccess.ReadOnly)]
     public string ContainerId = "contents";
+
+    /// <summary>
+    /// If a player trapped inside this parcel can escape from it by unwrapping it.
+    /// This is set by the <see cref="ParcelWrapComponent" /> used to create the parcel.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public bool CanSelfUnwrap = true;
 }
index f924a7c6c9bf2d82d0f741a52ebecae06d78ec60..bd2e1eb47f33115f2806f1822b40aeacc17448f9 100644 (file)
@@ -1,7 +1,9 @@
 using Content.Shared.DoAfter;
+using Content.Shared.IdentityManagement;
 using Content.Shared.Interaction;
 using Content.Shared.Item;
 using Content.Shared.ParcelWrap.Components;
+using Content.Shared.Popups;
 using Content.Shared.Verbs;
 using Robust.Shared.Utility;
 
@@ -59,9 +61,26 @@ public sealed partial class ParcelWrappingSystem
 
     private bool TryStartWrapDoAfter(EntityUid user, Entity<ParcelWrapComponent> wrapper, EntityUid target)
     {
+        var duration = wrapper.Comp.WrapDelay;
+
+        if (TryComp<ParcelWrapOverrideComponent>(target, out var overrideComp) && overrideComp.WrapDelay != null)
+            duration = overrideComp.WrapDelay.Value;
+
+        // In case the target is a player inform them with a popup.
+        if (target == user)
+        {
+            var selfMsg = Loc.GetString("parcel-wrap-popup-being-wrapped-self");
+            _popup.PopupClient(selfMsg, user, user);
+        }
+        else
+        {
+            var othersMsg = Loc.GetString("parcel-wrap-popup-being-wrapped", ("user", Identity.Entity(user, EntityManager)));
+            _popup.PopupEntity(othersMsg, target, target, PopupType.MediumCaution);
+        }
+
         return _doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager,
             user,
-            wrapper.Comp.WrapDelay,
+            duration,
             new ParcelWrapItemDoAfterEvent(),
             wrapper, // Raise the event on the wrapper because that's what the event handler expects.
             target,
@@ -81,18 +100,34 @@ public sealed partial class ParcelWrappingSystem
     /// <param name="target">The entity being wrapped.</param>
     private void WrapInternal(EntityUid user, Entity<ParcelWrapComponent> wrapper, EntityUid target)
     {
-        if (_net.IsServer)
+        // Consume a `use` on the wrapper, and delete the wrapper if it's empty.
+        _charges.TryUseCharges(wrapper.Owner, 1);
+        if (_charges.IsEmpty(wrapper.Owner))
+            PredictedQueueDel(wrapper);
+
+        // Play a wrapping sound.
+        _audio.PlayPredicted(wrapper.Comp.WrapSound, target, user);
+
+        if (_net.IsClient)
+            return; // Predicted spawns can't be interacted with yet.
+
+        EntityUid spawned;
+        var targetTransform = Transform(target);
+        // Check if the target has a pre-defined parcel type to be used.
+        if (TryComp<ParcelWrapOverrideComponent>(target, out var overrideComp))
+        {
+            spawned = Spawn(overrideComp.ParcelPrototype, targetTransform.Coordinates);
+        }
+        else // Create a parcel with the same size and generic sprites instead.
         {
-            var spawned = Spawn(wrapper.Comp.ParcelPrototype, Transform(target).Coordinates);
+            spawned = Spawn(wrapper.Comp.ParcelPrototype, targetTransform.Coordinates);
 
             // If this wrap maintains the size when wrapping, set the parcel's size to the target's size. Otherwise use the
             // wrap's fallback size.
             TryComp(target, out ItemComponent? targetItemComp);
             var size = wrapper.Comp.FallbackItemSize;
             if (wrapper.Comp.WrappedItemsMaintainSize && targetItemComp is not null)
-            {
                 size = targetItemComp.Size;
-            }
 
             // ParcelWrap's spawned entity should always have an `ItemComp`. As of writing, the only use has it hardcoded on
             // its prototype.
@@ -103,33 +138,28 @@ public sealed partial class ParcelWrappingSystem
             // If this wrap maintains the shape when wrapping and the item has a shape override, copy the shape override to
             // the parcel.
             if (wrapper.Comp.WrappedItemsMaintainShape && targetItemComp is { Shape: { } shape })
-            {
                 _item.SetShape(spawned, shape, item);
-            }
-
-            // If the target's in a container, try to put the parcel in its place in the container.
-            if (_container.TryGetContainingContainer((target, null, null), out var containerOfTarget))
-            {
-                _container.Remove(target, containerOfTarget);
-                _container.InsertOrDrop((spawned, null, null), containerOfTarget);
-            }
-
-            // Insert the target into the parcel.
-            var parcel = EnsureComp<WrappedParcelComponent>(spawned);
-            if (!_container.Insert(target, parcel.Contents))
-            {
-                DebugTools.Assert(
-                    $"Failed to insert target entity into newly spawned parcel. target={PrettyPrint.PrintUserFacing(target)}");
-                QueueDel(spawned);
-            }
         }
 
-        // Consume a `use` on the wrapper, and delete the wrapper if it's empty.
-        _charges.TryUseCharges(wrapper.Owner, 1);
-        if (_net.IsServer && _charges.IsEmpty(wrapper.Owner))
-            QueueDel(wrapper);
+        _transform.SetLocalRotation(spawned, targetTransform.LocalRotation);
 
-        // Play a wrapping sound.
-        _audio.PlayPredicted(wrapper.Comp.WrapSound, target, user);
+        // If the target is in a container, try to put the parcel in its place in the container.
+        if (_container.TryGetContainingContainer((target, null, null), out var containerOfTarget))
+        {
+            _container.Remove(target, containerOfTarget);
+            _container.InsertOrDrop((spawned, null, null), containerOfTarget);
+        }
+
+        // Insert the target into the parcel.
+        var parcel = EnsureComp<WrappedParcelComponent>(spawned);
+        parcel.CanSelfUnwrap = wrapper.Comp.CanSelfUnwrap;
+        Dirty(spawned, parcel);
+
+        if (!_container.Insert(target, parcel.Contents))
+        {
+            DebugTools.Assert(
+                $"Failed to insert target entity into newly spawned parcel. target={PrettyPrint.PrintUserFacing(target)}");
+            PredictedDel(spawned);
+        }
     }
 }
index 3f1aa2fa80dbc05f904c7fa26d9b8fd496fd7a6b..460ba7caf5afe9edc68873225533c1eb9a37f92c 100644 (file)
@@ -41,7 +41,7 @@ public sealed partial class ParcelWrappingSystem
         if (!args.CanAccess || !args.CanComplexInteract)
             return;
 
-        if (entity.Comp.Contents.Contains(args.User))
+        if (!entity.Comp.CanSelfUnwrap && entity.Comp.Contents.Contains(args.User))
             return;
 
         // "Capture" the values from `args` because C# doesn't like doing the capturing for `ref` values.
similarity index 71%
rename from Resources/Locale/en-US/pacel-wrap.ftl
rename to Resources/Locale/en-US/parcel-wrap.ftl
index 2351892fa544aa9dd85c6e036ab7c162a57a0755..560ed78dc7714a8020f15c039410b5badf1782b8 100644 (file)
@@ -2,6 +2,8 @@ parcel-wrap-verb-wrap = Wrap
 parcel-wrap-verb-unwrap = Unwrap
 
 parcel-wrap-popup-parcel-destroyed = The wrapping containing { THE($contents) } is destroyed!
+parcel-wrap-popup-being-wrapped = {CAPITALIZE(THE($user))} is trying to parcel wrap you!
+parcel-wrap-popup-being-wrapped-self = You start parcel wrapping yourself.
 
 # Shown when parcel wrap is examined in details range
 parcel-wrap-examine-detail-uses = { $uses ->
index d2a8b21a0b1afc741a8403ef504c76fcc1ca4a3e..f33cee8b209c1d83a001e2b36021fea772ef990d 100644 (file)
     factions:
     - NanoTrasen
   - type: CreamPied
+  - type: ParcelWrapOverride
+    parcelPrototype: WrappedParcelHumanoid
+    wrapDelay: 5
   - type: Stripping
   - type: UserInterface
     interfaces:
index 8d7baa4339539f73b6ac819dec41f41c4e5f1e04..df4b5541ff9e67890c2da84959ee46e950dfd8c8 100644 (file)
@@ -18,6 +18,7 @@
     whitelist:
       components:
       - Item
+      - ParcelWrapOverride
     blacklist:
       components:
       - NukeDisk # Don't try to hide the disk.
   - type: ParcelWrap
     whitelist: null
     blacklist: null
+    canSelfUnwrap: false # admins held me at gunpoint until I added this bool
 
 - type: entity
-  parent: BaseItem
-  id: WrappedParcel
-  categories: [ HideSpawnMenu ]
+  abstract: true
+  id: BaseWrappedParcel
   name: wrapped parcel
   description: Something wrapped up in paper. I wonder what's inside...
   components:
+  - type: Physics
+    bodyType: Dynamic
+  - type: GravityAffected
+  - type: Clickable
+  - type: InteractionOutline
+  - type: Pullable
   - type: ContainerContainer
     containers:
       contents: !type:ContainerSlot
       paper_label: !type:ContainerSlot
-  - type: Appearance
-  - type: GenericVisualizer
-    visuals:
-      enum.WrappedParcelVisuals.Size:
-        enum.WrappedParcelVisuals.Layer:
-          "Tiny": { state: "parcel-tiny" }
-          "Small": { state: "parcel-small" }
-          "Medium": { state: "parcel-medium" }
-          "Large": { state: "parcel-medium" }
-          "Huge": { state: "parcel-large" }
-          "Ginormous": { state: "parcel-large" }
-      enum.PaperLabelVisuals.HasLabel:
-        enum.PaperLabelVisuals.Layer:
-          True: { visible: true }
-          False: { visible: false }
-      enum.PaperLabelVisuals.LabelType:
-        enum.PaperLabelVisuals.Layer:
-          Paper: { state: paper }
-          Bounty: { state: bounty }
-          CaptainsPaper: { state: captains_paper }
-          Invoice: { state: invoice }
-  - type: Sprite
-    sprite: Objects/Misc/ParcelWrap/wrapped_parcel.rsi
-    layers:
-    - state: parcel-medium
-      map: [ "enum.WrappedParcelVisuals.Layer" ]
-    - state: paper
-      visible: false
-      sprite: Objects/Misc/ParcelWrap/paper_labels.rsi
-      map: ["enum.PaperLabelVisuals.Layer"]
   - type: WrappedParcel
     unwrapDelay: 0.5
     unwrapSound:
     tags:
     - Recyclable # Parcel entity is recyclable, and when it's destroyed, it'll drop its contents.
 
+- type: entity
+  parent: [BaseItem, BaseWrappedParcel]
+  id: WrappedParcel
+  components:
+  - type: Appearance
+  - type: GenericVisualizer
+    visuals:
+      enum.WrappedParcelVisuals.Size:
+        enum.WrappedParcelVisuals.Layer:
+          "Tiny": { state: "parcel-tiny" }
+          "Small": { state: "parcel-small" }
+          "Medium": { state: "parcel-medium" }
+          "Large": { state: "parcel-medium" }
+          "Huge": { state: "parcel-large" }
+          "Ginormous": { state: "parcel-large" }
+      enum.PaperLabelVisuals.HasLabel:
+        enum.PaperLabelVisuals.Layer:
+          True: { visible: true }
+          False: { visible: false }
+      enum.PaperLabelVisuals.LabelType:
+        enum.PaperLabelVisuals.Layer:
+          Paper: { state: paper }
+          Bounty: { state: bounty }
+          CaptainsPaper: { state: captains_paper }
+          Invoice: { state: invoice }
+  - type: Sprite
+    sprite: Objects/Misc/ParcelWrap/wrapped_parcel.rsi
+    layers:
+    - state: parcel-medium
+      map: [ "enum.WrappedParcelVisuals.Layer" ]
+    - state: paper
+      visible: false
+      sprite: Objects/Misc/ParcelWrap/paper_labels.rsi
+      map: ["enum.PaperLabelVisuals.Layer"]
+
+
+- type: entity
+  parent: BaseWrappedParcel
+  id: WrappedParcelHumanoid
+  description: Something wrapped up in paper. It's suspiciously human-shaped.
+  components:
+  - type: Physics
+    fixedRotation: false
+  - type: Sprite
+    sprite: Objects/Misc/ParcelWrap/wrapped_parcel.rsi
+    state: human
+    noRot: true
+    drawdepth: Mobs
+  - type: Fixtures
+    fixtures:
+      fix1:
+        shape:
+          !type:PhysShapeCircle
+          radius: 0.35
+        density: 185
+        restitution: 0.0
+        mask:
+        - MobMask
+        layer:
+        - MobLayer
+
 - type: entity
   parent: BaseItem
   id: ParcelWrapTrash
diff --git a/Resources/Textures/Objects/Misc/ParcelWrap/wrapped_parcel.rsi/human.png b/Resources/Textures/Objects/Misc/ParcelWrap/wrapped_parcel.rsi/human.png
new file mode 100644 (file)
index 0000000..b872967
Binary files /dev/null and b/Resources/Textures/Objects/Misc/ParcelWrap/wrapped_parcel.rsi/human.png differ
index f07107aaa88f478f781bbb497d0a1d2db765200c..ec930e60c60bab26562c8fd07918ba3826f444d5 100644 (file)
@@ -1,7 +1,7 @@
 {
     "version": 1,
     "license": "CC-BY-SA-4.0",
-    "copyright": "created for ss14 by ps3moira (github) based on package sprites from vgstation at https://github.com/vgstation-coders/vgstation13/pull/36993",
+    "copyright": "created for ss14 by ps3moira (github) based on package sprites from vgstation at https://github.com/vgstation-coders/vgstation13/pull/36993 | human by Davyei (Discord)",
     "size": {
         "x": 32,
         "y": 32
         {
             "name": "tall-crate",
             "directions": 1
+        },
+        {
+            "name": "human",
+            "directions": 4
         }
     ]
 }