using System.Diagnostics.CodeAnalysis; using Content.Shared.ActionBlocker; using Content.Shared.Administration.Logs; using Content.Shared.Destructible; using Content.Shared.Hands.EntitySystems; using Content.Shared.Popups; using Content.Shared.Whitelist; using Robust.Shared.Audio.Systems; using Robust.Shared.Containers; using Robust.Shared.GameStates; using Robust.Shared.Serialization.Manager; using Robust.Shared.Utility; namespace Content.Shared.Containers.ItemSlots; /// /// Handles interactions related to inserting and ejecting items into and from item slots. /// public sealed partial class ItemSlotsSystem : EntitySystem { [Dependency] private ISharedAdminLogManager _adminLogger = default!; [Dependency] private ActionBlockerSystem _actionBlockerSystem = default!; [Dependency] private SharedContainerSystem _containers = default!; [Dependency] private SharedPopupSystem _popupSystem = default!; [Dependency] private SharedHandsSystem _handsSystem = default!; [Dependency] private SharedAudioSystem _audioSystem = default!; [Dependency] private EntityWhitelistSystem _whitelistSystem = default!; [Dependency] private ISerializationManager _serializationManager = default!; /// /// Spawn in starting items for any item slots that should have one. /// [SubscribeLocalEvent] private void OnMapInit(Entity ent, ref MapInitEvent args) { foreach (var slot in ent.Comp.Slots.Values) { if (slot.HasItem || string.IsNullOrEmpty(slot.StartingItem)) continue; var item = Spawn(slot.StartingItem, Transform(ent).Coordinates); if (slot.ContainerSlot != null) _containers.Insert(item, slot.ContainerSlot); } } /// /// Ensure item slots have containers. /// [SubscribeLocalEvent] private void OnInitialize(Entity ent, ref ComponentInit args) { foreach (var (id, slot) in ent.Comp.Slots) { slot.ContainerSlot = _containers.EnsureContainer(ent, id); } } [SubscribeLocalEvent] private void OnBreak(Entity ent, ref BreakageEventArgs args) { EjectOnBreak(ent); } [SubscribeLocalEvent] private void OnDestruction(Entity ent, ref DestructionEventArgs args) { EjectOnBreak(ent); } /// /// Eject items from slots configured to do so when the entity is broken or destroyed. /// private void EjectOnBreak(Entity ent) { EjectFromAllSlots(ent, slot => slot.EjectOnBreak); } private void CopySlotState(ItemSlot source, ref ItemSlot target) { // These fields are DataFields, so SerializationManager would copy them, // but they are excluded from network serialization by NonSerialized. // Preserve the local values when applying received slot state. var startingItem = target.StartingItem; var ejectOnDeconstruct = target.EjectOnDeconstruct; var ejectOnBreak = target.EjectOnBreak; _serializationManager.CopyTo(source, ref target, notNullableOverride: true); target.StartingItem = startingItem; target.EjectOnDeconstruct = ejectOnDeconstruct; target.EjectOnBreak = ejectOnBreak; } /// /// Stores a new item slot in the and ensures that it has a backing container. /// /// /// If a local slot replaces one created from component state, the received state is copied onto the local slot. /// public void AddItemSlot(Entity ent, string id, ItemSlot slot) { ent.Comp ??= EnsureComp(ent); DebugTools.AssertOwner(ent, ent.Comp); if (ent.Comp.Slots.TryGetValue(id, out var existing)) { if (existing.Local) { Log.Error( $"Duplicate item slot key. Entity: {Comp(ent).EntityName} ({ent.Owner}), key: {id}"); } else // Server state takes priority. CopySlotState(existing, ref slot); } slot.ContainerSlot = _containers.EnsureContainer(ent, id); ent.Comp.Slots[id] = slot; Dirty(ent); } /// /// Removes an item slot. This should generally be called whenever a component that added a slot is removed. /// public void RemoveItemSlot(Entity ent, ItemSlot slot) { if (Terminating(ent) || slot.ContainerSlot == null) return; _containers.ShutdownContainer(slot.ContainerSlot); // 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(ent, ref ent.Comp, logMissing: false)) return; ent.Comp.Slots.Remove(slot.ContainerSlot.ID); if (ent.Comp.Slots.Count == 0) RemComp(ent, ent.Comp); else Dirty(ent); } public bool TryGetSlot(Entity ent, string slotId, [NotNullWhen(true)] out ItemSlot? itemSlot) { itemSlot = null; if (!Resolve(ent, ref ent.Comp)) return false; return ent.Comp.Slots.TryGetValue(slotId, out itemSlot); } /// /// Get the contents of some item slot. /// /// The item in the slot, or null if the slot is empty or the entity doesn't have an . public EntityUid? GetItemOrNull(Entity ent, string id) { if (!Resolve(ent, ref ent.Comp, logMissing: false)) return null; return ent.Comp.Slots.GetValueOrDefault(id)?.Item; } /// /// Reconciles local slot registrations and their serialized configuration with received component state. /// /// /// The slot's ContainerSlot performs its own networking, so the contained entity is not sent here. /// [SubscribeLocalEvent] private void HandleItemSlotsState(Entity ent, ref ComponentHandleState args) { if (args.Current is not ItemSlotsComponentState state) return; var removed = new List(); foreach (var (key, slot) in ent.Comp.Slots) { if (!state.Slots.ContainsKey(key)) removed.Add(slot); } foreach (var slot in removed) { RemoveItemSlot((ent.Owner, ent.Comp), slot); } foreach (var (serverKey, serverSlot) in state.Slots) { if (ent.Comp.Slots.TryGetValue(serverKey, out var itemSlot)) { CopySlotState(serverSlot, ref itemSlot); itemSlot.ContainerSlot = _containers.EnsureContainer(ent, serverKey); } else { var slot = new ItemSlot(); CopySlotState(serverSlot, ref slot); slot.Local = false; AddItemSlot((ent.Owner, ent.Comp), serverKey, slot); } } ent.Comp.AllowSmartEquip = state.AllowSmartEquip; } [SubscribeLocalEvent] private void GetItemSlotsState(Entity ent, ref ComponentGetState args) { args.State = new ItemSlotsComponentState(ent.Comp.Slots, ent.Comp.AllowSmartEquip); } }