Rejigging Item slots (#4933)
* itemslot overhaul * remove "shared" prefix * handle component shutdown * comments, cleanup, tests * comments and minor tweak * rename ItemSlotManagerState * fix swapping * fix merge * Add ItemSlot verb text override * fix merge (IEntity -> entityUid) * Fix merge (LabelSystem) * Fix merge (nuke disk) * fix test
This commit is contained in:
@@ -1,23 +0,0 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Content.Shared.Containers.ItemSlots
|
||||
{
|
||||
/// <summary>
|
||||
/// Item was placed in or removed from one of the slots in <see cref="SharedItemSlotsComponent"/>
|
||||
/// </summary>
|
||||
public class ItemSlotChangedEvent : EntityEventArgs
|
||||
{
|
||||
public SharedItemSlotsComponent SlotsComponent;
|
||||
public string SlotName;
|
||||
public ItemSlot Slot;
|
||||
public readonly EntityUid? ContainedItem;
|
||||
|
||||
public ItemSlotChangedEvent(SharedItemSlotsComponent slotsComponent, string slotName, ItemSlot slot)
|
||||
{
|
||||
SlotsComponent = slotsComponent;
|
||||
SlotName = slotName;
|
||||
Slot = slot;
|
||||
ContainedItem = slot.ContainerSlot.ContainedEntity?.Uid;
|
||||
}
|
||||
}
|
||||
}
|
||||
124
Content.Shared/Containers/ItemSlot/ItemSlotsComponent.cs
Normal file
124
Content.Shared/Containers/ItemSlot/ItemSlotsComponent.cs
Normal file
@@ -0,0 +1,124 @@
|
||||
using Content.Shared.Sound;
|
||||
using Content.Shared.Whitelist;
|
||||
using Robust.Shared.Analyzers;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
using Robust.Shared.ViewVariables;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Content.Shared.Containers.ItemSlots
|
||||
{
|
||||
/// <summary>
|
||||
/// Used for entities that can hold items in different slots. Needed by ItemSlotSystem to support basic
|
||||
/// insert/eject interactions.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
[Friend(typeof(ItemSlotsSystem))]
|
||||
public class ItemSlotsComponent : Component
|
||||
{
|
||||
public override string Name => "ItemSlots";
|
||||
|
||||
[ViewVariables]
|
||||
[DataField("slots")]
|
||||
public Dictionary<string, ItemSlot> Slots = new();
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class ItemSlotsComponentState : ComponentState
|
||||
{
|
||||
public readonly Dictionary<string, bool> SlotLocked;
|
||||
|
||||
public ItemSlotsComponentState(Dictionary<string, ItemSlot> slots)
|
||||
{
|
||||
SlotLocked = new(slots.Count);
|
||||
|
||||
foreach (var (key, slot) in slots)
|
||||
{
|
||||
SlotLocked[key] = slot.Locked;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is effectively a wrapper for a ContainerSlot that adds content functionality like entity whitelists and
|
||||
/// insert/eject sounds.
|
||||
/// </summary>
|
||||
[DataDefinition]
|
||||
[Friend(typeof(ItemSlotsSystem))]
|
||||
public class ItemSlot
|
||||
{
|
||||
[DataField("whitelist")]
|
||||
public EntityWhitelist? Whitelist;
|
||||
|
||||
[DataField("insertSound")]
|
||||
public SoundSpecifier? InsertSound;
|
||||
// maybe default to /Audio/Weapons/Guns/MagIn/batrifle_magin.ogg ??
|
||||
|
||||
[DataField("ejectSound")]
|
||||
public SoundSpecifier? EjectSound;
|
||||
// maybe default to /Audio/Machines/id_swipe.ogg?
|
||||
|
||||
/// <summary>
|
||||
/// The name of this item slot. This will be shown to the user in the verb menu.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This will be passed through Loc.GetString. If the name is an empty string, then verbs will use the name
|
||||
/// of the currently held or currently inserted entity instead.
|
||||
/// </remarks>
|
||||
[DataField("name")]
|
||||
public string Name = string.Empty;
|
||||
|
||||
[DataField("startingItem", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
|
||||
public string? StartingItem;
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not an item can currently be ejected or inserted from this slot.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This doesn't have to mean the slot is somehow physically locked. In the case of the item cabinet, the
|
||||
/// cabinet may simply be closed at the moment and needs to be opened first.
|
||||
/// </remarks>
|
||||
[DataField("locked")]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public bool Locked = false;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the item slots system will attempt to eject this item to the user's hands when interacted with.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// For most item slots, this is probably not the case (eject is usually an alt-click interaction). But
|
||||
/// there are some exceptions. For example item cabinets and charging stations should probably eject their
|
||||
/// contents when clicked on normally.
|
||||
/// </remarks>
|
||||
[DataField("ejectOnInteract")]
|
||||
public bool EjectOnInteract = false;
|
||||
|
||||
/// <summary>
|
||||
/// Override the insert verb text. Defaults to [insert category] -> [item-name]. If not null, the verb will
|
||||
/// not be given a category.
|
||||
/// </summary>
|
||||
[DataField("insertVerbText")]
|
||||
public string? InsertVerbText;
|
||||
|
||||
/// <summary>
|
||||
/// Override the insert verb text. Defaults to [eject category] -> [item-name]. If not null, the verb will
|
||||
/// not be given a category.
|
||||
/// </summary>
|
||||
[DataField("ejectVerbText")]
|
||||
public string? EjectVerbText;
|
||||
|
||||
[ViewVariables]
|
||||
public ContainerSlot ContainerSlot = default!;
|
||||
|
||||
public string ID => ContainerSlot.ID;
|
||||
|
||||
// Convenience properties
|
||||
public bool HasItem => ContainerSlot.ContainedEntity != null;
|
||||
public IEntity? Item => ContainerSlot.ContainedEntity;
|
||||
}
|
||||
}
|
||||
455
Content.Shared/Containers/ItemSlot/ItemSlotsSystem.cs
Normal file
455
Content.Shared/Containers/ItemSlot/ItemSlotsSystem.cs
Normal file
@@ -0,0 +1,455 @@
|
||||
using Content.Shared.ActionBlocker;
|
||||
using Content.Shared.Hands.Components;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Verbs;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Utility;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Content.Shared.Containers.ItemSlots
|
||||
{
|
||||
/// <summary>
|
||||
/// A class that handles interactions related to inserting/ejecting items into/from an item slot.
|
||||
/// </summary>
|
||||
public class ItemSlotsSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<ItemSlotsComponent, ComponentStartup>(OnStartup);
|
||||
SubscribeLocalEvent<ItemSlotsComponent, ComponentInit>(Oninitialize);
|
||||
|
||||
SubscribeLocalEvent<ItemSlotsComponent, InteractUsingEvent>(OnInteractUsing);
|
||||
SubscribeLocalEvent<ItemSlotsComponent, InteractHandEvent>(OnInteractHand);
|
||||
|
||||
SubscribeLocalEvent<ItemSlotsComponent, GetAlternativeVerbsEvent>(AddEjectVerbs);
|
||||
SubscribeLocalEvent<ItemSlotsComponent, GetInteractionVerbsEvent>(AddInteractionVerbsVerbs);
|
||||
|
||||
SubscribeLocalEvent<ItemSlotsComponent, ComponentGetState>(GetItemSlotsState);
|
||||
SubscribeLocalEvent<ItemSlotsComponent, ComponentHandleState>(HandleItemSlotsState);
|
||||
}
|
||||
|
||||
#region ComponentManagement
|
||||
/// <summary>
|
||||
/// Spawn in starting items for any item slots that should have one.
|
||||
/// </summary>
|
||||
private void OnStartup(EntityUid uid, ItemSlotsComponent itemSlots, ComponentStartup args)
|
||||
{
|
||||
foreach (var slot in itemSlots.Slots.Values)
|
||||
{
|
||||
if (slot.HasItem || string.IsNullOrEmpty(slot.StartingItem))
|
||||
continue;
|
||||
|
||||
var item = EntityManager.SpawnEntity(slot.StartingItem, itemSlots.Owner.Transform.Coordinates);
|
||||
slot.ContainerSlot.Insert(item);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensure item slots have containers.
|
||||
/// </summary>
|
||||
private void Oninitialize(EntityUid uid, ItemSlotsComponent itemSlots, ComponentInit args)
|
||||
{
|
||||
foreach (var (id, slot) in itemSlots.Slots)
|
||||
{
|
||||
slot.ContainerSlot = ContainerHelpers.EnsureContainer<ContainerSlot>(itemSlots.Owner, id);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Given a new item slot, store it in the <see cref="ItemSlotsComponent"/> and ensure the slot has an item
|
||||
/// container.
|
||||
/// </summary>
|
||||
public void AddItemSlot(EntityUid uid, string id, ItemSlot slot)
|
||||
{
|
||||
var itemSlots = EntityManager.EnsureComponent<ItemSlotsComponent>(uid);
|
||||
slot.ContainerSlot = ContainerHelpers.EnsureContainer<ContainerSlot>(itemSlots.Owner, id);
|
||||
DebugTools.Assert(!itemSlots.Slots.ContainsKey(id));
|
||||
itemSlots.Slots[id] = slot;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove an item slot. This should generally be called whenever a component that added a slot is being
|
||||
/// removed.
|
||||
/// </summary>
|
||||
public void RemoveItemSlot(EntityUid uid, ItemSlot slot, ItemSlotsComponent? itemSlots = null)
|
||||
{
|
||||
slot.ContainerSlot.Shutdown();
|
||||
|
||||
// Don't log missing resolves. when an entity has all of its components removed, the ItemSlotsComponent may
|
||||
// have been removed before some other component that added an item slot (and is now trying to remove it).
|
||||
if (!Resolve(uid, ref itemSlots, logMissing: false))
|
||||
return;
|
||||
|
||||
itemSlots.Slots.Remove(slot.ID);
|
||||
|
||||
if (itemSlots.Slots.Count == 0)
|
||||
EntityManager.RemoveComponent(uid, itemSlots);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Interactions
|
||||
/// <summary>
|
||||
/// Attempt to take an item from a slot, if any are set to EjectOnInteract.
|
||||
/// </summary>
|
||||
private void OnInteractHand(EntityUid uid, ItemSlotsComponent itemSlots, InteractHandEvent args)
|
||||
{
|
||||
if (args.Handled)
|
||||
return;
|
||||
|
||||
foreach (var slot in itemSlots.Slots.Values)
|
||||
{
|
||||
if (slot.Locked || !slot.EjectOnInteract || slot.Item == null)
|
||||
continue;
|
||||
|
||||
args.Handled = true;
|
||||
TryEjectToHands(uid, slot, args.UserUid);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to insert a held item in any fitting item slot. If a valid slot already contains an item, it will
|
||||
/// swap it out and place the old one in the user's hand.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This only handles the event if the user has an applicable entity that can be inserted. This allows for
|
||||
/// other interactions to still happen (e.g., open UI, or toggle-open), despite the user holding an item.
|
||||
/// Maybe this is undesirable.
|
||||
/// </remarks>
|
||||
private void OnInteractUsing(EntityUid uid, ItemSlotsComponent itemSlots, InteractUsingEvent args)
|
||||
{
|
||||
if (args.Handled)
|
||||
return;
|
||||
|
||||
if (!EntityManager.TryGetComponent(args.UserUid, out SharedHandsComponent? hands))
|
||||
return;
|
||||
|
||||
foreach (var slot in itemSlots.Slots.Values)
|
||||
{
|
||||
if (!CanInsert(args.UsedUid, slot, swap: true))
|
||||
continue;
|
||||
|
||||
// Drop the held item onto the floor. Return if the user cannot drop.
|
||||
if (!hands.Drop(args.Used))
|
||||
return;
|
||||
|
||||
if (slot.Item != null)
|
||||
hands.TryPutInAnyHand(slot.Item);
|
||||
|
||||
Insert(uid, slot, args.Used);
|
||||
args.Handled = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Insert
|
||||
/// <summary>
|
||||
/// Insert an item into a slot. This does not perform checks, so make sure to also use <see
|
||||
/// cref="CanInsert"/> or just use <see cref="TryInsert"/> instead.
|
||||
/// </summary>
|
||||
private void Insert(EntityUid uid, ItemSlot slot, IEntity item)
|
||||
{
|
||||
slot.ContainerSlot.Insert(item);
|
||||
// ContainerSlot automatically raises a directed EntInsertedIntoContainerMessage
|
||||
|
||||
if (slot.InsertSound != null)
|
||||
SoundSystem.Play(Filter.Pvs(uid), slot.InsertSound.GetSound(), uid);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check whether a given item can be inserted into a slot. Unless otherwise specified, this will return
|
||||
/// false if the slot is already filled.
|
||||
/// </summary>
|
||||
public bool CanInsert(EntityUid uid, ItemSlot slot, bool swap = false)
|
||||
{
|
||||
if (slot.Locked)
|
||||
return false;
|
||||
|
||||
if (!swap && slot.HasItem)
|
||||
return false;
|
||||
|
||||
if (slot.Whitelist != null && !slot.Whitelist.IsValid(uid))
|
||||
return false;
|
||||
|
||||
// We should also check ContainerSlot.CanInsert, but that prevents swapping interactions. Given that
|
||||
// ContainerSlot.CanInsert gets called when the item is actually inserted anyways, we can just get away with
|
||||
// fudging CanInsert and not performing those checks.
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to insert item into a specific slot.
|
||||
/// </summary>
|
||||
/// <returns>False if failed to insert item</returns>
|
||||
public bool TryInsert(EntityUid uid, string id, IEntity item, ItemSlotsComponent? itemSlots = null)
|
||||
{
|
||||
if (!Resolve(uid, ref itemSlots))
|
||||
return false;
|
||||
|
||||
if (!itemSlots.Slots.TryGetValue(id, out var slot))
|
||||
return false;
|
||||
|
||||
return TryInsert(uid, slot, item);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to insert item into a specific slot.
|
||||
/// </summary>
|
||||
/// <returns>False if failed to insert item</returns>
|
||||
public bool TryInsert(EntityUid uid, ItemSlot slot, IEntity item)
|
||||
{
|
||||
if (!CanInsert(item.Uid, slot))
|
||||
return false;
|
||||
|
||||
Insert(uid, slot, item);
|
||||
return true;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Eject
|
||||
/// <summary>
|
||||
/// Eject an item into a slot. This does not perform checks (e.g., is the slot locked?), so you should
|
||||
/// probably just use <see cref="TryEject"/> instead.
|
||||
/// </summary>
|
||||
private void Eject(EntityUid uid, ItemSlot slot, IEntity item)
|
||||
{
|
||||
slot.ContainerSlot.Remove(item);
|
||||
// ContainerSlot automatically raises a directed EntRemovedFromContainerMessage
|
||||
|
||||
if (slot.EjectSound != null)
|
||||
SoundSystem.Play(Filter.Pvs(uid), slot.EjectSound.GetSound(), uid);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to eject an item from a slot.
|
||||
/// </summary>
|
||||
/// <returns>False if item slot is locked or has no item inserted</returns>
|
||||
public bool TryEject(EntityUid uid, ItemSlot slot, [NotNullWhen(true)] out IEntity? item)
|
||||
{
|
||||
item = null;
|
||||
|
||||
if (slot.Locked || slot.Item == null)
|
||||
return false;
|
||||
|
||||
item = slot.Item;
|
||||
Eject(uid, slot, item);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to eject item from a slot.
|
||||
/// </summary>
|
||||
/// <returns>False if the id is not valid, the item slot is locked, or it has no item inserted</returns>
|
||||
public bool TryEject(EntityUid uid, string id, [NotNullWhen(true)] out IEntity? item, ItemSlotsComponent? itemSlots = null)
|
||||
{
|
||||
item = null;
|
||||
|
||||
if (!Resolve(uid, ref itemSlots))
|
||||
return false;
|
||||
|
||||
if (!itemSlots.Slots.TryGetValue(id, out var slot))
|
||||
return false;
|
||||
|
||||
return TryEject(uid, slot, out item);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to eject item from a slot directly into a user's hands. If they have no hands, the item will still
|
||||
/// be ejected onto the floor.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// False if the id is not valid, the item slot is locked, or it has no item inserted. True otherwise, even
|
||||
/// if the user has no hands.
|
||||
/// </returns>
|
||||
public bool TryEjectToHands(EntityUid uid, ItemSlot slot, EntityUid? user)
|
||||
{
|
||||
if (!TryEject(uid, slot, out var item))
|
||||
return false;
|
||||
|
||||
if (user != null && EntityManager.TryGetComponent(user.Value, out SharedHandsComponent? hands))
|
||||
hands.TryPutInAnyHand(item);
|
||||
|
||||
return true;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Verbs
|
||||
private void AddEjectVerbs(EntityUid uid, ItemSlotsComponent itemSlots, GetAlternativeVerbsEvent args)
|
||||
{
|
||||
if (args.Hands == null || !args.CanAccess ||!args.CanInteract ||
|
||||
!_actionBlockerSystem.CanPickup(args.User.Uid))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var slot in itemSlots.Slots.Values)
|
||||
{
|
||||
if (slot.Locked || !slot.HasItem)
|
||||
continue;
|
||||
|
||||
if (slot.EjectOnInteract)
|
||||
// For this item slot, ejecting/inserting is a primary interaction. Instead of an eject category
|
||||
// alt-click verb, there will be a "Take item" primary interaction verb.
|
||||
continue;
|
||||
|
||||
var verbSubject = slot.Name != string.Empty
|
||||
? Loc.GetString(slot.Name)
|
||||
: slot.Item!.Name ?? string.Empty;
|
||||
|
||||
Verb verb = new();
|
||||
verb.Act = () => TryEjectToHands(uid, slot, args.User.Uid);
|
||||
|
||||
if (slot.EjectVerbText == null)
|
||||
{
|
||||
verb.Text = verbSubject;
|
||||
verb.Category = VerbCategory.Eject;
|
||||
}
|
||||
else
|
||||
{
|
||||
verb.Text = Loc.GetString(slot.EjectVerbText);
|
||||
verb.IconTexture = "/Textures/Interface/VerbIcons/eject.svg.192dpi.png";
|
||||
}
|
||||
|
||||
args.Verbs.Add(verb);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddInteractionVerbsVerbs(EntityUid uid, ItemSlotsComponent itemSlots, GetInteractionVerbsEvent args)
|
||||
{
|
||||
if (args.Hands == null || !args.CanAccess || !args.CanInteract)
|
||||
return;
|
||||
|
||||
// If there are any slots that eject on left-click, add a "Take <item>" verb.
|
||||
if (_actionBlockerSystem.CanPickup(args.User.Uid))
|
||||
{
|
||||
foreach (var slot in itemSlots.Slots.Values)
|
||||
{
|
||||
if (!slot.EjectOnInteract || slot.Locked || !slot.HasItem)
|
||||
continue;
|
||||
|
||||
var verbSubject = slot.Name != string.Empty
|
||||
? Loc.GetString(slot.Name)
|
||||
: slot.Item!.Name ?? string.Empty;
|
||||
|
||||
Verb takeVerb = new();
|
||||
takeVerb.Act = () => TryEjectToHands(uid, slot, args.User.Uid);
|
||||
takeVerb.IconTexture = "/Textures/Interface/VerbIcons/pickup.svg.192dpi.png";
|
||||
|
||||
if (slot.EjectVerbText == null)
|
||||
takeVerb.Text = Loc.GetString("take-item-verb-text", ("subject", verbSubject));
|
||||
else
|
||||
takeVerb.Text = Loc.GetString(slot.EjectVerbText);
|
||||
|
||||
args.Verbs.Add(takeVerb);
|
||||
}
|
||||
}
|
||||
|
||||
// Next, add the insert-item verbs
|
||||
if (args.Using == null || !_actionBlockerSystem.CanDrop(args.User.Uid))
|
||||
return;
|
||||
|
||||
foreach (var slot in itemSlots.Slots.Values)
|
||||
{
|
||||
if (!CanInsert(args.Using.Uid, slot))
|
||||
continue;
|
||||
|
||||
var verbSubject = slot.Name != string.Empty
|
||||
? Loc.GetString(slot.Name)
|
||||
: args.Using.Name ?? string.Empty;
|
||||
|
||||
Verb insertVerb = new();
|
||||
insertVerb.Act = () => Insert(uid, slot, args.Using);
|
||||
|
||||
if (slot.InsertVerbText != null)
|
||||
{
|
||||
insertVerb.Text = Loc.GetString(slot.InsertVerbText);
|
||||
insertVerb.IconTexture = "/Textures/Interface/VerbIcons/insert.svg.192dpi.png";
|
||||
}
|
||||
else if(slot.EjectOnInteract)
|
||||
{
|
||||
// Inserting/ejecting is a primary interaction for this entity. Instead of using the insert
|
||||
// category, we will use a single "Place <item>" verb.
|
||||
insertVerb.Text = Loc.GetString("place-item-verb-text", ("subject", verbSubject));
|
||||
insertVerb.IconTexture = "/Textures/Interface/VerbIcons/drop.svg.192dpi.png";
|
||||
}
|
||||
else
|
||||
{
|
||||
insertVerb.Category = VerbCategory.Insert;
|
||||
insertVerb.Text = verbSubject;
|
||||
}
|
||||
|
||||
args.Verbs.Add(insertVerb);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Get the contents of some item slot.
|
||||
/// </summary>
|
||||
public IEntity? GetItem(EntityUid uid, string id, ItemSlotsComponent? itemSlots = null)
|
||||
{
|
||||
if (!Resolve(uid, ref itemSlots))
|
||||
return null;
|
||||
|
||||
return itemSlots.Slots.GetValueOrDefault(id)?.Item;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lock an item slot. This stops items from being inserted into or ejected from this slot.
|
||||
/// </summary>
|
||||
public void SetLock(EntityUid uid, string id, bool locked, ItemSlotsComponent? itemSlots = null)
|
||||
{
|
||||
if (!Resolve(uid, ref itemSlots))
|
||||
return;
|
||||
|
||||
if (!itemSlots.Slots.TryGetValue(id, out var slot))
|
||||
return;
|
||||
|
||||
SetLock(itemSlots, slot, locked);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lock an item slot. This stops items from being inserted into or ejected from this slot.
|
||||
/// </summary>
|
||||
public void SetLock(ItemSlotsComponent itemSlots, ItemSlot slot, bool locked)
|
||||
{
|
||||
slot.Locked = locked;
|
||||
itemSlots.Dirty();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the locked state of the managed item slots.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Note that the slot's ContainerSlot performs its own networking, so we don't need to send information
|
||||
/// about the contained entity.
|
||||
/// </remarks>
|
||||
private void HandleItemSlotsState(EntityUid uid, ItemSlotsComponent component, ref ComponentHandleState args)
|
||||
{
|
||||
if (args.Current is not ItemSlotsComponentState state)
|
||||
return;
|
||||
|
||||
foreach (var (id, locked) in state.SlotLocked)
|
||||
{
|
||||
component.Slots[id].Locked = locked;
|
||||
}
|
||||
}
|
||||
|
||||
private void GetItemSlotsState(EntityUid uid, ItemSlotsComponent component, ref ComponentGetState args)
|
||||
{
|
||||
args.State = new ItemSlotsComponentState(component.Slots);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
using Content.Shared.Sound;
|
||||
using Content.Shared.Whitelist;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
using Robust.Shared.ViewVariables;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Content.Shared.Containers.ItemSlots
|
||||
{
|
||||
/// <summary>
|
||||
/// Used for entities that can hold items in different slots
|
||||
/// Allows basic insert/eject interaction
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public class SharedItemSlotsComponent : Component
|
||||
{
|
||||
public override string Name => "ItemSlots";
|
||||
|
||||
[ViewVariables] [DataField("slots")] public Dictionary<string, ItemSlot> Slots = new();
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
[DataDefinition]
|
||||
public class ItemSlot
|
||||
{
|
||||
[ViewVariables] [DataField("whitelist")] public EntityWhitelist? Whitelist;
|
||||
[ViewVariables] [DataField("insertSound")] public SoundSpecifier? InsertSound;
|
||||
[ViewVariables] [DataField("ejectSound")] public SoundSpecifier? EjectSound;
|
||||
|
||||
/// <summary>
|
||||
/// The name of this item slot. This will be shown to the user in the verb menu.
|
||||
/// </summary>
|
||||
[ViewVariables] public string Name
|
||||
{
|
||||
get => _name != string.Empty
|
||||
? Loc.GetString(_name)
|
||||
: ContainerSlot.ContainedEntity?.Name ?? string.Empty;
|
||||
set => _name = value;
|
||||
}
|
||||
[DataField("name")] private string _name = string.Empty;
|
||||
|
||||
[DataField("item", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
|
||||
[ViewVariables] public string? StartingItem;
|
||||
|
||||
[ViewVariables] public ContainerSlot ContainerSlot = default!;
|
||||
|
||||
public bool HasEntity => ContainerSlot.ContainedEntity != null;
|
||||
}
|
||||
}
|
||||
@@ -1,255 +0,0 @@
|
||||
using Content.Shared.ActionBlocker;
|
||||
using Content.Shared.Hands.Components;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Verbs;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Content.Shared.Containers.ItemSlots
|
||||
{
|
||||
public class SharedItemSlotsSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<SharedItemSlotsComponent, ComponentInit>(OnComponentInit);
|
||||
SubscribeLocalEvent<SharedItemSlotsComponent, MapInitEvent>(OnMapInit);
|
||||
SubscribeLocalEvent<SharedItemSlotsComponent, InteractUsingEvent>(OnInteractUsing);
|
||||
|
||||
SubscribeLocalEvent<SharedItemSlotsComponent, GetAlternativeVerbsEvent>(AddEjectVerbs);
|
||||
SubscribeLocalEvent<SharedItemSlotsComponent, GetInteractionVerbsEvent>(AddInsertVerbs);
|
||||
}
|
||||
|
||||
private void OnComponentInit(EntityUid uid, SharedItemSlotsComponent itemSlots, ComponentInit args)
|
||||
{
|
||||
// create container for each slot
|
||||
foreach (var pair in itemSlots.Slots)
|
||||
{
|
||||
var slotName = pair.Key;
|
||||
var slot = pair.Value;
|
||||
|
||||
slot.ContainerSlot = ContainerHelpers.EnsureContainer<ContainerSlot>(itemSlots.Owner, slotName);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnMapInit(EntityUid uid, SharedItemSlotsComponent itemSlots, MapInitEvent args)
|
||||
{
|
||||
foreach (var pair in itemSlots.Slots)
|
||||
{
|
||||
var slot = pair.Value;
|
||||
var slotName = pair.Key;
|
||||
|
||||
// Check if someone already put item inside container
|
||||
if (slot.ContainerSlot.ContainedEntity != null)
|
||||
continue;
|
||||
|
||||
// Try to spawn item inside each slot
|
||||
if (!string.IsNullOrEmpty(slot.StartingItem))
|
||||
{
|
||||
var item = EntityManager.SpawnEntity(slot.StartingItem, itemSlots.Owner.Transform.Coordinates);
|
||||
slot.ContainerSlot.Insert(item);
|
||||
|
||||
RaiseLocalEvent(uid, new ItemSlotChangedEvent(itemSlots, slotName, slot));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void AddEjectVerbs(EntityUid uid, SharedItemSlotsComponent component, GetAlternativeVerbsEvent args)
|
||||
{
|
||||
if (args.Hands == null ||
|
||||
!args.CanAccess ||
|
||||
!args.CanInteract ||
|
||||
!_actionBlockerSystem.CanPickup(args.User.Uid))
|
||||
return;
|
||||
|
||||
foreach (var (slotName, slot) in component.Slots)
|
||||
{
|
||||
if (slot.ContainerSlot.ContainedEntity == null)
|
||||
continue;
|
||||
|
||||
Verb verb = new();
|
||||
verb.Text = slot.Name;
|
||||
verb.Category = VerbCategory.Eject;
|
||||
verb.Act = () => TryEjectContent(uid, slotName, args.User, component);
|
||||
|
||||
args.Verbs.Add(verb);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddInsertVerbs(EntityUid uid, SharedItemSlotsComponent component, GetInteractionVerbsEvent args)
|
||||
{
|
||||
if (args.Using == null ||
|
||||
!args.CanAccess ||
|
||||
!args.CanInteract ||
|
||||
!_actionBlockerSystem.CanDrop(args.User.Uid))
|
||||
return;
|
||||
|
||||
foreach (var (slotName, slot) in component.Slots)
|
||||
{
|
||||
if (!CanInsertContent(args.Using, slot))
|
||||
continue;
|
||||
|
||||
Verb verb = new();
|
||||
verb.Text = slot.Name != string.Empty ? slot.Name : args.Using.Name;
|
||||
verb.Category = VerbCategory.Insert;
|
||||
verb.Act = () => InsertContent(component, slot, slotName, args.Using);
|
||||
args.Verbs.Add(verb);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnInteractUsing(EntityUid uid, SharedItemSlotsComponent itemSlots, InteractUsingEvent args)
|
||||
{
|
||||
if (args.Handled)
|
||||
return;
|
||||
|
||||
args.Handled = TryInsertContent(uid, args.Used, args.User, itemSlots);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to insert or swap an item in any fitting item slot from users hand. If a valid slot already contains an item, it will swap it out.
|
||||
/// </summary>
|
||||
/// <returns>False if failed to insert item</returns>
|
||||
public bool TryInsertContent(EntityUid uid, IEntity item, IEntity user, SharedItemSlotsComponent? itemSlots = null, SharedHandsComponent? hands = null)
|
||||
{
|
||||
if (!Resolve(uid, ref itemSlots))
|
||||
return false;
|
||||
|
||||
if (!Resolve(user.Uid, ref hands))
|
||||
{
|
||||
itemSlots.Owner.PopupMessage(user, Loc.GetString("item-slots-try-insert-no-hands"));
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var (slotName, slot) in itemSlots.Slots)
|
||||
{
|
||||
// check if item allowed in whitelist
|
||||
if (slot.Whitelist != null && !slot.Whitelist.IsValid(item.Uid))
|
||||
continue;
|
||||
|
||||
// check if slot does not contain the item currently being inserted???
|
||||
if (slot.ContainerSlot.Contains(item))
|
||||
continue;
|
||||
|
||||
// get item inside container
|
||||
IEntity? swap = null;
|
||||
if (slot.ContainerSlot.ContainedEntity != null)
|
||||
swap = slot.ContainerSlot.ContainedEntity;
|
||||
|
||||
// return if user can't drop active item in hand
|
||||
if (!hands.Drop(item))
|
||||
return true;
|
||||
|
||||
// swap item in hand and item in slot
|
||||
if (swap != null)
|
||||
hands.TryPutInAnyHand(swap);
|
||||
|
||||
InsertContent(itemSlots, slot, slotName, item);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void InsertContent(SharedItemSlotsComponent itemSlots, ItemSlot slot, string slotName, IEntity item)
|
||||
{
|
||||
// insert item
|
||||
slot.ContainerSlot.Insert(item);
|
||||
RaiseLocalEvent(itemSlots.OwnerUid, new ItemSlotChangedEvent(itemSlots, slotName, slot));
|
||||
|
||||
// play sound
|
||||
if (slot.InsertSound != null)
|
||||
SoundSystem.Play(Filter.Pvs(itemSlots.Owner), slot.InsertSound.GetSound(), itemSlots.Owner);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Can a given item be inserted into a slot, without ejecting the current item in that slot.
|
||||
/// </summary>
|
||||
public bool CanInsertContent(IEntity item, ItemSlot slot)
|
||||
{
|
||||
if (slot.ContainerSlot.ContainedEntity != null)
|
||||
return false;
|
||||
|
||||
// check if item allowed in whitelist
|
||||
if (slot.Whitelist != null && !slot.Whitelist.IsValid(item.Uid))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to insert item in known slot. Doesn't interact with user
|
||||
/// </summary>
|
||||
/// <returns>False if failed to insert item</returns>
|
||||
public bool TryInsertContent(SharedItemSlotsComponent itemSlots, IEntity item, string slotName)
|
||||
{
|
||||
if (!itemSlots.Slots.TryGetValue(slotName, out var slot))
|
||||
return false;
|
||||
|
||||
if (!CanInsertContent(item, slot))
|
||||
return false;
|
||||
|
||||
InsertContent(itemSlots, slot, slotName, item);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if slot has some content in it (without ejecting item)
|
||||
/// </summary>
|
||||
/// <returns>Null if doesn't have any content</returns>
|
||||
public IEntity? PeekItemInSlot(SharedItemSlotsComponent itemSlots, string slotName)
|
||||
{
|
||||
if (!itemSlots.Slots.TryGetValue(slotName, out var slot))
|
||||
return null;
|
||||
|
||||
var item = slot.ContainerSlot.ContainedEntity;
|
||||
return item;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to eject item from slot to users hands
|
||||
/// </summary>
|
||||
public bool TryEjectContent(EntityUid uid, string slotName, IEntity? user, SharedItemSlotsComponent? itemSlots = null)
|
||||
{
|
||||
if (!Resolve(uid, ref itemSlots))
|
||||
return false;
|
||||
|
||||
if (!itemSlots.Slots.TryGetValue(slotName, out var slot))
|
||||
return false;
|
||||
|
||||
if (slot.ContainerSlot.ContainedEntity == null)
|
||||
return false;
|
||||
|
||||
var item = slot.ContainerSlot.ContainedEntity;
|
||||
if (!slot.ContainerSlot.Remove(item))
|
||||
return false;
|
||||
|
||||
// try eject item to users hand
|
||||
if (user != null)
|
||||
{
|
||||
if (user.TryGetComponent(out SharedHandsComponent? hands))
|
||||
{
|
||||
hands.TryPutInAnyHand(item);
|
||||
}
|
||||
else
|
||||
{
|
||||
itemSlots.Owner.PopupMessage(user, Loc.GetString("item-slots-try-insert-no-hands"));
|
||||
}
|
||||
}
|
||||
|
||||
if (slot.EjectSound != null)
|
||||
SoundSystem.Play(Filter.Pvs(itemSlots.Owner), slot.EjectSound.GetSound(), itemSlots.Owner);
|
||||
|
||||
RaiseLocalEvent(itemSlots.OwnerUid, new ItemSlotChangedEvent(itemSlots, slotName, slot));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user