using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using Content.Server.Clothing.Components; using Content.Server.Hands.Components; using Content.Server.Interaction; using Content.Server.Items; using Content.Server.Storage.Components; using Content.Shared.ActionBlocker; using Content.Shared.Acts; using Content.Shared.Inventory; using Content.Shared.Movement.EntitySystems; using Content.Shared.Popups; using Robust.Shared.Audio; using Robust.Shared.Containers; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Localization; using Robust.Shared.Map; using Robust.Shared.Network; using Robust.Shared.Player; using Robust.Shared.Players; using Robust.Shared.ViewVariables; using static Content.Shared.Inventory.EquipmentSlotDefines; using static Content.Shared.Inventory.SharedInventoryComponent.ClientInventoryMessage; namespace Content.Server.Inventory.Components { [RegisterComponent] [ComponentReference(typeof(SharedInventoryComponent))] public class InventoryComponent : SharedInventoryComponent, IExAct { [Dependency] private readonly IEntitySystemManager _entitySystemManager = default!; [Dependency] private readonly IEntityManager _entities = default!; [ViewVariables] private readonly Dictionary _slotContainers = new(); private KeyValuePair? _hoverEntity; public IEnumerable Slots => _slotContainers.Keys; public event Action? OnItemChanged; protected override void Initialize() { base.Initialize(); foreach (var slotName in InventoryInstance.SlotMasks) { if (slotName != EquipmentSlotDefines.Slots.NONE) { AddSlot(slotName); } } } protected override void OnRemove() { var slots = _slotContainers.Keys.ToList(); foreach (var slot in slots) { if (TryGetSlotItem(slot, out ItemComponent? item)) { _entities.DeleteEntity(item.Owner); } RemoveSlot(slot); } base.OnRemove(); } public IEnumerable GetAllHeldItems() { foreach (var (_, container) in _slotContainers) { foreach (var entity in container.ContainedEntities) { yield return entity; } } } /// /// Helper to get container name for specified slot on this component /// /// /// private string GetSlotString(Slots slot) { return Name + "_" + Enum.GetName(typeof(Slots), slot); } /// /// Gets the clothing equipped to the specified slot. /// /// The slot to get the item for. /// Null if the slot is empty, otherwise the item. public ItemComponent? GetSlotItem(Slots slot) { return GetSlotItem(slot); } public IEnumerable LookupItems() where T : Component { return _slotContainers.Values .SelectMany(x => x.ContainedEntities.Select(e => _entities.GetComponentOrNull(e))) .Where(x => x != null); } public T? GetSlotItem(Slots slot) where T : ItemComponent { if (!_slotContainers.ContainsKey(slot)) { return null; } var containedEntity = _slotContainers[slot].ContainedEntity; if (containedEntity != null && _entities.GetComponent(containedEntity.Value).EntityDeleted) { _slotContainers.Remove(slot); containedEntity = null; Dirty(); } return containedEntity.HasValue ? _entities.GetComponent(containedEntity.Value) : null; } public bool TryGetSlotItem(Slots slot, [NotNullWhen(true)] out T? itemComponent) where T : ItemComponent { itemComponent = GetSlotItem(slot); return itemComponent != null; } /// /// Equips slothing to the specified slot. /// /// /// This will fail if there is already an item in the specified slot. /// /// The slot to put the item in. /// The item to insert into the slot. /// Whether to perform an ActionBlocker check to the entity. /// The translated reason why the item cannot be equipped, if this function returns false. Can be null. /// True if the item was successfully inserted, false otherwise. public bool Equip(Slots slot, ItemComponent item, bool mobCheck, [NotNullWhen(false)] out string? reason) { if (item == null) { throw new ArgumentNullException(nameof(item), "Clothing must be passed here. To remove some clothing from a slot, use Unequip()"); } if (!CanEquip(slot, item, mobCheck, out reason)) { return false; } var inventorySlot = _slotContainers[slot]; if (!inventorySlot.Insert(item.Owner)) { reason = Loc.GetString("inventory-component-on-equip-cannot"); return false; } // TODO: Make clothing component not inherit ItemComponent, for fuck's sake. // TODO: Make clothing component not required for playing a sound on equip... Move it to its own component. if (mobCheck && item is ClothingComponent { EquipSound: {} equipSound }) { SoundSystem.Play(Filter.Pvs(Owner), equipSound.GetSound(), Owner, AudioParams.Default.WithVolume(-2f)); } _entitySystemManager.GetEntitySystem().EquippedInteraction(Owner, item.Owner, slot); OnItemChanged?.Invoke(); Dirty(); UpdateMovementSpeed(); return true; } public bool Equip(Slots slot, ItemComponent item, bool mobCheck = true) => Equip(slot, item, mobCheck, out var _); public bool Equip(Slots slot, EntityUid entity, bool mobCheck = true) => Equip(slot, _entities.GetComponent(entity), mobCheck); /// /// Checks whether an item can be put in the specified slot. /// /// The slot to check for. /// The item to check for. /// The translated reason why the item cannot be equiped, if this function returns false. Can be null. /// True if the item can be inserted into the specified slot. public bool CanEquip(Slots slot, ItemComponent item, bool mobCheck, [NotNullWhen(false)] out string? reason) { var pass = false; reason = null; if (mobCheck && !EntitySystem.Get().CanEquip(Owner)) { reason = Loc.GetString("inventory-component-can-equip-cannot"); return false; } if (item is ClothingComponent clothing) { if (clothing.SlotFlags != SlotFlags.PREVENTEQUIP && (clothing.SlotFlags & SlotMasks[slot]) != 0) { pass = true; } else { reason = Loc.GetString("inventory-component-can-equip-does-not-fit"); } } if (_entities.TryGetComponent(Owner, out IInventoryController? controller)) { pass = controller.CanEquip(slot, item.Owner, pass, out var controllerReason); reason = controllerReason ?? reason; } if (!pass) { reason = reason ?? Loc.GetString("inventory-component-can-equip-cannot"); return false; } var canEquip = pass && _slotContainers[slot].CanInsert(item.Owner); if (!canEquip) { reason = Loc.GetString("inventory-component-can-equip-cannot"); } return canEquip; } public bool CanEquip(Slots slot, ItemComponent item, bool mobCheck = true) => CanEquip(slot, item, mobCheck, out var _); public bool CanEquip(Slots slot, EntityUid entity, bool mobCheck = true) => CanEquip(slot, _entities.GetComponent(entity), mobCheck); /// /// Drops the item in a slot. /// /// The slot to drop the item from. /// True if an item was dropped, false otherwise. /// Whether to perform an ActionBlocker check to the entity. public bool Unequip(Slots slot, bool mobCheck = true) { if (!CanUnequip(slot, mobCheck)) { return false; } var inventorySlot = _slotContainers[slot]; if (inventorySlot.ContainedEntity is not {Valid: true} entity) { return false; } if (!inventorySlot.Remove(entity)) { return false; } // TODO: The item should be dropped to the container our owner is in, if any. _entities.GetComponent(entity).AttachParentToContainerOrGrid(); _entitySystemManager.GetEntitySystem().UnequippedInteraction(Owner, entity, slot); OnItemChanged?.Invoke(); Dirty(); UpdateMovementSpeed(); return true; } private void UpdateMovementSpeed() { EntitySystem.Get().RefreshMovementSpeedModifiers(Owner); } public void ForceUnequip(Slots slot) { var inventorySlot = _slotContainers[slot]; if (inventorySlot.ContainedEntity is not {Valid: true} entity) { return; } var item = _entities.GetComponent(entity); inventorySlot.ForceRemove(entity); var itemTransform = _entities.GetComponent(entity); itemTransform.AttachParentToContainerOrGrid(); _entitySystemManager.GetEntitySystem().UnequippedInteraction(Owner, item.Owner, slot); OnItemChanged?.Invoke(); Dirty(); } /// /// Checks whether an item can be dropped from the specified slot. /// /// The slot to check for. /// Whether to perform an ActionBlocker check to the entity. /// /// True if there is an item in the slot and it can be dropped, false otherwise. /// public bool CanUnequip(Slots slot, bool mobCheck = true) { if (mobCheck && !EntitySystem.Get().CanUnequip(Owner)) return false; var inventorySlot = _slotContainers[slot]; return inventorySlot.ContainedEntity != null && inventorySlot.CanRemove(inventorySlot.ContainedEntity.Value); } /// /// Adds a new slot to this inventory component. /// /// The name of the slot to add. /// /// Thrown if the slot with specified name already exists. /// public ContainerSlot AddSlot(Slots slot) { if (HasSlot(slot)) { throw new InvalidOperationException($"Slot '{slot}' already exists."); } Dirty(); var container = ContainerHelpers.CreateContainer(Owner, GetSlotString(slot)); container.OccludesLight = false; _slotContainers[slot] = container; OnItemChanged?.Invoke(); return _slotContainers[slot]; } /// /// Removes a slot from this inventory component. /// /// /// If the slot contains an item, the item is dropped. /// /// The name of the slot to remove. public void RemoveSlot(Slots slot) { if (!HasSlot(slot)) { throw new InvalidOperationException($"Slot '{slot}' does not exist."); } ForceUnequip(slot); var container = _slotContainers[slot]; container.Shutdown(); _slotContainers.Remove(slot); OnItemChanged?.Invoke(); Dirty(); } /// /// Checks whether a slot with the specified name exists. /// /// The slot name to check. /// True if the slot exists, false otherwise. public bool HasSlot(Slots slot) { return _slotContainers.ContainsKey(slot); } /// /// The underlying Container System just notified us that an entity was removed from it. /// We need to make sure we process that removed entity as being unequipped from the slot. /// public void ForceUnequip(IContainer container, EntityUid entity) { // make sure this is one of our containers. // Technically the correct way would be to enumerate the possible slot names // comparing with this container, but I might as well put the dictionary to good use. if (container is not ContainerSlot slot || !_slotContainers.ContainsValue(slot)) return; if (_entities.TryGetComponent(entity, out ItemComponent? itemComp)) { itemComp.RemovedFromSlot(); } OnItemChanged?.Invoke(); Dirty(); } /// /// Message that tells us to equip or unequip items from the inventory slots /// /// private async void HandleInventoryMessage(ClientInventoryMessage msg) { switch (msg.Updatetype) { case ClientInventoryUpdate.Equip: { var hands = _entities.GetComponent(Owner); var activeHand = hands.ActiveHand; var activeItem = hands.GetActiveHand; if (activeHand != null && activeItem != null && _entities.TryGetComponent(activeItem.Owner, out ItemComponent? item)) { hands.TryDropNoInteraction(); if (!Equip(msg.Inventoryslot, item, true, out var reason)) { hands.PutInHand(item); Owner.PopupMessageCursor(reason); } } break; } case ClientInventoryUpdate.Use: { var interactionSystem = _entitySystemManager.GetEntitySystem(); var hands = _entities.GetComponent(Owner); var activeHand = hands.GetActiveHand; var itemContainedInSlot = GetSlotItem(msg.Inventoryslot); if (itemContainedInSlot != null) { if (activeHand != null) { await interactionSystem.InteractUsing(Owner, activeHand.Owner, itemContainedInSlot.Owner, new EntityCoordinates()); } else if (Unequip(msg.Inventoryslot)) { hands.PutInHand(itemContainedInSlot); } } break; } case ClientInventoryUpdate.Hover: { var hands = _entities.GetComponent(Owner); var activeHand = hands.GetActiveHand; if (activeHand != null && GetSlotItem(msg.Inventoryslot) == null) { var canEquip = CanEquip(msg.Inventoryslot, activeHand, true, out var reason); _hoverEntity = new KeyValuePair(msg.Inventoryslot, (Uid: activeHand.Owner, canEquip)); Dirty(); } break; } } } /// [Obsolete("Component Messages are deprecated, use Entity Events instead.")] public override void HandleNetworkMessage(ComponentMessage message, INetChannel netChannel, ICommonSession? session = null) { base.HandleNetworkMessage(message, netChannel, session); if (session == null) { throw new ArgumentNullException(nameof(session)); } switch (message) { case ClientInventoryMessage msg: var playerentity = session.AttachedEntity; if (playerentity == Owner) HandleInventoryMessage(msg); break; case OpenSlotStorageUIMessage msg: if (!HasSlot(msg.Slot)) // client input sanitization return; var item = GetSlotItem(msg.Slot); if (item != null && _entities.TryGetComponent(item.Owner, out ServerStorageComponent? storage)) storage.OpenStorageUI(Owner); break; } } public override ComponentState GetComponentState() { var list = new List>(); foreach (var (slot, container) in _slotContainers) { if (container is {ContainedEntity: { }}) { list.Add(new KeyValuePair(slot, container.ContainedEntity.Value)); } } var hover = _hoverEntity; _hoverEntity = null; return new InventoryComponentState(list, hover); } void IExAct.OnExplosion(ExplosionEventArgs eventArgs) { if (eventArgs.Severity < ExplosionSeverity.Heavy) { return; } foreach (var slot in _slotContainers.Values.ToList()) { foreach (var entity in slot.ContainedEntities) { var exActs = _entities.GetComponents(entity).ToList(); foreach (var exAct in exActs) { exAct.OnExplosion(eventArgs); } } } } public override bool IsEquipped(EntityUid item) { if (item == default) return false; foreach (var containerSlot in _slotContainers.Values) { // we don't want a recursive check here if (containerSlot.Contains(item)) { return true; } } return false; } public override bool TryGetSlot(Slots slot, [NotNullWhen(true)] out EntityUid? item) { if (_slotContainers.TryGetValue(slot, out var container)) { item = container.ContainedEntity; return item != null; } item = null; return false; } } }