using System.Collections.Frozen; using System.Diagnostics.CodeAnalysis; using System.Linq; using Content.Shared.ActionBlocker; using Content.Shared.Containers.ItemSlots; using Content.Shared.Destructible; using Content.Shared.DoAfter; using Content.Shared.Hands.Components; using Content.Shared.Hands.EntitySystems; using Content.Shared.Implants.Components; using Content.Shared.Interaction; using Content.Shared.Item; using Content.Shared.Lock; using Content.Shared.Placeable; using Content.Shared.Popups; using Content.Shared.Stacks; using Content.Shared.Storage.Components; using Content.Shared.Timing; using Content.Shared.Verbs; using Robust.Shared.Audio.Systems; using Robust.Shared.Containers; using Robust.Shared.GameStates; using Robust.Shared.Map; using Robust.Shared.Prototypes; using Robust.Shared.Random; using Robust.Shared.Serialization; namespace Content.Shared.Storage.EntitySystems; public abstract class SharedStorageSystem : EntitySystem { [Dependency] private readonly IPrototypeManager _prototype = default!; [Dependency] protected readonly IRobustRandom Random = default!; [Dependency] protected readonly ActionBlockerSystem ActionBlocker = default!; [Dependency] private readonly EntityLookupSystem _entityLookupSystem = default!; [Dependency] private readonly SharedAppearanceSystem _appearance = default!; [Dependency] protected readonly SharedAudioSystem Audio = default!; [Dependency] private readonly SharedContainerSystem _containerSystem = default!; [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!; [Dependency] protected readonly SharedEntityStorageSystem EntityStorage = default!; [Dependency] private readonly SharedInteractionSystem _interactionSystem = default!; [Dependency] protected readonly SharedItemSystem ItemSystem = default!; [Dependency] private readonly SharedPopupSystem _popupSystem = default!; [Dependency] private readonly SharedHandsSystem _sharedHandsSystem = default!; [Dependency] private readonly SharedStackSystem _stack = default!; [Dependency] protected readonly SharedTransformSystem TransformSystem = default!; [Dependency] private readonly SharedUserInterfaceSystem _ui = default!; [Dependency] protected readonly UseDelaySystem UseDelay = default!; private EntityQuery _itemQuery; private EntityQuery _stackQuery; private EntityQuery _xformQuery; [ValidatePrototypeId] public const string DefaultStorageMaxItemSize = "Normal"; private ItemSizePrototype _defaultStorageMaxItemSize = default!; public bool CheckingCanInsert; private readonly List _sortedSizes = new(); private FrozenDictionary _nextSmallest = FrozenDictionary.Empty; /// public override void Initialize() { base.Initialize(); _itemQuery = GetEntityQuery(); _stackQuery = GetEntityQuery(); _xformQuery = GetEntityQuery(); _prototype.PrototypesReloaded += OnPrototypesReloaded; SubscribeLocalEvent(OnStorageGetState); SubscribeLocalEvent(OnStorageHandleState); SubscribeLocalEvent(OnComponentInit, before: new[] { typeof(SharedContainerSystem) }); SubscribeLocalEvent>(AddTransferVerbs); SubscribeLocalEvent(OnInteractUsing, after: new[] { typeof(ItemSlotsSystem) }); SubscribeLocalEvent(OnActivate); SubscribeLocalEvent(OnImplantActivate); SubscribeLocalEvent(AfterInteract); SubscribeLocalEvent(OnDestroy); SubscribeLocalEvent(OnBoundUIOpen); SubscribeLocalEvent(OnStackCountChanged); SubscribeLocalEvent(OnEntInserted); SubscribeLocalEvent(OnEntRemoved); SubscribeLocalEvent(OnInsertAttempt); SubscribeLocalEvent(OnDoAfter); SubscribeAllEvent(OnInteractWithItem); SubscribeAllEvent(OnSetItemLocation); SubscribeAllEvent(OnInsertItemIntoLocation); SubscribeAllEvent(OnRemoveItem); UpdatePrototypeCache(); } private void OnStorageGetState(EntityUid uid, StorageComponent component, ref ComponentGetState args) { var storedItems = new Dictionary(); foreach (var (ent, location) in component.StoredItems) { storedItems[GetNetEntity(ent)] = location; } args.State = new StorageComponentState() { Grid = new List(component.Grid), IsUiOpen = component.IsUiOpen, MaxItemSize = component.MaxItemSize, StoredItems = storedItems }; } private void OnStorageHandleState(EntityUid uid, StorageComponent component, ref ComponentHandleState args) { if (args.Current is not StorageComponentState state) return; component.Grid.Clear(); component.Grid.AddRange(state.Grid); component.IsUiOpen = state.IsUiOpen; component.MaxItemSize = state.MaxItemSize; component.StoredItems.Clear(); foreach (var (nent, location) in state.StoredItems) { var ent = EnsureEntity(nent, uid); component.StoredItems[ent] = location; } } public override void Shutdown() { _prototype.PrototypesReloaded -= OnPrototypesReloaded; } private void OnPrototypesReloaded(PrototypesReloadedEventArgs args) { if (args.ByType.ContainsKey(typeof(ItemSizePrototype)) || (args.Removed?.ContainsKey(typeof(ItemSizePrototype)) ?? false)) { UpdatePrototypeCache(); } } private void UpdatePrototypeCache() { _defaultStorageMaxItemSize = _prototype.Index(DefaultStorageMaxItemSize); _sortedSizes.Clear(); _sortedSizes.AddRange(_prototype.EnumeratePrototypes()); _sortedSizes.Sort(); var nextSmallest = new KeyValuePair[_sortedSizes.Count]; for (var i = 0; i < _sortedSizes.Count; i++) { var k = _sortedSizes[i].ID; var v = _sortedSizes[Math.Max(i - 1, 0)]; nextSmallest[i] = new(k, v); } _nextSmallest = nextSmallest.ToFrozenDictionary(); } private void OnComponentInit(EntityUid uid, StorageComponent storageComp, ComponentInit args) { storageComp.Container = _containerSystem.EnsureContainer(uid, StorageComponent.ContainerId); UpdateAppearance((uid, storageComp, null)); } public virtual void UpdateUI(Entity entity) {} public virtual void OpenStorageUI(EntityUid uid, EntityUid entity, StorageComponent? storageComp = null, bool silent = false) { } private void AddTransferVerbs(EntityUid uid, StorageComponent component, GetVerbsEvent args) { if (!args.CanAccess || !args.CanInteract) return; var entities = component.Container.ContainedEntities; if (entities.Count == 0 || TryComp(uid, out LockComponent? lockComponent) && lockComponent.Locked) return; // if the target is storage, add a verb to transfer storage. if (TryComp(args.Target, out StorageComponent? targetStorage) && (!TryComp(args.Target, out LockComponent? targetLock) || !targetLock.Locked)) { UtilityVerb verb = new() { Text = Loc.GetString("storage-component-transfer-verb"), IconEntity = GetNetEntity(args.Using), Act = () => TransferEntities(uid, args.Target, args.User, component, lockComponent, targetStorage, targetLock) }; args.Verbs.Add(verb); } } /// /// Inserts storable entities into this storage container if possible, otherwise return to the hand of the user /// /// true if inserted, false otherwise private void OnInteractUsing(EntityUid uid, StorageComponent storageComp, InteractUsingEvent args) { if (args.Handled || !storageComp.ClickInsert || TryComp(uid, out LockComponent? lockComponent) && lockComponent.Locked) return; if (HasComp(uid)) return; PlayerInsertHeldEntity(uid, args.User, storageComp); // Always handle it, even if insertion fails. // We don't want to trigger any AfterInteract logic here. // Example issue would be placing wires if item doesn't fit in backpack. args.Handled = true; } /// /// Sends a message to open the storage UI /// private void OnActivate(EntityUid uid, StorageComponent storageComp, ActivateInWorldEvent args) { if (args.Handled || TryComp(uid, out var lockComponent) && lockComponent.Locked) return; OpenStorageUI(uid, args.User, storageComp); args.Handled = true; } /// /// Specifically for storage implants. /// private void OnImplantActivate(EntityUid uid, StorageComponent storageComp, OpenStorageImplantEvent args) { if (args.Handled) return; OpenStorageUI(uid, args.Performer, storageComp); args.Handled = true; } /// /// Allows a user to pick up entities by clicking them, or pick up all entities in a certain radius /// around a click. /// /// private void AfterInteract(EntityUid uid, StorageComponent storageComp, AfterInteractEvent args) { if (args.Handled || !args.CanReach) return; // Pick up all entities in a radius around the clicked location. // The last half of the if is because carpets exist and this is terrible if (storageComp.AreaInsert && (args.Target == null || !HasComp(args.Target.Value))) { var validStorables = new List(); foreach (var entity in _entityLookupSystem.GetEntitiesInRange(args.ClickLocation, storageComp.AreaInsertRadius, LookupFlags.Dynamic | LookupFlags.Sundries)) { if (entity == args.User || !_itemQuery.HasComponent(entity) || !CanInsert(uid, entity, out _, storageComp) || !_interactionSystem.InRangeUnobstructed(args.User, entity)) { continue; } validStorables.Add(entity); } //If there's only one then let's be generous if (validStorables.Count > 1) { var doAfterArgs = new DoAfterArgs(EntityManager, args.User, 0.2f * validStorables.Count, new AreaPickupDoAfterEvent(GetNetEntityList(validStorables)), uid, target: uid) { BreakOnDamage = true, BreakOnUserMove = true, NeedHand = true }; _doAfterSystem.TryStartDoAfter(doAfterArgs); args.Handled = true; } return; } // Pick up the clicked entity if (storageComp.QuickInsert) { if (args.Target is not { Valid: true } target) return; if (_containerSystem.IsEntityInContainer(target) || target == args.User || !HasComp(target)) { return; } if (_xformQuery.TryGetComponent(uid, out var transformOwner) && TryComp(target, out var transformEnt)) { var parent = transformOwner.ParentUid; var position = EntityCoordinates.FromMap( parent.IsValid() ? parent : uid, TransformSystem.GetMapCoordinates(transformEnt), TransformSystem ); args.Handled = true; if (PlayerInsertEntityInWorld((uid, storageComp), args.User, target)) { RaiseNetworkEvent(new AnimateInsertingEntitiesEvent(GetNetEntity(uid), new List { GetNetEntity(target) }, new List { GetNetCoordinates(position) }, new List { transformOwner.LocalRotation })); } } } } private void OnDoAfter(EntityUid uid, StorageComponent component, AreaPickupDoAfterEvent args) { if (args.Handled || args.Cancelled) return; args.Handled = true; var successfullyInserted = new List(); var successfullyInsertedPositions = new List(); var successfullyInsertedAngles = new List(); _xformQuery.TryGetComponent(uid, out var xform); foreach (var netEntity in args.Entities) { var entity = GetEntity(netEntity); // Check again, situation may have changed for some entities, but we'll still pick up any that are valid if (_containerSystem.IsEntityInContainer(entity) || entity == args.Args.User || !_itemQuery.HasComponent(entity)) continue; if (xform == null || !_xformQuery.TryGetComponent(entity, out var targetXform) || targetXform.MapID != xform.MapID) { continue; } var position = EntityCoordinates.FromMap( xform.ParentUid.IsValid() ? xform.ParentUid : uid, new MapCoordinates(TransformSystem.GetWorldPosition(targetXform), targetXform.MapID), TransformSystem ); var angle = targetXform.LocalRotation; if (PlayerInsertEntityInWorld((uid, component), args.Args.User, entity)) { successfullyInserted.Add(entity); successfullyInsertedPositions.Add(position); successfullyInsertedAngles.Add(angle); } } // If we picked up at least one thing, play a sound and do a cool animation! if (successfullyInserted.Count > 0) { Audio.PlayPvs(component.StorageInsertSound, uid); RaiseNetworkEvent(new AnimateInsertingEntitiesEvent( GetNetEntity(uid), GetNetEntityList(successfullyInserted), GetNetCoordinatesList(successfullyInsertedPositions), successfullyInsertedAngles)); } args.Handled = true; } private void OnDestroy(EntityUid uid, StorageComponent storageComp, DestructionEventArgs args) { var coordinates = TransformSystem.GetMoverCoordinates(uid); // Being destroyed so need to recalculate. _containerSystem.EmptyContainer(storageComp.Container, destination: coordinates); } /// /// This function gets called when the user clicked on an item in the storage UI. This will either place the /// item in the user's hand if it is currently empty, or interact with the item using the user's currently /// held item. /// private void OnInteractWithItem(StorageInteractWithItemEvent msg, EntitySessionEventArgs args) { if (args.SenderSession.AttachedEntity is not { } player) return; var uid = GetEntity(msg.StorageUid); var entity = GetEntity(msg.InteractedItemUid); if (!TryComp(uid, out var storageComp)) return; if (!_ui.TryGetUi(uid, StorageComponent.StorageUiKey.Key, out var bui) || !bui.SubscribedSessions.Contains(args.SenderSession)) return; if (!Exists(entity)) { Log.Error($"Player {args.SenderSession} interacted with non-existent item {msg.InteractedItemUid} stored in {ToPrettyString(uid)}"); return; } if (!ActionBlocker.CanInteract(player, entity) || !storageComp.Container.Contains(entity)) return; // Does the player have hands? if (!TryComp(player, out HandsComponent? hands) || hands.Count == 0) return; // If the user's active hand is empty, try pick up the item. if (hands.ActiveHandEntity == null) { if (_sharedHandsSystem.TryPickupAnyHand(player, entity, handsComp: hands) && storageComp.StorageRemoveSound != null) Audio.PlayPredicted(storageComp.StorageRemoveSound, uid, player); { return; } } // Else, interact using the held item _interactionSystem.InteractUsing(player, hands.ActiveHandEntity.Value, entity, Transform(entity).Coordinates, checkCanInteract: false); } private void OnSetItemLocation(StorageSetItemLocationEvent msg, EntitySessionEventArgs args) { if (args.SenderSession.AttachedEntity is not { } player) return; var storageEnt = GetEntity(msg.StorageEnt); var itemEnt = GetEntity(msg.ItemEnt); if (!TryComp(storageEnt, out var storageComp)) return; if (!_ui.TryGetUi(storageEnt, StorageComponent.StorageUiKey.Key, out var bui) || !bui.SubscribedSessions.Contains(args.SenderSession)) return; if (!Exists(itemEnt)) { Log.Error($"Player {args.SenderSession} set location of non-existent item {msg.ItemEnt} stored in {ToPrettyString(storageEnt)}"); return; } if (!ActionBlocker.CanInteract(player, itemEnt)) return; TrySetItemStorageLocation((itemEnt, null), (storageEnt, storageComp), msg.Location); } private void OnRemoveItem(StorageRemoveItemEvent msg, EntitySessionEventArgs args) { if (args.SenderSession.AttachedEntity is not { } player) return; var storageEnt = GetEntity(msg.StorageEnt); var itemEnt = GetEntity(msg.ItemEnt); if (!TryComp(storageEnt, out var storageComp)) return; if (!_ui.TryGetUi(storageEnt, StorageComponent.StorageUiKey.Key, out var bui) || !bui.SubscribedSessions.Contains(args.SenderSession)) return; if (!Exists(itemEnt)) { Log.Error($"Player {args.SenderSession} set location of non-existent item {msg.ItemEnt} stored in {ToPrettyString(storageEnt)}"); return; } if (!ActionBlocker.CanInteract(player, itemEnt)) return; TransformSystem.DropNextTo(itemEnt, player); Audio.PlayPredicted(storageComp.StorageRemoveSound, storageEnt, player); } private void OnInsertItemIntoLocation(StorageInsertItemIntoLocationEvent msg, EntitySessionEventArgs args) { if (args.SenderSession.AttachedEntity is not { } player) return; var storageEnt = GetEntity(msg.StorageEnt); var itemEnt = GetEntity(msg.ItemEnt); if (!TryComp(storageEnt, out var storageComp)) return; if (!_ui.TryGetUi(storageEnt, StorageComponent.StorageUiKey.Key, out var bui) || !bui.SubscribedSessions.Contains(args.SenderSession)) return; if (!Exists(itemEnt)) { Log.Error($"Player {args.SenderSession} set location of non-existent item {msg.ItemEnt} stored in {ToPrettyString(storageEnt)}"); return; } if (!ActionBlocker.CanInteract(player, itemEnt) || !_sharedHandsSystem.IsHolding(player, itemEnt, out _)) return; InsertAt((storageEnt, storageComp), (itemEnt, null), msg.Location, out _, player, stackAutomatically: false); } private void OnBoundUIOpen(EntityUid uid, StorageComponent storageComp, BoundUIOpenedEvent args) { if (!storageComp.IsUiOpen) { storageComp.IsUiOpen = true; UpdateAppearance((uid, storageComp, null)); } } private void OnEntInserted(Entity entity, ref EntInsertedIntoContainerMessage args) { // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract if (entity.Comp.Container == null) return; if (args.Container.ID != StorageComponent.ContainerId) return; if (!entity.Comp.StoredItems.ContainsKey(args.Entity)) { if (!TryGetAvailableGridSpace((entity.Owner, entity.Comp), (args.Entity, null), out var location)) { _containerSystem.Remove(args.Entity, args.Container, force: true); return; } entity.Comp.StoredItems[args.Entity] = location.Value; Dirty(entity, entity.Comp); } UpdateAppearance((entity, entity.Comp, null)); UpdateUI((entity, entity.Comp)); } private void OnEntRemoved(Entity entity, ref EntRemovedFromContainerMessage args) { // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract if (entity.Comp.Container == null) return; if (args.Container.ID != StorageComponent.ContainerId) return; entity.Comp.StoredItems.Remove(args.Entity); Dirty(entity, entity.Comp); UpdateAppearance((entity, entity.Comp, null)); UpdateUI((entity, entity.Comp)); } private void OnInsertAttempt(EntityUid uid, StorageComponent component, ContainerIsInsertingAttemptEvent args) { if (args.Cancelled || args.Container.ID != StorageComponent.ContainerId) return; // don't run cyclical CanInsert() loops if (CheckingCanInsert) return; if (!CanInsert(uid, args.EntityUid, out _, component, ignoreStacks: true)) args.Cancel(); } public void UpdateAppearance(Entity entity) { // TODO STORAGE remove appearance data and just use the data on the component. var (uid, storage, appearance) = entity; if (!Resolve(uid, ref storage, ref appearance, false)) return; // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract if (storage.Container == null) return; // component hasn't yet been initialized. var capacity = storage.Grid.GetArea(); var used = GetCumulativeItemAreas((uid, storage)); _appearance.SetData(uid, StorageVisuals.StorageUsed, used, appearance); _appearance.SetData(uid, StorageVisuals.Capacity, capacity, appearance); _appearance.SetData(uid, StorageVisuals.Open, storage.IsUiOpen, appearance); _appearance.SetData(uid, SharedBagOpenVisuals.BagState, storage.IsUiOpen ? SharedBagState.Open : SharedBagState.Closed, appearance); _appearance.SetData(uid, StackVisuals.Hide, !storage.IsUiOpen, appearance); } /// /// Move entities from one storage to another. /// public void TransferEntities(EntityUid source, EntityUid target, EntityUid? user = null, StorageComponent? sourceComp = null, LockComponent? sourceLock = null, StorageComponent? targetComp = null, LockComponent? targetLock = null) { if (!Resolve(source, ref sourceComp) || !Resolve(target, ref targetComp)) return; var entities = sourceComp.Container.ContainedEntities; if (entities.Count == 0) return; if (Resolve(source, ref sourceLock, false) && sourceLock.Locked || Resolve(target, ref targetLock, false) && targetLock.Locked) return; foreach (var entity in entities.ToArray()) { Insert(target, entity, out _, user: user, targetComp, playSound: false); } Audio.PlayPredicted(sourceComp.StorageInsertSound, target, user); } /// /// Verifies if an entity can be stored and if it fits /// /// The entity to check /// /// If returning false, the reason displayed to the player /// /// /// /// /// true if it can be inserted, false otherwise public bool CanInsert( EntityUid uid, EntityUid insertEnt, out string? reason, StorageComponent? storageComp = null, ItemComponent? item = null, bool ignoreStacks = false, bool ignoreLocation = false) { if (!Resolve(uid, ref storageComp) || !Resolve(insertEnt, ref item, false)) { reason = null; return false; } if (Transform(insertEnt).Anchored) { reason = "comp-storage-anchored-failure"; return false; } if (storageComp.Whitelist?.IsValid(insertEnt, EntityManager) == false) { reason = "comp-storage-invalid-container"; return false; } if (storageComp.Blacklist?.IsValid(insertEnt, EntityManager) == true) { reason = "comp-storage-invalid-container"; return false; } if (!ignoreStacks && _stackQuery.TryGetComponent(insertEnt, out var stack) && HasSpaceInStacks((uid, storageComp), stack.StackTypeId)) { reason = null; return true; } var maxSize = GetMaxItemSize((uid, storageComp)); if (ItemSystem.GetSizePrototype(item.Size) > maxSize) { reason = "comp-storage-too-big"; return false; } if (TryComp(insertEnt, out var insertStorage) && GetMaxItemSize((insertEnt, insertStorage)) >= maxSize) { reason = "comp-storage-too-big"; return false; } if (!ignoreLocation && !storageComp.StoredItems.ContainsKey(insertEnt)) { if (!TryGetAvailableGridSpace((uid, storageComp), (insertEnt, item), out _)) { reason = "comp-storage-insufficient-capacity"; return false; } } CheckingCanInsert = true; if (!_containerSystem.CanInsert(insertEnt, storageComp.Container)) { CheckingCanInsert = false; reason = null; return false; } CheckingCanInsert = false; reason = null; return true; } /// /// Inserts into the storage container at a given location /// /// true if the entity was inserted, false otherwise. This will also return true if a stack was partially /// inserted. public bool InsertAt( Entity uid, Entity insertEnt, ItemStorageLocation location, out EntityUid? stackedEntity, EntityUid? user = null, bool playSound = true, bool stackAutomatically = true) { stackedEntity = null; if (!Resolve(uid, ref uid.Comp)) return false; if (!ItemFitsInGridLocation(insertEnt, uid, location)) return false; uid.Comp.StoredItems[insertEnt] = location; Dirty(uid, uid.Comp); if (Insert(uid, insertEnt, out stackedEntity, out _, user: user, storageComp: uid.Comp, playSound: playSound, stackAutomatically: stackAutomatically)) { return true; } uid.Comp.StoredItems.Remove(insertEnt); return false; } /// /// Inserts into the storage container /// /// true if the entity was inserted, false otherwise. This will also return true if a stack was partially /// inserted. public bool Insert( EntityUid uid, EntityUid insertEnt, out EntityUid? stackedEntity, EntityUid? user = null, StorageComponent? storageComp = null, bool playSound = true, bool stackAutomatically = true) { return Insert(uid, insertEnt, out stackedEntity, out _, user: user, storageComp: storageComp, playSound: playSound, stackAutomatically: stackAutomatically); } /// /// Inserts into the storage container /// /// true if the entity was inserted, false otherwise. This will also return true if a stack was partially /// inserted public bool Insert( EntityUid uid, EntityUid insertEnt, out EntityUid? stackedEntity, out string? reason, EntityUid? user = null, StorageComponent? storageComp = null, bool playSound = true, bool stackAutomatically = true) { stackedEntity = null; reason = null; if (!Resolve(uid, ref storageComp)) return false; /* * 1. If the inserted thing is stackable then try to stack it to existing stacks * 2. If anything remains insert whatever is possible. * 3. If insertion is not possible then leave the stack as is. * At either rate still play the insertion sound * * For now we just treat items as always being the same size regardless of stack count. */ if (!stackAutomatically || !_stackQuery.TryGetComponent(insertEnt, out var insertStack)) { if (!_containerSystem.Insert(insertEnt, storageComp.Container)) return false; if (playSound) Audio.PlayPredicted(storageComp.StorageInsertSound, uid, user); return true; } var toInsertCount = insertStack.Count; foreach (var ent in storageComp.Container.ContainedEntities) { if (!_stackQuery.TryGetComponent(ent, out var containedStack)) continue; if (!_stack.TryAdd(insertEnt, ent, insertStack, containedStack)) continue; stackedEntity = ent; if (insertStack.Count == 0) break; } // Still stackable remaining if (insertStack.Count > 0 && !_containerSystem.Insert(insertEnt, storageComp.Container) && toInsertCount == insertStack.Count) { // Failed to insert anything. return false; } if (playSound) Audio.PlayPredicted(storageComp.StorageInsertSound, uid, user); return true; } /// /// Inserts an entity into storage from the player's active hand /// /// /// The player to insert an entity from /// /// true if inserted, false otherwise public bool PlayerInsertHeldEntity(EntityUid uid, EntityUid player, StorageComponent? storageComp = null) { if (!Resolve(uid, ref storageComp) || !TryComp(player, out HandsComponent? hands) || hands.ActiveHandEntity == null) return false; var toInsert = hands.ActiveHandEntity; if (!CanInsert(uid, toInsert.Value, out var reason, storageComp)) { _popupSystem.PopupClient(Loc.GetString(reason ?? "comp-storage-cant-insert"), uid, player); return false; } if (!_sharedHandsSystem.CanDrop(player, toInsert.Value, hands)) { _popupSystem.PopupClient(Loc.GetString("comp-storage-cant-drop", ("entity", toInsert.Value)), uid, player); return false; } return PlayerInsertEntityInWorld((uid, storageComp), player, toInsert.Value); } /// /// Inserts an Entity () in the world into storage, informing if it fails. /// is *NOT* held, see . /// /// /// The player to insert an entity with /// /// true if inserted, false otherwise public bool PlayerInsertEntityInWorld(Entity uid, EntityUid player, EntityUid toInsert) { if (!Resolve(uid, ref uid.Comp) || !_interactionSystem.InRangeUnobstructed(player, uid)) return false; if (!Insert(uid, toInsert, out _, user: player, uid.Comp)) { _popupSystem.PopupClient(Loc.GetString("comp-storage-cant-insert"), uid, player); return false; } return true; } /// /// Attempts to set the location of an item already inside of a storage container. /// public bool TrySetItemStorageLocation(Entity itemEnt, Entity storageEnt, ItemStorageLocation location) { if (!Resolve(itemEnt, ref itemEnt.Comp) || !Resolve(storageEnt, ref storageEnt.Comp)) return false; if (!storageEnt.Comp.Container.ContainedEntities.Contains(itemEnt)) return false; if (!ItemFitsInGridLocation(itemEnt, storageEnt, location.Position, location.Rotation)) return false; storageEnt.Comp.StoredItems[itemEnt] = location; Dirty(storageEnt, storageEnt.Comp); return true; } /// /// Tries to find the first available spot on a storage grid. /// starts at the top-left and goes right and down. /// public bool TryGetAvailableGridSpace( Entity storageEnt, Entity itemEnt, [NotNullWhen(true)] out ItemStorageLocation? storageLocation) { storageLocation = null; if (!Resolve(storageEnt, ref storageEnt.Comp) || !Resolve(itemEnt, ref itemEnt.Comp)) return false; var storageBounding = storageEnt.Comp.Grid.GetBoundingBox(); Angle startAngle; if (storageEnt.Comp.DefaultStorageOrientation == null) { startAngle = Angle.FromDegrees(-itemEnt.Comp.StoredRotation); } else { if (storageBounding.Width < storageBounding.Height) { startAngle = storageEnt.Comp.DefaultStorageOrientation == StorageDefaultOrientation.Horizontal ? Angle.Zero : Angle.FromDegrees(90); } else { startAngle = storageEnt.Comp.DefaultStorageOrientation == StorageDefaultOrientation.Vertical ? Angle.Zero : Angle.FromDegrees(90); } } for (var y = storageBounding.Bottom; y <= storageBounding.Top; y++) { for (var x = storageBounding.Left; x <= storageBounding.Right; x++) { for (var angle = startAngle; angle <= Angle.FromDegrees(360 - startAngle); angle += Math.PI / 2f) { var location = new ItemStorageLocation(angle, (x, y)); if (ItemFitsInGridLocation(itemEnt, storageEnt, location)) { storageLocation = location; return true; } } } } return false; } /// /// Checks if an item fits into a specific spot on a storage grid. /// public bool ItemFitsInGridLocation( Entity itemEnt, Entity storageEnt, ItemStorageLocation location) { return ItemFitsInGridLocation(itemEnt, storageEnt, location.Position, location.Rotation); } /// /// Checks if an item fits into a specific spot on a storage grid. /// public bool ItemFitsInGridLocation( Entity itemEnt, Entity storageEnt, Vector2i position, Angle rotation) { if (!Resolve(itemEnt, ref itemEnt.Comp) || !Resolve(storageEnt, ref storageEnt.Comp)) return false; var gridBounds = storageEnt.Comp.Grid.GetBoundingBox(); if (!gridBounds.Contains(position)) return false; var itemShape = ItemSystem.GetAdjustedItemShape(itemEnt, rotation, position); foreach (var box in itemShape) { for (var offsetY = box.Bottom; offsetY <= box.Top; offsetY++) { for (var offsetX = box.Left; offsetX <= box.Right; offsetX++) { var pos = (offsetX, offsetY); if (!IsGridSpaceEmpty(itemEnt, storageEnt, pos)) return false; } } } return true; } /// /// Checks if a space on a grid is valid and not occupied by any other pieces. /// public bool IsGridSpaceEmpty(Entity itemEnt, Entity storageEnt, Vector2i location) { if (!Resolve(storageEnt, ref storageEnt.Comp)) return false; var validGrid = false; foreach (var grid in storageEnt.Comp.Grid) { if (grid.Contains(location)) { validGrid = true; break; } } if (!validGrid) return false; foreach (var (ent, storedItem) in storageEnt.Comp.StoredItems) { if (ent == itemEnt.Owner) continue; if (!_itemQuery.TryGetComponent(ent, out var itemComp)) continue; var adjustedShape = ItemSystem.GetAdjustedItemShape((ent, itemComp), storedItem); foreach (var box in adjustedShape) { if (box.Contains(location)) return false; } } return true; } /// /// Returns true if there is enough space to theoretically fit another item. /// public bool HasSpace(Entity uid) { if (!Resolve(uid, ref uid.Comp)) return false; return GetCumulativeItemAreas(uid) < uid.Comp.Grid.GetArea() || HasSpaceInStacks(uid); } private bool HasSpaceInStacks(Entity uid, string? stackType = null) { if (!Resolve(uid, ref uid.Comp)) return false; foreach (var contained in uid.Comp.Container.ContainedEntities) { if (!_stackQuery.TryGetComponent(contained, out var stack)) continue; if (stackType != null && !stack.StackTypeId.Equals(stackType)) continue; if (_stack.GetAvailableSpace(stack) == 0) continue; return true; } return false; } /// /// Returns the sum of all the ItemSizes of the items inside of a storage. /// public int GetCumulativeItemAreas(Entity entity) { if (!Resolve(entity, ref entity.Comp)) return 0; var sum = 0; foreach (var item in entity.Comp.Container.ContainedEntities) { if (!_itemQuery.TryGetComponent(item, out var itemComp)) continue; sum += ItemSystem.GetItemShape((item, itemComp)).GetArea(); } return sum; } public ItemSizePrototype GetMaxItemSize(Entity uid) { if (!Resolve(uid, ref uid.Comp)) return _defaultStorageMaxItemSize; // If we specify a max item size, use that if (uid.Comp.MaxItemSize != null) return _prototype.Index(uid.Comp.MaxItemSize.Value); if (!_itemQuery.TryGetComponent(uid, out var item)) return _defaultStorageMaxItemSize; // if there is no max item size specified, the value used // is one below the item size of the storage entity. return _nextSmallest[item.Size]; } private void OnStackCountChanged(EntityUid uid, MetaDataComponent component, StackCountChangedEvent args) { if (_containerSystem.TryGetContainingContainer(uid, out var container, component) && container.ID == StorageComponent.ContainerId) { UpdateAppearance(container.Owner); UpdateUI(container.Owner); } } /// /// Plays a clientside pickup animation for the specified uid. /// public abstract void PlayPickupAnimation(EntityUid uid, EntityCoordinates initialCoordinates, EntityCoordinates finalCoordinates, Angle initialRotation, EntityUid? user = null); [Serializable, NetSerializable] protected sealed class StorageComponentState : ComponentState { public bool IsUiOpen; public Dictionary StoredItems = new(); public List Grid = new(); public ProtoId? MaxItemSize; } }