From 9562e009bf0e06d5d94742e2cd9de0512bad4262 Mon Sep 17 00:00:00 2001 From: slarticodefast <161409025+slarticodefast@users.noreply.github.com> Date: Wed, 15 Oct 2025 22:19:34 +0200 Subject: [PATCH] Make parcelwrap able to wrap humanoids (#40911) * parcel * help, I'm being forced to add admin abuse features * review --- .../Components/ParcelWrapComponent.cs | 24 ++-- .../Components/ParcelWrapOverrideComponent.cs | 24 ++++ .../Components/WrappedParcelComponent.cs | 16 ++- .../ParcelWrappingSystem.ParcelWrap.cs | 88 ++++++++++----- .../ParcelWrappingSystem.WrappedParcel.cs | 2 +- .../en-US/{pacel-wrap.ftl => parcel-wrap.ftl} | 2 + .../Prototypes/Entities/Mobs/Species/base.yml | 3 + .../Entities/Objects/Misc/parcel_wrap.yml | 104 ++++++++++++------ .../ParcelWrap/wrapped_parcel.rsi/human.png | Bin 0 -> 1237 bytes .../ParcelWrap/wrapped_parcel.rsi/meta.json | 6 +- 10 files changed, 192 insertions(+), 77 deletions(-) create mode 100644 Content.Shared/ParcelWrap/Components/ParcelWrapOverrideComponent.cs rename Resources/Locale/en-US/{pacel-wrap.ftl => parcel-wrap.ftl} (71%) create mode 100644 Resources/Textures/Objects/Misc/ParcelWrap/wrapped_parcel.rsi/human.png diff --git a/Content.Shared/ParcelWrap/Components/ParcelWrapComponent.cs b/Content.Shared/ParcelWrap/Components/ParcelWrapComponent.cs index b5afb49dca..41375e5c2a 100644 --- a/Content.Shared/ParcelWrap/Components/ParcelWrapComponent.cs +++ b/Content.Shared/ParcelWrap/Components/ParcelWrapComponent.cs @@ -10,58 +10,64 @@ namespace Content.Shared.ParcelWrap.Components; /// This component gives its owning entity the ability to wrap items into parcels. /// /// -[RegisterComponent, NetworkedComponent] +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] [Access] // Readonly, except for VV editing public sealed partial class ParcelWrapComponent : Component { /// /// The of the parcel created by using this component. /// - [DataField(required: true)] + [DataField(required: true), AutoNetworkedField] public EntProtoId ParcelPrototype; /// /// If true, parcels created by this will have the same size as the item they /// contain. If false, parcels created by this will always have the size specified by . /// - [DataField] + [DataField, AutoNetworkedField] public bool WrappedItemsMaintainSize = true; /// /// The size of parcels created by this component's entity. This is used if /// is false, or if the item being wrapped somehow doesn't have a size. /// - [DataField] + [DataField, AutoNetworkedField] public ProtoId FallbackItemSize = "Ginormous"; /// /// 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. /// - [DataField] + [DataField, AutoNetworkedField] public bool WrappedItemsMaintainShape; /// /// How long it takes to use this to wrap something. /// - [DataField(required: true)] + [DataField(required: true), AutoNetworkedField] public TimeSpan WrapDelay = TimeSpan.FromSeconds(1); /// /// Sound played when this is used to wrap something. /// - [DataField] + [DataField, AutoNetworkedField] public SoundSpecifier? WrapSound; /// /// Defines the set of things which can be wrapped (unless it fails the ). /// - [DataField] + [DataField, AutoNetworkedField] public EntityWhitelist? Whitelist; /// /// Defines the set of things which cannot be wrapped (even if it passes the ). /// - [DataField] + [DataField, AutoNetworkedField] public EntityWhitelist? Blacklist; + + /// + /// If a player trapped inside this parcel can escape from it by unwrapping it. + /// + [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 index 0000000000..e401d30a7f --- /dev/null +++ b/Content.Shared/ParcelWrap/Components/ParcelWrapOverrideComponent.cs @@ -0,0 +1,24 @@ +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Shared.ParcelWrap.Components; + +/// +/// Added to an entity to override the datafields in when it is being wrapped. +/// Use this for special, non-item parcel types, for example Urist-shaped parcels. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class ParcelWrapOverrideComponent : Component +{ + /// + /// The of the parcel created by wrapping this entity. + /// + [DataField(required: true), AutoNetworkedField] + public EntProtoId? ParcelPrototype; + + /// + /// How long it takes to use this to wrap something. + /// + [DataField(required: true), AutoNetworkedField] + public TimeSpan? WrapDelay; +} diff --git a/Content.Shared/ParcelWrap/Components/WrappedParcelComponent.cs b/Content.Shared/ParcelWrap/Components/WrappedParcelComponent.cs index e0ceaeae49..2ed018e344 100644 --- a/Content.Shared/ParcelWrap/Components/WrappedParcelComponent.cs +++ b/Content.Shared/ParcelWrap/Components/WrappedParcelComponent.cs @@ -11,7 +11,8 @@ namespace Content.Shared.ParcelWrap.Components; /// destroying this entity and releasing . /// /// -[RegisterComponent, NetworkedComponent, Access(typeof(ParcelWrappingSystem))] +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +[Access(typeof(ParcelWrappingSystem))] public sealed partial class WrappedParcelComponent : Component { /// @@ -23,19 +24,19 @@ public sealed partial class WrappedParcelComponent : Component /// /// Specifies the entity to spawn when this parcel is unwrapped. /// - [DataField] + [DataField, AutoNetworkedField] public EntProtoId? UnwrapTrash; /// /// How long it takes to unwrap this parcel. /// - [DataField(required: true)] + [DataField(required: true), AutoNetworkedField] public TimeSpan UnwrapDelay = TimeSpan.FromSeconds(1); /// /// Sound played when unwrapping this parcel. /// - [DataField] + [DataField, AutoNetworkedField] public SoundSpecifier? UnwrapSound; /// @@ -43,4 +44,11 @@ public sealed partial class WrappedParcelComponent : Component /// [DataField, ViewVariables(VVAccess.ReadOnly)] public string ContainerId = "contents"; + + /// + /// If a player trapped inside this parcel can escape from it by unwrapping it. + /// This is set by the used to create the parcel. + /// + [DataField, AutoNetworkedField] + public bool CanSelfUnwrap = true; } diff --git a/Content.Shared/ParcelWrap/Systems/ParcelWrappingSystem.ParcelWrap.cs b/Content.Shared/ParcelWrap/Systems/ParcelWrappingSystem.ParcelWrap.cs index f924a7c6c9..bd2e1eb47f 100644 --- a/Content.Shared/ParcelWrap/Systems/ParcelWrappingSystem.ParcelWrap.cs +++ b/Content.Shared/ParcelWrap/Systems/ParcelWrappingSystem.ParcelWrap.cs @@ -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 wrapper, EntityUid target) { + var duration = wrapper.Comp.WrapDelay; + + if (TryComp(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 /// The entity being wrapped. private void WrapInternal(EntityUid user, Entity 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(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(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(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); + } } } diff --git a/Content.Shared/ParcelWrap/Systems/ParcelWrappingSystem.WrappedParcel.cs b/Content.Shared/ParcelWrap/Systems/ParcelWrappingSystem.WrappedParcel.cs index 3f1aa2fa80..460ba7caf5 100644 --- a/Content.Shared/ParcelWrap/Systems/ParcelWrappingSystem.WrappedParcel.cs +++ b/Content.Shared/ParcelWrap/Systems/ParcelWrappingSystem.WrappedParcel.cs @@ -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. diff --git a/Resources/Locale/en-US/pacel-wrap.ftl b/Resources/Locale/en-US/parcel-wrap.ftl similarity index 71% rename from Resources/Locale/en-US/pacel-wrap.ftl rename to Resources/Locale/en-US/parcel-wrap.ftl index 2351892fa5..560ed78dc7 100644 --- a/Resources/Locale/en-US/pacel-wrap.ftl +++ b/Resources/Locale/en-US/parcel-wrap.ftl @@ -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 -> diff --git a/Resources/Prototypes/Entities/Mobs/Species/base.yml b/Resources/Prototypes/Entities/Mobs/Species/base.yml index d2a8b21a0b..f33cee8b20 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/base.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/base.yml @@ -176,6 +176,9 @@ factions: - NanoTrasen - type: CreamPied + - type: ParcelWrapOverride + parcelPrototype: WrappedParcelHumanoid + wrapDelay: 5 - type: Stripping - type: UserInterface interfaces: diff --git a/Resources/Prototypes/Entities/Objects/Misc/parcel_wrap.yml b/Resources/Prototypes/Entities/Objects/Misc/parcel_wrap.yml index 8d7baa4339..df4b5541ff 100644 --- a/Resources/Prototypes/Entities/Objects/Misc/parcel_wrap.yml +++ b/Resources/Prototypes/Entities/Objects/Misc/parcel_wrap.yml @@ -18,6 +18,7 @@ whitelist: components: - Item + - ParcelWrapOverride blacklist: components: - NukeDisk # Don't try to hide the disk. @@ -38,48 +39,24 @@ - 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: @@ -118,6 +95,67 @@ 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 index 0000000000000000000000000000000000000000..b8729670b2a97fee5c3d1955f2a3080762e76ba2 GIT binary patch literal 1237 zcmV;`1SPx(k4Z#9RCt{2o6l?8NEFAv+7>mKSZkW`(z!RqXt#q1F5ofa-U3L*nTO3;(Ow z`P6?DhWCco2w#gZ!s3?)7ypMW-~iBSR+0B~$b(O1006M3UT|I8mJ{F4Q_DE4d@2KU zbwe^tX8~qUJs75w`xMB7PrV5CO?%P~hG9aHqE@ zm!3d=B*8Ep;QlEw^@M>>#KTWL0@|ADA06%nuLmP1fbZ`p*xymeN4ac?%lH7e_^BGk z;Y7WGh1Pz3;r-LIt4v42GX65r@YgPaVffgYt!zs^KRVpaY)j0WpL!SQ4Xm|WjXm|y z8(3*?Nu|8_D-A%bPv+Z=JB!S{um&RsH|{LB&SDgZ;8Qz74^4#uHr;_uE0qB%B^gi4 zTd8jNw7i8%Nlwu+z4GL*HNe=Jp{8sF3rys*$>OSfYTZgn#(3fdj!{<8;VS;e&tJItYrQ41mGn&;&l?rAnpM=4Eqwg^1s9f!y?PnH z{PZKn&J4ZbbZ+m<8n(FZ-!3DPEOBP`>GJyBME>YQNQ?d_X3et+>w(hhcf;&|k# zB))z=62>o7=z)qkSP3sQ$0r^9{Nk0c<8c7^^!}ay_Vt_KHEZC-Tptk5(+~gs8x!l& z->yFsVuXox>HqiUH^j~gm=0u^>jP@aVumK`1CeF?q(c%C4%70PJV8-zFO7t|!bvHo zyo)536-e-;-!h8@Scf2uy1F6thS!wj?SKXLPKQBy^XK| zFc(NLj{xt^hj-`0a_51_h2;tw$Ow~TT}hLdKvN~3H9<5)XVb%3DJu4 z_v&S)^@qAbL~YCq5h^7)*b2waOjw-|=|d793{!z=3K00|*%bgJ`UH84Fi+2}P*d1X zqnOu#)3YlqtJxE&R(|S{(Pax@to~=j8zxJPUPR9t8aXLu!nA3XqcZutfd0NVpHZ;dSIa z;nCnjQDFd{KmKk#!G|Pn18i~>WWnY#z_%0&u!?&G*i#Q`LqS_p{fTv%duDWEUHajq zEU%b6K_mno$0wag!9-pfeb8}y(n0uKkVpRmbxAEH4RKfH00000NkvXXu0mjf9pYTH literal 0 HcmV?d00001 diff --git a/Resources/Textures/Objects/Misc/ParcelWrap/wrapped_parcel.rsi/meta.json b/Resources/Textures/Objects/Misc/ParcelWrap/wrapped_parcel.rsi/meta.json index f07107aaa8..ec930e60c6 100644 --- a/Resources/Textures/Objects/Misc/ParcelWrap/wrapped_parcel.rsi/meta.json +++ b/Resources/Textures/Objects/Misc/ParcelWrap/wrapped_parcel.rsi/meta.json @@ -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 @@ -34,6 +34,10 @@ { "name": "tall-crate", "directions": 1 + }, + { + "name": "human", + "directions": 4 } ] } -- 2.51.2