diff --git a/Content.Client/Verbs/UI/VerbMenuElement.cs b/Content.Client/Verbs/UI/VerbMenuElement.cs index 09d2f900e4..c1f4f5cd12 100644 --- a/Content.Client/Verbs/UI/VerbMenuElement.cs +++ b/Content.Client/Verbs/UI/VerbMenuElement.cs @@ -41,6 +41,18 @@ namespace Content.Client.Verbs.UI ExpansionIndicator.Visible = true; } + if (verb.Icon == null && verb.IconEntity != null) + { + var spriteView = new SpriteView() + { + OverrideDirection = Direction.South, + Sprite = IoCManager.Resolve().GetComponentOrNull(verb.IconEntity.Value) + }; + + Icon.AddChild(spriteView); + return; + } + Icon.AddChild(new TextureRect() { Texture = verb.Icon?.Frame0(), diff --git a/Content.Client/Verbs/UI/VerbMenuPresenter.cs b/Content.Client/Verbs/UI/VerbMenuPresenter.cs index 3b0fd72b34..65b2132f3e 100644 --- a/Content.Client/Verbs/UI/VerbMenuPresenter.cs +++ b/Content.Client/Verbs/UI/VerbMenuPresenter.cs @@ -107,7 +107,7 @@ namespace Content.Client.Verbs.UI if (verb.Category?.Text == category.Text) { verbsInCategory.Add(verb); - drawIcons = drawIcons || verb.Icon != null; + drawIcons = drawIcons || verb.Icon != null || verb.IconEntity != null; } } diff --git a/Content.Server/Storage/EntitySystems/StorageSystem.cs b/Content.Server/Storage/EntitySystems/StorageSystem.cs index 06f5f01812..270a6e83dc 100644 --- a/Content.Server/Storage/EntitySystems/StorageSystem.cs +++ b/Content.Server/Storage/EntitySystems/StorageSystem.cs @@ -1,4 +1,7 @@ using System.Collections.Generic; +using System.Linq; +using Content.Server.Disposal.Unit.Components; +using Content.Server.Disposal.Unit.EntitySystems; using Content.Server.Hands.Components; using Content.Server.Storage.Components; using Content.Shared.Interaction; @@ -8,9 +11,6 @@ using JetBrains.Annotations; using Robust.Server.GameObjects; using Robust.Server.Player; using Robust.Shared.Containers; -using Robust.Shared.GameObjects; -using Robust.Shared.IoC; -using Robust.Shared.Localization; using Robust.Shared.Random; using Robust.Shared.Timing; @@ -21,6 +21,7 @@ namespace Content.Server.Storage.EntitySystems { [Dependency] private readonly IGameTiming _gameTiming = default!; [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly DisposalUnitSystem _disposalSystem = default!; private readonly List _sessionCache = new(); @@ -33,9 +34,11 @@ namespace Content.Server.Storage.EntitySystems SubscribeLocalEvent(HandleEntityInsertedIntoContainer); SubscribeLocalEvent>(AddToggleOpenVerb); - SubscribeLocalEvent>(AddOpenUiVerb); SubscribeLocalEvent(OnRelayMovement); + SubscribeLocalEvent>(AddOpenUiVerb); + SubscribeLocalEvent>(AddTransferVerbs); + SubscribeLocalEvent(OnStorageFillMapInit); } @@ -117,6 +120,97 @@ namespace Content.Server.Storage.EntitySystems args.Verbs.Add(verb); } + private void AddTransferVerbs(EntityUid uid, ServerStorageComponent component, GetVerbsEvent args) + { + if (!args.CanAccess || !args.CanInteract) + return; + + var entities = component.Storage?.ContainedEntities; + if (entities == null || entities.Count == 0) + return; + + if (TryComp(uid, out LockComponent? lockComponent) && lockComponent.Locked) + return; + + // if the target is storage, add a verb to transfer storage. + if (TryComp(args.Target, out ServerStorageComponent? targetStorage) + && (!TryComp(uid, out LockComponent? targetLock) || !targetLock.Locked)) + { + UtilityVerb verb = new() + { + Text = Loc.GetString("storage-component-transfer-verb"), + IconEntity = args.Using, + Act = () => TransferEntities(uid, args.Target, component, lockComponent, targetStorage, targetLock) + }; + + args.Verbs.Add(verb); + } + + // if the target is a disposal unit, add a verb to transfer storage into the unit (e.g., empty a trash bag). + if (!TryComp(args.Target, out DisposalUnitComponent? disposal)) + return; + + UtilityVerb dispose = new() + { + Text = Loc.GetString("storage-component-dispose-verb"), + IconEntity = args.Using, + Act = () => DisposeEntities(args.User, uid, args.Target, component, lockComponent, disposal) + }; + + args.Verbs.Add(dispose); + } + + /// + /// Move entities from one storage to another. + /// + public void TransferEntities(EntityUid source, EntityUid target, + ServerStorageComponent? sourceComp = null, LockComponent? sourceLock = null, + ServerStorageComponent? targetComp = null, LockComponent? targetLock = null) + { + if (!Resolve(source, ref sourceComp) || !Resolve(target, ref targetComp)) + return; + + var entities = sourceComp.Storage?.ContainedEntities; + if (entities == null || entities.Count == 0) + return; + + if (Resolve(source, ref sourceLock, false) && sourceLock.Locked + || Resolve(target, ref targetLock, false) && targetLock.Locked) + return; + + foreach (var entity in entities.ToList()) + { + targetComp.Insert(entity); + } + } + + /// + /// Move entities from storage into a disposal unit. + /// + public void DisposeEntities(EntityUid user, EntityUid source, EntityUid target, + ServerStorageComponent? sourceComp = null, LockComponent? sourceLock = null, + DisposalUnitComponent? disposalComp = null) + { + if (!Resolve(source, ref sourceComp) || !Resolve(target, ref disposalComp)) + return; + + var entities = sourceComp.Storage?.ContainedEntities; + if (entities == null || entities.Count == 0) + return; + + if (Resolve(source, ref sourceLock, false) && sourceLock.Locked) + return; + + foreach (var entity in entities.ToList()) + { + if (_disposalSystem.CanInsert(disposalComp, entity) + && disposalComp.Container.Insert(entity)) + { + _disposalSystem.AfterInsert(disposalComp, entity); + } + } + } + private void HandleEntityRemovedFromContainer(EntRemovedFromContainerMessage message) { var oldParentEntity = message.Container.Owner; diff --git a/Content.Shared/Containers/ItemSlot/ItemSlotsSystem.cs b/Content.Shared/Containers/ItemSlot/ItemSlotsSystem.cs index 69eb679a26..c1b22e0d82 100644 --- a/Content.Shared/Containers/ItemSlot/ItemSlotsSystem.cs +++ b/Content.Shared/Containers/ItemSlot/ItemSlotsSystem.cs @@ -404,6 +404,7 @@ namespace Content.Shared.Containers.ItemSlots : EntityManager.GetComponent(slot.Item!.Value).EntityName ?? string.Empty; AlternativeVerb verb = new(); + verb.IconEntity = slot.Item; verb.Act = () => TryEjectToHands(uid, slot, args.User, excludeUserAudio: true); if (slot.EjectVerbText == null) @@ -414,7 +415,6 @@ namespace Content.Shared.Containers.ItemSlots else { verb.Text = Loc.GetString(slot.EjectVerbText); - verb.IconTexture = "/Textures/Interface/VerbIcons/eject.svg.192dpi.png"; } args.Verbs.Add(verb); @@ -439,8 +439,8 @@ namespace Content.Shared.Containers.ItemSlots : EntityManager.GetComponent(slot.Item!.Value).EntityName ?? string.Empty; InteractionVerb takeVerb = new(); + takeVerb.IconEntity = slot.Item; takeVerb.Act = () => TryEjectToHands(uid, slot, args.User, excludeUserAudio: true); - takeVerb.IconTexture = "/Textures/Interface/VerbIcons/pickup.svg.192dpi.png"; if (slot.EjectVerbText == null) takeVerb.Text = Loc.GetString("take-item-verb-text", ("subject", verbSubject)); @@ -465,6 +465,7 @@ namespace Content.Shared.Containers.ItemSlots : Name(args.Using.Value) ?? string.Empty; InteractionVerb insertVerb = new(); + insertVerb.IconEntity = args.Using; insertVerb.Act = () => Insert(uid, slot, args.Using.Value, args.User, excludeUserAudio: true); if (slot.InsertVerbText != null) diff --git a/Content.Shared/Verbs/SharedVerbSystem.cs b/Content.Shared/Verbs/SharedVerbSystem.cs index bfde29b3b6..138214f1aa 100644 --- a/Content.Shared/Verbs/SharedVerbSystem.cs +++ b/Content.Shared/Verbs/SharedVerbSystem.cs @@ -47,16 +47,16 @@ namespace Content.Shared.Verbs /// Raises a number of events in order to get all verbs of the given type(s) defined in local systems. This /// does not request verbs from the server. /// - public SortedSet GetLocalVerbs(EntityUid target, EntityUid user, Type type, bool force = false, bool all = false) + public SortedSet GetLocalVerbs(EntityUid target, EntityUid user, Type type, bool force = false) { - return GetLocalVerbs(target, user, new List() { type }, force, all); + return GetLocalVerbs(target, user, new List() { type }, force); } /// /// Raises a number of events in order to get all verbs of the given type(s) defined in local systems. This /// does not request verbs from the server. /// - public SortedSet GetLocalVerbs(EntityUid target, EntityUid user, List types, bool force = false, bool all = false) + public SortedSet GetLocalVerbs(EntityUid target, EntityUid user, List types, bool force = false) { SortedSet verbs = new(); @@ -99,6 +99,15 @@ namespace Content.Shared.Verbs verbs.UnionWith(verbEvent.Verbs); } + if (types.Contains(typeof(UtilityVerb)) + && @using != null + && @using != target) + { + var verbEvent = new GetVerbsEvent(user, target, @using, hands, canInteract, canAccess); + RaiseLocalEvent(@using.Value, verbEvent); // directed at used, not at target + verbs.UnionWith(verbEvent.Verbs); + } + if (types.Contains(typeof(AlternativeVerb))) { var verbEvent = new GetVerbsEvent(user, target, @using, hands, canInteract, canAccess); diff --git a/Content.Shared/Verbs/Verb.cs b/Content.Shared/Verbs/Verb.cs index f718022073..4ad9f3e867 100644 --- a/Content.Shared/Verbs/Verb.cs +++ b/Content.Shared/Verbs/Verb.cs @@ -121,6 +121,12 @@ namespace Content.Shared.Verbs /// public string? IconTexture; + /// + /// If this is not null, and no icon or icon texture were specified, a sprite view of this entity will be + /// used as the icon for this verb. + /// + public EntityUid? IconEntity; + /// /// Whether or not to close the context menu after using it to run this verb. /// @@ -200,6 +206,7 @@ namespace Content.Shared.Verbs { { typeof(Verb) }, { typeof(InteractionVerb) }, + { typeof(UtilityVerb) }, { typeof(AlternativeVerb) }, { typeof(ActivationVerb) }, { typeof(ExamineVerb) } @@ -226,6 +233,27 @@ namespace Content.Shared.Verbs } } + /// + /// These verbs are similar to the normal interaction verbs, except these interactions are facilitated by the + /// currently held entity. + /// + /// + /// The only notable difference between these and InteractionVerbs is that they are obtained by raising an event + /// directed at the currently held entity. Distinguishing between utility and interaction verbs helps avoid + /// confusion if a component enables verbs both when the item is used on something else, or when it is the + /// target of an interaction. These verbs are only obtained if the target and the held entity are NOT the same. + /// + [Serializable, NetSerializable] + public sealed class UtilityVerb : Verb + { + public override int TypePriority => 3; + + public UtilityVerb() : base() + { + TextStyleClass = InteractionVerb.DefaultTextStyleClass; + } + } + /// /// Verbs for alternative-interactions. /// diff --git a/Resources/Locale/en-US/storage/components/storage-component.ftl b/Resources/Locale/en-US/storage/components/storage-component.ftl new file mode 100644 index 0000000000..e27020cd0e --- /dev/null +++ b/Resources/Locale/en-US/storage/components/storage-component.ftl @@ -0,0 +1,2 @@ +storage-component-transfer-verb = Transfer contents +storage-component-dispose-verb = Dispose of contents \ No newline at end of file