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 { /// /// Used for entities that can hold items in different slots. Needed by ItemSlotSystem to support basic /// insert/eject interactions. /// [RegisterComponent] [Friend(typeof(ItemSlotsSystem))] public class ItemSlotsComponent : Component { public override string Name => "ItemSlots"; /// /// The dictionary that stores all of the item slots whose interactions will be managed by the . /// [DataField("slots", readOnly:true)] public readonly 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 SlotLocked; public ItemSlotsComponentState(Dictionary slots) { SlotLocked = new(slots.Count); foreach (var (key, slot) in slots) { SlotLocked[key] = slot.Locked; } } } /// /// This is effectively a wrapper for a ContainerSlot that adds content functionality like entity whitelists and /// insert/eject sounds. /// [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? /// /// 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("name")] public string Name = string.Empty; [DataField("startingItem", customTypeSerializer: typeof(PrototypeIdSerializer))] 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("locked")] [ViewVariables(VVAccess.ReadWrite)] public bool Locked = false; /// /// 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("ejectOnInteract")] public bool EjectOnInteract = false; /// /// Override the insert verb text. Defaults to [insert category] -> [item-name]. If not null, the verb will /// not be given a category. /// [DataField("insertVerbText")] public string? InsertVerbText; /// /// Override the insert verb text. Defaults to [eject category] -> [item-name]. If not null, the verb will /// not be given a category. /// [DataField("ejectVerbText")] public string? EjectVerbText; [ViewVariables] 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("ejectOnDeconstruct")] 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("ejectOnBreak")] public bool EjectOnBreak = false; /// /// If this is not an empty string, this will generate a popup when someone attempts to insert a bad item /// into this slot. This string will be passed through localization. /// [DataField("whitelistFailPopup")] public string WhitelistFailPopup = string.Empty; /// /// 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("swap")] public bool Swap = true; public string ID => ContainerSlot.ID; // Convenience properties public bool HasItem => ContainerSlot.ContainedEntity != null; public IEntity? Item => ContainerSlot.ContainedEntity; // and to make it easier for whenever IEntity is removed public EntityUid? ItemUid => ContainerSlot.ContainedEntity?.Uid; } }