using Content.Shared.Whitelist; using Robust.Shared.Audio; using Robust.Shared.Containers; using Robust.Shared.GameStates; using Robust.Shared.Prototypes; using Robust.Shared.Serialization; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; namespace Content.Shared.Containers.ItemSlots { /// /// Used for entities that can hold items in different slots. Needed by ItemSlotSystem to support basic /// insert/eject interactions. /// [RegisterComponent] [Access(typeof(ItemSlotsSystem))] [NetworkedComponent] public sealed partial class ItemSlotsComponent : Component { /// /// The dictionary that stores all of the item slots whose interactions will be managed by the . /// [DataField(readOnly:true)] public Dictionary 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. Alternatively: split the Slots dictionary here into two: one // datafield, one that is actually used by the ItemSlotsSystem for keeping track of slots. } [Serializable, NetSerializable] public sealed class ItemSlotsComponentState : ComponentState { public readonly Dictionary Slots; public ItemSlotsComponentState(Dictionary slots) { Slots = slots; } } /// /// This is effectively a wrapper for a ContainerSlot that adds content functionality like entity whitelists and /// insert/eject sounds. /// [DataDefinition] [Access(typeof(ItemSlotsSystem))] [Serializable, NetSerializable] public sealed partial class ItemSlot { public ItemSlot() { } public ItemSlot(ItemSlot other) { CopyFrom(other); } [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"); /// /// The name of this item slot. This will be shown to the user in the verb menu. /// /// /// 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. /// [DataField(readOnly: true)] [Access(typeof(ItemSlotsSystem), Other = AccessPermissions.ReadWriteExecute)] // FIXME Friends public string Name = string.Empty; /// /// The entity prototype that is spawned into this slot on map init. /// /// /// 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. /// [DataField(readOnly: true, customTypeSerializer: typeof(PrototypeIdSerializer))] [Access(typeof(ItemSlotsSystem), Other = AccessPermissions.ReadWriteExecute)] // FIXME Friends [NonSerialized] public string? StartingItem; /// /// Whether or not an item can currently be ejected or inserted from this slot. /// /// /// 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. /// [DataField(readOnly: true)] [ViewVariables(VVAccess.ReadWrite)] public bool Locked = false; /// /// Prevents adding the eject alt-verb, but still lets you swap items. /// /// /// This does not affect EjectOnInteract, since if you do that you probably want ejecting to work. /// [DataField, ViewVariables(VVAccess.ReadWrite)] public bool DisableEject = false; /// /// 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. /// [DataField] public bool InsertOnInteract = true; /// /// Whether the item slots system will attempt to eject this item to the user's hands when interacted with. /// /// /// 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. /// [DataField] public bool EjectOnInteract = false; /// /// If true, and if this slot is attached to an item, then it will attempt to eject slot when to the slot is /// used in the user's hands. /// /// /// Desirable for things like ranged weapons ('Z' to eject), but not desirable for others (e.g., PDA uses /// 'Z' to open UI). Unlike , this will not make any changes to the context /// menu, nor will it disable alt-click interactions. /// [DataField] public bool EjectOnUse = false; /// /// 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. /// [DataField] public string? InsertVerbText; /// /// 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 /// [DataField] public string? EjectVerbText; [ViewVariables, NonSerialized] public ContainerSlot? ContainerSlot = default!; /// /// If this slot belongs to some de-constructible component, should the item inside the slot be ejected upon /// deconstruction? /// /// /// The actual deconstruction logic is handled by the server-side EmptyOnMachineDeconstructSystem. /// [DataField] [Access(typeof(ItemSlotsSystem), Other = AccessPermissions.ReadWriteExecute)] [NonSerialized] public bool EjectOnDeconstruct = true; /// /// If this slot belongs to some breakable or destructible entity, should the item inside the slot be /// ejected when it is broken or destroyed? /// [DataField] [Access(typeof(ItemSlotsSystem), Other = AccessPermissions.ReadWriteExecute)] [NonSerialized] public bool EjectOnBreak = false; /// /// When specified, a popup will be generated whenever someone attempts to insert a bad item into this slot. /// [DataField] public LocId? WhitelistFailPopup; /// /// When specified, a popup will be generated whenever someone attempts to insert a valid item, or eject an item /// from the slot while that slot is locked. /// [DataField] public LocId? LockedFailPopup; /// /// When specified, a popup will be generated whenever someone successfully inserts a valid item into this slot. /// This is also used for insertions resulting from swapping. /// [DataField] public LocId? InsertSuccessPopup; /// /// If the user interacts with an entity with an already-filled item slot, should they attempt to swap out the item? /// /// /// Useful for things like chem dispensers, but undesirable for things like the ID card console, where you /// want to insert more than one item that matches the same whitelist. /// [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; /// /// Priority for use with the eject & insert verbs for this slot. /// [DataField] public int Priority = 0; /// /// If false, errors when adding an item slot with a duplicate key are suppressed. Local==true implies that /// the slot was added via client component state handling. /// [NonSerialized] public bool Local = true; public void CopyFrom(ItemSlot other) { // These fields are mutable reference types. But they generally don't get modified, so this should be fine. Whitelist = other.Whitelist; InsertSound = other.InsertSound; EjectSound = other.EjectSound; Name = other.Name; Locked = other.Locked; InsertOnInteract = other.InsertOnInteract; EjectOnInteract = other.EjectOnInteract; EjectOnUse = other.EjectOnUse; InsertVerbText = other.InsertVerbText; EjectVerbText = other.EjectVerbText; WhitelistFailPopup = other.WhitelistFailPopup; LockedFailPopup = other.LockedFailPopup; InsertSuccessPopup = other.InsertSuccessPopup; Swap = other.Swap; Priority = other.Priority; } } /// /// Event raised on the slot entity and the item being inserted to determine if an item can be inserted into an item slot. /// [ByRefEvent] public record struct ItemSlotInsertAttemptEvent(EntityUid SlotEntity, EntityUid Item, EntityUid? User, ItemSlot Slot, bool Cancelled = false); /// /// Event raised on the slot entity and the item being inserted to determine if an item can be ejected from an item slot. /// [ByRefEvent] public record struct ItemSlotEjectAttemptEvent(EntityUid SlotEntity, EntityUid Item, EntityUid? User, ItemSlot Slot, bool Cancelled = false); }