#nullable enable using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using Content.Server.DoAfter; using Content.Server.Hands.Components; using Content.Server.Items; using Content.Server.Placeable; using Content.Shared.Acts; using Content.Shared.Audio; using Content.Shared.Interaction; using Content.Shared.Interaction.Helpers; using Content.Shared.Item; using Content.Shared.Notification; using Content.Shared.Storage; using Robust.Server.GameObjects; using Robust.Server.Player; using Robust.Shared.Audio; using Robust.Shared.Containers; using Robust.Shared.Enums; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Log; using Robust.Shared.Map; using Robust.Shared.Network; using Robust.Shared.Player; using Robust.Shared.Players; using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.ViewVariables; namespace Content.Server.Storage.Components { /// /// Storage component for containing entities within this one, matches a UI on the client which shows stored entities /// [RegisterComponent] [ComponentReference(typeof(IActivate))] [ComponentReference(typeof(IStorageComponent))] public class ServerStorageComponent : SharedStorageComponent, IInteractUsing, IUse, IActivate, IStorageComponent, IDestroyAct, IExAct, IAfterInteract { private const string LoggerName = "Storage"; private Container? _storage; private readonly Dictionary _sizeCache = new(); [DataField("occludesLight")] private bool _occludesLight = true; [DataField("quickInsert")] private bool _quickInsert; //Can insert storables by "attacking" them with the storage entity [DataField("areaInsert")] private bool _areaInsert; //"Attacking" with the storage entity causes it to insert all nearby storables after a delay private bool _storageInitialCalculated; private int _storageUsed; [DataField("capacity")] private int _storageCapacityMax = 10000; public readonly HashSet SubscribedSessions = new(); [DataField("storageSoundCollection")] public string? StorageSoundCollection { get; set; } [ViewVariables] public override IReadOnlyList? StoredEntities => _storage?.ContainedEntities; [ViewVariables(VVAccess.ReadWrite)] public bool OccludesLight { get => _occludesLight; set { _occludesLight = value; if (_storage != null) _storage.OccludesLight = value; } } private void EnsureInitialCalculated() { if (_storageInitialCalculated) { return; } RecalculateStorageUsed(); _storageInitialCalculated = true; } private void RecalculateStorageUsed() { _storageUsed = 0; if (_storage == null) { return; } foreach (var entity in _storage.ContainedEntities) { var item = entity.GetComponent(); _storageUsed += item.Size; } } /// /// Verifies if an entity can be stored and if it fits /// /// The entity to check /// true if it can be inserted, false otherwise public bool CanInsert(IEntity entity) { EnsureInitialCalculated(); if (entity.TryGetComponent(out ServerStorageComponent? storage) && storage._storageCapacityMax >= _storageCapacityMax) { return false; } if (entity.TryGetComponent(out SharedItemComponent? store) && store.Size > _storageCapacityMax - _storageUsed) { return false; } return true; } /// /// Inserts into the storage container /// /// The entity to insert /// true if the entity was inserted, false otherwise public bool Insert(IEntity entity) { return CanInsert(entity) && _storage?.Insert(entity) == true; } public override bool Remove(IEntity entity) { EnsureInitialCalculated(); return _storage?.Remove(entity) == true; } public void HandleEntityMaybeInserted(EntInsertedIntoContainerMessage message) { if (message.Container != _storage) { return; } PlaySoundCollection(StorageSoundCollection); EnsureInitialCalculated(); Logger.DebugS(LoggerName, $"Storage (UID {Owner.Uid}) had entity (UID {message.Entity.Uid}) inserted into it."); var size = 0; if (message.Entity.TryGetComponent(out SharedItemComponent? storable)) size = storable.Size; _storageUsed += size; _sizeCache[message.Entity] = size; UpdateClientInventories(); } public void HandleEntityMaybeRemoved(EntRemovedFromContainerMessage message) { if (message.Container != _storage) { return; } EnsureInitialCalculated(); Logger.DebugS(LoggerName, $"Storage (UID {Owner}) had entity (UID {message.Entity}) removed from it."); if (!_sizeCache.TryGetValue(message.Entity, out var size)) { Logger.WarningS(LoggerName, $"Removed entity {message.Entity} without a cached size from storage {Owner} at {Owner.Transform.MapPosition}"); RecalculateStorageUsed(); return; } _storageUsed -= size; UpdateClientInventories(); } /// /// 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(IEntity player) { EnsureInitialCalculated(); if (!player.TryGetComponent(out IHandsComponent? hands) || hands.GetActiveHand == null) { return false; } var toInsert = hands.GetActiveHand; if (!hands.Drop(toInsert.Owner)) { Owner.PopupMessage(player, "Can't insert."); return false; } if (!Insert(toInsert.Owner)) { hands.PutInHand(toInsert); Owner.PopupMessage(player, "Can't insert."); return false; } return true; } /// /// 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(IEntity player, IEntity toInsert) { EnsureInitialCalculated(); if (!Insert(toInsert)) { Owner.PopupMessage(player, "Can't insert."); return false; } return true; } /// /// Opens the storage UI for an entity /// /// The entity to open the UI for public void OpenStorageUI(IEntity entity) { PlaySoundCollection(StorageSoundCollection); EnsureInitialCalculated(); var userSession = entity.GetComponent().PlayerSession; Logger.DebugS(LoggerName, $"Storage (UID {Owner.Uid}) \"used\" by player session (UID {userSession.AttachedEntityUid})."); SubscribeSession(userSession); SendNetworkMessage(new OpenStorageUIMessage(), userSession.ConnectedClient); UpdateClientInventory(userSession); } /// /// Updates the storage UI on all subscribed actors, informing them of the state of the container. /// private void UpdateClientInventories() { foreach (var session in SubscribedSessions) { UpdateClientInventory(session); } } /// /// Updates storage UI on a client, informing them of the state of the container. /// /// The client to be updated private void UpdateClientInventory(IPlayerSession session) { if (session.AttachedEntity == null) { Logger.DebugS(LoggerName, $"Storage (UID {Owner.Uid}) detected no attached entity in player session (UID {session.AttachedEntityUid})."); UnsubscribeSession(session); return; } if (_storage == null) { Logger.WarningS(LoggerName, $"{nameof(UpdateClientInventory)} called with null {nameof(_storage)}"); return; } if (StoredEntities == null) { Logger.WarningS(LoggerName, $"{nameof(UpdateClientInventory)} called with null {nameof(StoredEntities)}"); return; } var stored = StoredEntities.Select(e => e.Uid).ToArray(); SendNetworkMessage(new StorageHeldItemsMessage(stored, _storageUsed, _storageCapacityMax), session.ConnectedClient); } /// /// Adds a session to the update list. /// /// The session to add private void SubscribeSession(IPlayerSession session) { EnsureInitialCalculated(); if (!SubscribedSessions.Contains(session)) { Logger.DebugS(LoggerName, $"Storage (UID {Owner.Uid}) subscribed player session (UID {session.AttachedEntityUid})."); session.PlayerStatusChanged += HandlePlayerSessionChangeEvent; SubscribedSessions.Add(session); UpdateDoorState(); } } /// /// Removes a session from the update list. /// /// The session to remove public void UnsubscribeSession(IPlayerSession session) { if (SubscribedSessions.Contains(session)) { Logger.DebugS(LoggerName, $"Storage (UID {Owner.Uid}) unsubscribed player session (UID {session.AttachedEntityUid})."); SubscribedSessions.Remove(session); SendNetworkMessage(new CloseStorageUIMessage(), session.ConnectedClient); UpdateDoorState(); } } private void HandlePlayerSessionChangeEvent(object? obj, SessionStatusEventArgs sessionStatus) { Logger.DebugS(LoggerName, $"Storage (UID {Owner.Uid}) handled a status change in player session (UID {sessionStatus.Session.AttachedEntityUid})."); if (sessionStatus.NewStatus != SessionStatus.InGame) { UnsubscribeSession(sessionStatus.Session); } } private void UpdateDoorState() { if (Owner.TryGetComponent(out AppearanceComponent? appearance)) { appearance.SetData(StorageVisuals.Open, SubscribedSessions.Count != 0); } } public override void Initialize() { base.Initialize(); // ReSharper disable once StringLiteralTypo _storage = ContainerHelpers.EnsureContainer(Owner, "storagebase"); _storage.OccludesLight = _occludesLight; } public override void HandleNetworkMessage(ComponentMessage message, INetChannel channel, ICommonSession? session = null) { base.HandleNetworkMessage(message, channel, session); if (session == null) { throw new ArgumentException(nameof(session)); } switch (message) { case RemoveEntityMessage remove: { EnsureInitialCalculated(); var player = session.AttachedEntity; if (player == null) { break; } var ownerTransform = Owner.Transform; var playerTransform = player.Transform; if (!playerTransform.Coordinates.InRange(Owner.EntityManager, ownerTransform.Coordinates, 2) || !ownerTransform.IsMapTransform && !playerTransform.ContainsEntity(ownerTransform)) { break; } var entity = Owner.EntityManager.GetEntity(remove.EntityUid); if (entity == null || _storage?.Contains(entity) == false) { break; } var item = entity.GetComponent(); if (item == null || !player.TryGetComponent(out HandsComponent? hands)) { break; } if (!hands.CanPutInHand(item)) { break; } hands.PutInHand(item); break; } case InsertEntityMessage _: { EnsureInitialCalculated(); var player = session.AttachedEntity; if (player == null) { break; } if (!player.InRangeUnobstructed(Owner, popup: true)) { break; } PlayerInsertHeldEntity(player); break; } case CloseStorageUIMessage _: { if (session is not IPlayerSession playerSession) { break; } UnsubscribeSession(playerSession); break; } } } /// /// Inserts storable entities into this storage container if possible, otherwise return to the hand of the user /// /// /// true if inserted, false otherwise async Task IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs) { Logger.DebugS(LoggerName, $"Storage (UID {Owner.Uid}) attacked by user (UID {eventArgs.User.Uid}) with entity (UID {eventArgs.Using.Uid})."); if (Owner.HasComponent()) { return false; } return PlayerInsertHeldEntity(eventArgs.User); } /// /// Sends a message to open the storage UI /// /// /// bool IUse.UseEntity(UseEntityEventArgs eventArgs) { EnsureInitialCalculated(); OpenStorageUI(eventArgs.User); return false; } void IActivate.Activate(ActivateEventArgs eventArgs) { ((IUse) this).UseEntity(new UseEntityEventArgs(eventArgs.User)); } /// /// Allows a user to pick up entities by clicking them, or pick up all entities in a certain radius /// arround a click. /// /// /// async Task IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs) { if (!eventArgs.InRangeUnobstructed(ignoreInsideBlocker: true, popup: true)) return false; // 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(_areaInsert && (eventArgs.Target == null || !eventArgs.Target.HasComponent())) { var validStorables = new List(); foreach (var entity in IoCManager.Resolve().GetEntitiesInRange(eventArgs.ClickLocation, 1)) { if (!entity.Transform.IsMapTransform || entity == eventArgs.User || !entity.HasComponent()) continue; validStorables.Add(entity); } //If there's only one then let's be generous if (validStorables.Count > 1) { var doAfterSystem = EntitySystem.Get(); var doAfterArgs = new DoAfterEventArgs(eventArgs.User, 0.2f * validStorables.Count, CancellationToken.None, Owner) { BreakOnStun = true, BreakOnDamage = true, BreakOnUserMove = true, NeedHand = true, }; var result = await doAfterSystem.DoAfter(doAfterArgs); if (result != DoAfterStatus.Finished) return true; } var successfullyInserted = new List(); var successfullyInsertedPositions = new List(); foreach (var entity in validStorables) { // Check again, situation may have changed for some entities, but we'll still pick up any that are valid if (!entity.Transform.IsMapTransform || entity == eventArgs.User || !entity.HasComponent()) continue; var coords = entity.Transform.Coordinates; if (PlayerInsertEntityInWorld(eventArgs.User, entity)) { successfullyInserted.Add(entity.Uid); successfullyInsertedPositions.Add(coords); } } // If we picked up atleast one thing, play a sound and do a cool animation! if (successfullyInserted.Count>0) { PlaySoundCollection(StorageSoundCollection); SendNetworkMessage( new AnimateInsertingEntitiesMessage( successfullyInserted, successfullyInsertedPositions ) ); } return true; } // Pick up the clicked entity else if(_quickInsert) { if (eventArgs.Target == null || !eventArgs.Target.Transform.IsMapTransform || eventArgs.Target == eventArgs.User || !eventArgs.Target.HasComponent()) return false; var position = eventArgs.Target.Transform.Coordinates; if(PlayerInsertEntityInWorld(eventArgs.User, eventArgs.Target)) { SendNetworkMessage(new AnimateInsertingEntitiesMessage( new List() { eventArgs.Target.Uid }, new List() { position } )); return true; } return true; } return false; } void IDestroyAct.OnDestroy(DestructionEventArgs eventArgs) { var storedEntities = StoredEntities?.ToList(); if (storedEntities == null) { return; } foreach (var entity in storedEntities) { Remove(entity); } } void IExAct.OnExplosion(ExplosionEventArgs eventArgs) { if (eventArgs.Severity < ExplosionSeverity.Heavy) { return; } var storedEntities = StoredEntities?.ToList(); if (storedEntities == null) { return; } foreach (var entity in storedEntities) { var exActs = entity.GetAllComponents().ToArray(); foreach (var exAct in exActs) { exAct.OnExplosion(eventArgs); } } } protected void PlaySoundCollection(string? name) { if (string.IsNullOrEmpty(name)) { return; } var file = AudioHelpers.GetRandomFileFromSoundCollection(name); SoundSystem.Play(Filter.Pvs(Owner), file, Owner, AudioParams.Default); } } }