Skip to content

Commit

Permalink
Parcel Wrap
Browse files Browse the repository at this point in the history
  • Loading branch information
Centronias committed Jan 16, 2025
1 parent 5d9311d commit 0f29e4e
Show file tree
Hide file tree
Showing 57 changed files with 624 additions and 1 deletion.
49 changes: 49 additions & 0 deletions Content.Server/ParcelWrap/Components/ParcelWrapComponent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using Content.Shared.Item;
using Content.Shared.Whitelist;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;

namespace Content.Server.ParcelWrap.Components;

/// <summary>
/// This component gives its owning entity the ability to wrap items into parcels.
/// </summary>
/// <seealso cref="Components.WrappedParcelComponent"/>
[RegisterComponent]
[Access] // Readonly, except for VV editing
public sealed partial class ParcelWrapComponent : Component
{
/// <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, ViewVariables]
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, ViewVariables]
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, ViewVariables]
public bool WrappedItemsMaintainShape = false;

/// <summary>
/// Sound played when this is used to wrap something.
/// </summary>
[DataField, ViewVariables]
public SoundSpecifier? WrapSound;

/// <summary>
/// Defines the set of things which cannot be wrapped.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadOnly)]
public EntityWhitelist? Blacklist;
}
31 changes: 31 additions & 0 deletions Content.Server/ParcelWrap/Components/WrappedParcelComponent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using Content.Server.ParcelWrap.EntitySystems;
using Robust.Shared.Audio;
using Robust.Shared.Containers;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;

namespace Content.Server.ParcelWrap.Components;

/// <summary>
/// This component marks its owner as being a parcel created by wrapping another item up. It can be unwrapped,
/// destroying this entity and releasing <see cref="Contents"/>.
/// </summary>
/// <seealso cref="ParcelWrapComponent"/>
[RegisterComponent, Access(typeof(ParcelWrappingSystem))]
public sealed partial class WrappedParcelComponent : Component
{
/// <summary>
/// The contents of this parcel.
/// </summary>
[ViewVariables(VVAccess.ReadOnly)]
public ContainerSlot Contents = default!;

[DataField, ViewVariables]
public ProtoId<EntityPrototype>? UnwrapTrash;

/// <summary>
/// Sound played when unwrapping this parcel.
/// </summary>
[DataField, ViewVariables]
public SoundSpecifier? UnwrapSound;
}
236 changes: 236 additions & 0 deletions Content.Server/ParcelWrap/EntitySystems/ParcelWrappingSystem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
using Content.Server.ParcelWrap.Components;
using Content.Shared.Destructible;
using Content.Shared.IdentityManagement;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
using Content.Shared.Item;
using Content.Shared.Materials;
using Content.Shared.ParcelWrap.Components;
using Content.Shared.Popups;
using Content.Shared.Verbs;
using Content.Shared.Whitelist;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Containers;
using Robust.Shared.Utility;

namespace Content.Server.ParcelWrap.EntitySystems;

/// <summary>
/// This system handles things related to package wrap, both wrapping items to create parcels, and unwrapping existing
/// parcels.
/// </summary>
/// <seealso cref="ParcelWrapComponent"/>
/// <seealso cref="WrappedParcelComponent"/>
public sealed class ParcelWrappingSystem : EntitySystem
{
[Dependency] private readonly SharedContainerSystem _container = default!;
[Dependency] private readonly SharedItemSystem _item = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;

private const string WrappedParcelContainerId = "wrapped_parcel";

/// <inheritdoc/>
public override void Initialize()
{
base.Initialize();

SubscribeLocalEvent<ParcelWrapComponent, AfterInteractEvent>(OnAfterInteract);
SubscribeLocalEvent<ParcelWrapComponent, GetVerbsEvent<UtilityVerb>>(OnGetVerbsForParcelWrap);

SubscribeLocalEvent<WrappedParcelComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<WrappedParcelComponent, UseInHandEvent>(OnUseInHand);
SubscribeLocalEvent<WrappedParcelComponent, GetVerbsEvent<InteractionVerb>>(OnGetVerbsForWrappedParcel);
SubscribeLocalEvent<WrappedParcelComponent, DestructionEventArgs>(OnDestroyed);
SubscribeLocalEvent<WrappedParcelComponent, GotReclaimedEvent>(OnDestroyed);
}

private void OnAfterInteract(Entity<ParcelWrapComponent> entity, ref AfterInteractEvent args)
{
if (args.Handled ||
args.Target is not { } target ||
!args.CanReach ||
!IsWrappable(entity, target))
return;

WrapInternal(args.User, entity, target);

args.Handled = true;
}

private void OnGetVerbsForParcelWrap(Entity<ParcelWrapComponent> entity,
ref GetVerbsEvent<UtilityVerb> args)
{
if (!args.CanAccess || !IsWrappable(entity, args.Target))
return;

// "Capture" the values from `args` because C# doesn't like doing the capturing for `ref` values.
var user = args.User;
var target = args.Target;

// "Wrap" verb for when just left-clicking doesn't work.
args.Verbs.Add(new UtilityVerb
{
Text = Loc.GetString("parcel-wrap-verb-wrap"),
Act = () => WrapInternal(user, entity, target),
});
}

private void OnComponentInit(Entity<WrappedParcelComponent> entity, ref ComponentInit args)
{
entity.Comp.Contents = _container.EnsureContainer<ContainerSlot>(entity, WrappedParcelContainerId);
}

private void OnUseInHand(Entity<WrappedParcelComponent> entity, ref UseInHandEvent args)
{
if (args.Handled)
{
return;
}

UnwrapInternal(entity);
args.Handled = true;
}

private void OnGetVerbsForWrappedParcel(Entity<WrappedParcelComponent> entity,
ref GetVerbsEvent<InteractionVerb> args)
{
if (!args.CanAccess)
return;

args.Verbs.Add(new InteractionVerb
{
Text = Loc.GetString("parcel-wrap-verb-unwrap"),
Act = () => UnwrapInternal(entity),
});
}

private void OnDestroyed<T>(Entity<WrappedParcelComponent> parcel, ref T args)
{
// Unwrap the package and if something was in it, show a popup describing "wow something came out!"
if (UnwrapInternal(parcel) is { } contents)
{
var parcelId = Identity.Name(contents, EntityManager);
_popup.PopupPredicted(Loc.GetString("parcel-wrap-popup-parcel-destroyed", ("contents", contents)),
contents,
null,
PopupType.MediumCaution);
}
}

#region accessors

/// <summary>
/// Returns whether or not <paramref name="wrapper"/> can be used to wrap <paramref name="target"/>.
/// </summary>
public bool IsWrappable(Entity<ParcelWrapComponent> wrapper, EntityUid target) =>
// Wrapping cannot wrap itself
wrapper.Owner != target &&
// Only wrap items
HasComp<ItemComponent>(target) &&
_whitelist.IsBlacklistFail(wrapper.Comp.Blacklist, target);

#endregion

#region internalImplementation

/// <summary>
/// Spawns a WrappedParcel containing <paramref name="target"/>.
/// </summary>
/// <param name="user">The entity using <paramref name="wrapper"/> to wrap <paramref name="target"/>.</param>
/// <param name="wrapper">The wrapping being used. Determines appearance of the spawned parcel.</param>
/// <param name="target">The entity being wrapped.</param>
/// <returns>The newly created parcel. Returns null only in exceptional failure cases.</returns>
private Entity<WrappedParcelComponent>? WrapInternal(EntityUid user, ParcelWrapComponent wrapper, EntityUid target)
{
var spawned = Spawn("WrappedParcel", Transform(target).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.
ItemComponent? targetItemComp = null;
var size = wrapper.FallbackItemSize;
if (wrapper.WrappedItemsMaintainSize && Resolve(target, ref targetItemComp, logMissing: false))
{
size = targetItemComp.Size;
}

var item = Comp<ItemComponent>(spawned);
_item.SetSize(spawned, size, item);
_appearance.SetData(spawned, WrappedParcelVisuals.Size, size.Id);

// If this wrap maintains the shape when wrapping and the item has a shape override, copy the shape override to
// the parcel.
if (wrapper.WrappedItemsMaintainShape && Resolve(target, ref targetItemComp, logMissing: false) &&
targetItemComp.Shape is { } 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);
return null;
}

// Play a wrapping sound.
_audio.PlayPvs(wrapper.WrapSound, spawned);

return (spawned, parcel);
}

/// <summary>
/// Despawns <paramref name="parcel"/>, leaving the contained entity where the parcel was.
/// </summary>
/// <param name="parcel">The entity being unwrapped.</param>
/// <returns>
/// The newly unwrapped, contained entity. Returns null only in the exceptional case that the parcel contained
/// nothing, which should be prevented by not creating such parcels.
/// </returns>
private EntityUid? UnwrapInternal(Entity<WrappedParcelComponent> parcel)
{
var parcelCoords = Comp<TransformComponent>(parcel).Coordinates;

var containedEntity = parcel.Comp.Contents.ContainedEntity;
if (containedEntity is { } parcelContents)
{
_container.Remove(parcelContents,
parcel.Comp.Contents,
true,
true,
parcelCoords);

// If the parcel is in a container, try to put the unwrapped contents in that container.
if (_container.TryGetContainingContainer((parcel, null, null), out var outerContainer))
{
// Make space in the container for the parcel contents.
_container.Remove((parcel, null, null), outerContainer, force: true);
_container.InsertOrDrop((parcelContents, null, null), outerContainer);
}
}

// Make some trash and play an unwrapping sound.
var trash = Spawn(parcel.Comp.UnwrapTrash, parcelCoords);
_transform.DropNextTo((trash, null), (parcel, null));
_audio.PlayPvs(parcel.Comp.UnwrapSound, parcelCoords);

EntityManager.DeleteEntity(parcel);

return containedEntity;
}

#endregion
}
13 changes: 13 additions & 0 deletions Content.Shared/ParcelWrap/Components/WrappedParcelVisuals.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using Robust.Shared.Serialization;

namespace Content.Shared.ParcelWrap.Components;

/// <summary>
/// This enum is used to change the sprite used by WrappedParcels based on the parcel's size.
/// </summary>
[Serializable, NetSerializable]
public enum WrappedParcelVisuals : byte
{
Size,
Layer,
}
4 changes: 4 additions & 0 deletions Resources/Locale/en-US/pacel-wrap.ftl
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
parcel-wrap-verb-wrap = Wrap
parcel-wrap-verb-unwrap = Unwrap
parcel-wrap-popup-parcel-destroyed = The wrapping containing { THE($contents) } is destroyed!
Loading

0 comments on commit 0f29e4e

Please sign in to comment.