CatStation/Content.Shared/Containers/ItemSlots/ItemSlotsComponent.cs
UpAndLeaves dcba448c4c
Smart equip fixes: Allow you to smart-equip guns without... (#45171)
* Smart-Equip fixes: you can now smart-equip guns without accidentally taking out their magazines.

* Because someone WILL mention this in review

* RQC

* Fix after merge

* Whoops

* RQC

* This how one does it?

* Fixed minor edge case

* RQC

* refactor: whitespace

---------

Co-authored-by: Fildrance <fildrance@gmail.com>
2026-08-20 11:53:56 +00:00

247 lines
9.7 KiB
C#

using Content.Shared.Whitelist;
using Robust.Shared.Audio;
using Robust.Shared.Containers;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
namespace Content.Shared.Containers.ItemSlots;
/// <summary>
/// Used for entities that can hold items in different slots. Needed by <see cref="ItemSlotsSystem"/> to support
/// basic insert/eject interactions.
/// </summary>
[RegisterComponent]
[Access(typeof(ItemSlotsSystem))]
[NetworkedComponent]
public sealed partial class ItemSlotsComponent : Component
{
/// <summary>
/// The dictionary that stores all of the item slots whose interactions will be managed by the <see
/// cref="ItemSlotsSystem"/>.
/// </summary>
[DataField(readOnly:true)]
public Dictionary<string, ItemSlot> Slots = new();
// There are two ways to use item slots:
//
// #1 - Give your component an ItemSlot datafield, and add/remove the item slot through the ItemSlotsSystem on
// component init/remove.
//
// #2 - Give your component a key string datafield, and make sure that every entity with that component also has
// an ItemSlots component with a matching key. Then use ItemSlots system to get the slot with this key whenever
// you need it, or just get a reference to the slot on init and store it. This is how generic entity containers
// are usually used.
//
// In order to avoid #1 leading to duplicate slots when saving a map, the Slots dictionary is a read-only
// datafield. This means that if your system/component dynamically changes the item slot (e.g., updating
// whitelist or whatever), you should use #1.
/// <summary>
/// When trying to smart-equip this item, decide whether to allow smart-equip to work on the item slots within this entity.
/// </summary>
/// <remarks>
/// Defaults to false as so many ItemSlots entities do NOT want the smart-equip functionality.
/// False: Items that you more often want to equip than their contents (Guns, battery-powered tools)
/// True: Items that you want to equip their contents more often then themselves (Sabre sheathes, portable rechargers)
/// </remarks>
[DataField]
public bool AllowSmartEquip;
}
/// <summary>
/// The component state for <see cref="ItemSlotsComponent"/>. Any new parameters must be added here and to the correct events in <see cref="ItemSlotsSystem"/>.
/// </summary>
/// <remarks>
/// This can't be auto-generated due to the complexity of <paramref name="Slots"/>.
/// </remarks>
[Serializable, NetSerializable]
public sealed class ItemSlotsComponentState(Dictionary<string, ItemSlot> slots, bool allowSmartEquip) : ComponentState
{
public readonly Dictionary<string, ItemSlot> Slots = slots;
public readonly bool AllowSmartEquip = allowSmartEquip;
}
/// <summary>
/// This is effectively a wrapper for a ContainerSlot that adds content functionality like entity whitelists and
/// insert/eject sounds.
/// </summary>
[DataDefinition]
[Access(typeof(ItemSlotsSystem))]
[Serializable, NetSerializable]
public sealed partial class ItemSlot
{
[DataField]
[Access(typeof(ItemSlotsSystem), Other = AccessPermissions.ReadWriteExecute)]
public EntityWhitelist? Whitelist;
[DataField]
public EntityWhitelist? Blacklist;
[DataField]
public SoundSpecifier? InsertSound = new SoundPathSpecifier("/Audio/Weapons/Guns/MagIn/revolver_magin.ogg");
[DataField]
public SoundSpecifier? EjectSound = new SoundPathSpecifier("/Audio/Weapons/Guns/MagOut/revolver_magout.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(readOnly: true)]
[Access(typeof(ItemSlotsSystem), Other = AccessPermissions.ReadWriteExecute)] // FIXME Friends
public string Name = string.Empty;
/// <summary>
/// The entity prototype that is spawned into this slot on map init.
/// </summary>
/// <remarks>
/// Marked as readOnly because some components (e.g. PowerCellSlot) set the starting item based on some
/// property of that component (e.g., cell slot size category), and this can lead to unnecessary changes
/// when mapping.
/// </remarks>
[DataField(readOnly: true)]
[Access(typeof(ItemSlotsSystem), Other = AccessPermissions.ReadWriteExecute)] // FIXME Friends
[NonSerialized]
public EntProtoId? 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(readOnly: true)]
public bool Locked;
/// <summary>
/// Prevents adding the eject alt-verb and ejecting through BUI, but still lets you swap items.
/// </summary>
/// <remarks>
/// This does not affect EjectOnInteract, since if you do that you probably want ejecting to work.
/// </remarks>
[DataField]
public bool DisableEject;
/// <summary>
/// Whether the item slots system will attempt to insert item from the user's hands into this slot when interacted with.
/// It doesn't block other insertion methods, like verbs.
/// </summary>
[DataField]
public bool InsertOnInteract = true;
/// <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]
public bool EjectOnInteract;
/// <summary>
/// If true, and if this slot is attached to an item, then it will attempt to eject the slot when the item is
/// used in the user's hands.
/// </summary>
/// <remarks>
/// Desirable for things like ranged weapons ('Z' to eject), but not desirable for others (e.g., PDA uses
/// 'Z' to open UI). Unlike <see cref="EjectOnInteract"/>, this will not make any changes to the context
/// menu, nor will it disable alt-click interactions.
/// </remarks>
[DataField]
public bool EjectOnUse;
/// <summary>
/// Override the insert verb text. Defaults to using the slot's name (if specified) or the name of the
/// targeted item. If specified, the verb will not be added to the default insert verb category.
/// </summary>
[DataField]
public string? InsertVerbText;
/// <summary>
/// Override the eject verb text. Defaults to using the slot's name (if specified) or the name of the
/// targeted item. If specified, the verb will not be added to the default eject verb category
/// </summary>
[DataField]
public string? EjectVerbText;
[ViewVariables, NonSerialized]
public ContainerSlot? ContainerSlot;
/// <summary>
/// If this slot belongs to some de-constructible component, should the item inside the slot be ejected upon
/// deconstruction?
/// </summary>
/// <remarks>
/// The actual deconstruction logic is handled by the server-side EmptyOnMachineDeconstructSystem.
/// </remarks>
[DataField]
[Access(typeof(ItemSlotsSystem), Other = AccessPermissions.ReadWriteExecute)]
[NonSerialized]
public bool EjectOnDeconstruct = true;
/// <summary>
/// If this slot belongs to some breakable or destructible entity, should the item inside the slot be
/// ejected when it is broken or destroyed?
/// </summary>
[DataField]
[Access(typeof(ItemSlotsSystem), Other = AccessPermissions.ReadWriteExecute)]
[NonSerialized]
public bool EjectOnBreak;
/// <summary>
/// The popup shown when a standard insertion interaction uses an item rejected by this slot's filters.
/// </summary>
[DataField]
public LocId? WhitelistFailPopup;
/// <summary>
/// The popup shown when a standard interaction tries to insert into or eject from this slot while it is locked.
/// </summary>
[DataField]
public LocId? LockedFailPopup;
/// <summary>
/// The popup shown after a successful standard insertion interaction, including a swap.
/// </summary>
[DataField]
public LocId? InsertSuccessPopup;
/// <summary>
/// Whether insertion interactions may replace the current item after it passes ejection checks.
/// </summary>
/// <remarks>
/// This only affects standard insertion interactions. Direct insertion APIs do not perform slot swapping.
/// </remarks>
[DataField]
[Access(typeof(ItemSlotsSystem), Other = AccessPermissions.ReadWriteExecute)]
public bool Swap = true;
public string? ID => ContainerSlot?.ID;
// Convenience properties
public bool HasItem => ContainerSlot?.ContainedEntity != null;
public EntityUid? Item => ContainerSlot?.ContainedEntity;
/// <summary>
/// Priority used when selecting and ordering this slot.
/// </summary>
[DataField]
public int Priority;
/// <summary>
/// Whether this slot originated from local registration rather than received component state.
/// </summary>
/// <remarks>
/// A false value suppresses duplicate-key errors and preserves received configuration when a local component
/// later registers its corresponding slot.
/// </remarks>
[NonSerialized]
public bool Local = true;
}