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);
}
}