using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using Content.Server.Administration.Commands; using Content.Server.GameObjects.Components.Items.Clothing; using Content.Server.GameObjects.Components.Items.Storage; using Content.Server.GameObjects.EntitySystems.Click; using Content.Server.Interfaces.GameObjects; using Content.Shared.GameObjects.Components.Inventory; using Content.Shared.GameObjects.Components.Movement; using Content.Shared.GameObjects.EntitySystems; using Content.Shared.GameObjects.EntitySystems.ActionBlocker; using Content.Shared.GameObjects.EntitySystems.EffectBlocker; using Content.Shared.GameObjects.Verbs; using Content.Shared.Interfaces; using Robust.Server.Console; using Robust.Server.GameObjects; using Robust.Server.Player; using Robust.Shared.Console; 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.Players; using Robust.Shared.ViewVariables; using static Content.Shared.GameObjects.Components.Inventory.EquipmentSlotDefines; using static Content.Shared.GameObjects.Components.Inventory.SharedInventoryComponent.ClientInventoryMessage; namespace Content.Server.GameObjects.Components.GUI { [RegisterComponent] [ComponentReference(typeof(SharedInventoryComponent))] public class InventoryComponent : SharedInventoryComponent, IExAct, IPressureProtection, IEffectBlocker { [Dependency] private readonly IEntitySystemManager _entitySystemManager = default!; [ViewVariables] private readonly Dictionary _slotContainers = new(); private KeyValuePair? _hoverEntity; public IEnumerable Slots => _slotContainers.Keys; public event Action? OnItemChanged; public override void Initialize() { base.Initialize(); foreach (var slotName in InventoryInstance.SlotMasks) { if (slotName != EquipmentSlotDefines.Slots.NONE) { AddSlot(slotName); } } } // Optimization: Cache this [ViewVariables] public float HighPressureMultiplier { get { var multiplier = 1f; foreach (var (slot, containerSlot) in _slotContainers) { foreach (var entity in containerSlot.ContainedEntities) { foreach (var protection in entity.GetAllComponents()) { multiplier *= protection.HighPressureMultiplier; } } } return multiplier; } } // Optimization: Cache this [ViewVariables] public float LowPressureMultiplier { get { var multiplier = 1f; foreach (var (slot, containerSlot) in _slotContainers) { foreach (var entity in containerSlot.ContainedEntities) { foreach (var protection in entity.GetAllComponents()) { multiplier *= protection.LowPressureMultiplier; } } } return multiplier; } } public override float WalkSpeedModifier { get { var mod = 1f; foreach (var slot in _slotContainers.Values) { if (slot.ContainedEntity != null) { foreach (var modifier in slot.ContainedEntity.GetAllComponents()) { mod *= modifier.WalkSpeedModifier; } } } return mod; } } public override float SprintSpeedModifier { get { var mod = 1f; foreach (var slot in _slotContainers.Values) { if (slot.ContainedEntity != null) { foreach (var modifier in slot.ContainedEntity.GetAllComponents()) { mod *= modifier.SprintSpeedModifier; } } } return mod; } } bool IEffectBlocker.CanSlip() { return !TryGetSlotItem(EquipmentSlotDefines.Slots.SHOES, out ItemComponent? shoes) || EffectBlockerSystem.CanSlip(shoes.Owner); } public override void OnRemove() { var slots = _slotContainers.Keys.ToList(); foreach (var slot in slots) { if (TryGetSlotItem(slot, out ItemComponent? item)) { item.Owner.Delete(); } 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 => e.GetComponentOrNull())) .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?.Deleted == true) { _slotContainers.Remove(slot); containedEntity = null; Dirty(); } return containedEntity?.GetComponent(); } 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("You can't equip this!"); return false; } _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, IEntity entity, bool mobCheck = true) => Equip(slot, entity.GetComponent(), 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 && !ActionBlockerSystem.CanEquip(Owner)) { reason = Loc.GetString("You can't equip this!"); return false; } if (item is ClothingComponent clothing) { if (clothing.SlotFlags != SlotFlags.PREVENTEQUIP && (clothing.SlotFlags & SlotMasks[slot]) != 0) { pass = true; } else { reason = Loc.GetString("This doesn't fit."); } } if (Owner.TryGetComponent(out IInventoryController? controller)) { pass = controller.CanEquip(slot, item.Owner, pass, out var controllerReason); reason = controllerReason ?? reason; } if (!pass && reason == null) { reason = Loc.GetString("You can't equip this!"); } var canEquip = pass && _slotContainers[slot].CanInsert(item.Owner); if (!canEquip) { reason = Loc.GetString("You can't equip this!"); } return canEquip; } public bool CanEquip(Slots slot, ItemComponent item, bool mobCheck = true) => CanEquip(slot, item, mobCheck, out var _); public bool CanEquip(Slots slot, IEntity entity, bool mobCheck = true) => CanEquip(slot, entity.GetComponent(), 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]; var entity = inventorySlot.ContainedEntity; if (entity == null) { return false; } if (!inventorySlot.Remove(entity)) { return false; } // TODO: The item should be dropped to the container our owner is in, if any. entity.Transform.AttachParentToContainerOrGrid(); _entitySystemManager.GetEntitySystem().UnequippedInteraction(Owner, entity, slot); OnItemChanged?.Invoke(); Dirty(); UpdateMovementSpeed(); return true; } private void UpdateMovementSpeed() { if (Owner.TryGetComponent(out MovementSpeedModifierComponent? mod)) { mod.RefreshMovementSpeedModifiers(); } } public void ForceUnequip(Slots slot) { var inventorySlot = _slotContainers[slot]; var entity = inventorySlot.ContainedEntity; if (entity == null) { return; } var item = entity.GetComponent(); inventorySlot.ForceRemove(entity); var itemTransform = entity.Transform; 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 && !ActionBlockerSystem.CanUnequip(Owner)) return false; var inventorySlot = _slotContainers[slot]; return inventorySlot.ContainedEntity != null && inventorySlot.CanRemove(inventorySlot.ContainedEntity); } /// /// 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($"Slow '{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. /// private void ForceUnequip(IContainer container, IEntity 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 (entity.TryGetComponent(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 = Owner.GetComponent(); var activeHand = hands.ActiveHand; var activeItem = hands.GetActiveHand; if (activeHand != null && activeItem != null && activeItem.Owner.TryGetComponent(out ItemComponent? clothing)) { hands.Drop(activeHand, doDropInteraction: false); if (!Equip(msg.Inventoryslot, clothing, true, out var reason)) { hands.PutInHand(clothing); Owner.PopupMessageCursor(reason); } } break; } case ClientInventoryUpdate.Use: { var interactionSystem = _entitySystemManager.GetEntitySystem(); var hands = Owner.GetComponent(); var activeHand = hands.GetActiveHand; var itemContainedInSlot = GetSlotItem(msg.Inventoryslot); if (itemContainedInSlot != null) { if (activeHand != null) { await interactionSystem.Interaction(Owner, activeHand.Owner, itemContainedInSlot.Owner, new EntityCoordinates()); } else if (Unequip(msg.Inventoryslot)) { hands.PutInHand(itemContainedInSlot); } } break; } case ClientInventoryUpdate.Hover: { var hands = Owner.GetComponent(); 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, (activeHand.Owner.Uid, canEquip)); Dirty(); } break; } } } /// public override void HandleMessage(ComponentMessage message, IComponent? component) { base.HandleMessage(message, component); switch (message) { case ContainerContentsModifiedMessage msg: if (msg.Removed) ForceUnequip(msg.Container, msg.Entity); break; default: break; } } /// 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 && item.Owner.TryGetComponent(out ServerStorageComponent? storage)) storage.OpenStorageUI(Owner); break; } } public override ComponentState GetComponentState(ICommonSession player) { var list = new List>(); foreach (var (slot, container) in _slotContainers) { if (container != null && container.ContainedEntity != null) { list.Add(new KeyValuePair(slot, container.ContainedEntity.Uid)); } } 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 = entity.GetAllComponents().ToList(); foreach (var exAct in exActs) { exAct.OnExplosion(eventArgs); } } } } public override bool IsEquipped(IEntity item) { if (item == null) return false; foreach (var containerSlot in _slotContainers.Values) { // we don't want a recursive check here if (containerSlot.Contains(item)) { return true; } } return false; } [Verb] private sealed class SetOutfitVerb : Verb { public override bool RequireInteractionRange => false; public override bool BlockedByContainers => false; protected override void GetData(IEntity user, InventoryComponent component, VerbData data) { data.Visibility = VerbVisibility.Invisible; if (!CanCommand(user)) return; data.Visibility = VerbVisibility.Visible; data.Text = Loc.GetString("Set Outfit"); data.CategoryData = VerbCategories.Debug; data.IconTexture = "/Textures/Interface/VerbIcons/outfit.svg.192dpi.png"; } protected override void Activate(IEntity user, InventoryComponent component) { if (!CanCommand(user)) return; var target = component.Owner; var entityId = target.Uid.ToString(); var command = new SetOutfitCommand(); var host = IoCManager.Resolve(); var args = new string[] {entityId}; var session = user.PlayerSession(); command.Execute(new ConsoleShell(host, session), $"{command.Command} {entityId}", args); } private static bool CanCommand(IEntity user) { var groupController = IoCManager.Resolve(); return user.TryGetComponent(out var player) && groupController.CanCommand(player.playerSession, "setoutfit"); } } } }