diff --git a/Content.Client/Storage/Components/EntityStorageComponent.cs b/Content.Client/Storage/Components/EntityStorageComponent.cs new file mode 100644 index 0000000000..65b510cf6f --- /dev/null +++ b/Content.Client/Storage/Components/EntityStorageComponent.cs @@ -0,0 +1,10 @@ +using Content.Shared.Storage.Components; +using Robust.Shared.GameStates; + +namespace Content.Client.Storage.Components; + +[RegisterComponent, ComponentReference(typeof(SharedEntityStorageComponent))] +public sealed class EntityStorageComponent : SharedEntityStorageComponent +{ + +} diff --git a/Content.Client/Storage/Systems/EntityStorageSystem.cs b/Content.Client/Storage/Systems/EntityStorageSystem.cs new file mode 100644 index 0000000000..36532b0e9f --- /dev/null +++ b/Content.Client/Storage/Systems/EntityStorageSystem.cs @@ -0,0 +1,8 @@ +using Content.Shared.Storage.EntitySystems; + +namespace Content.Client.Storage.Systems; + +public sealed class EntityStorageSystem : SharedEntityStorageSystem +{ + +} diff --git a/Content.Server/Storage/Components/EntityStorageComponent.cs b/Content.Server/Storage/Components/EntityStorageComponent.cs index 705ad00ab9..568c67bc06 100644 --- a/Content.Server/Storage/Components/EntityStorageComponent.cs +++ b/Content.Server/Storage/Components/EntityStorageComponent.cs @@ -1,97 +1,12 @@ using Content.Server.Atmos; -using Content.Shared.Physics; -using Content.Shared.Whitelist; -using Robust.Shared.Audio; -using Robust.Shared.Containers; +using Content.Shared.Storage.Components; +using Robust.Shared.GameStates; namespace Content.Server.Storage.Components; -[RegisterComponent] -public sealed class EntityStorageComponent : Component, IGasMixtureHolder +[RegisterComponent, ComponentReference(typeof(SharedEntityStorageComponent))] +public sealed class EntityStorageComponent : SharedEntityStorageComponent, IGasMixtureHolder { - public readonly float MaxSize = 1.0f; // maximum width or height of an entity allowed inside the storage. - public const float GasMixVolume = 70f; - - public static readonly TimeSpan InternalOpenAttemptDelay = TimeSpan.FromSeconds(0.5); - public TimeSpan LastInternalOpenAttempt; - - /// - /// Collision masks that get removed when the storage gets opened. - /// - public readonly int MasksToRemove = (int) ( - CollisionGroup.MidImpassable | - CollisionGroup.HighImpassable | - CollisionGroup.LowImpassable); - - /// - /// Collision masks that were removed from ANY layer when the storage was opened; - /// - [DataField("removedMasks")] - public int RemovedMasks; - - [DataField("capacity")] - public int Capacity = 30; - - [DataField("isCollidableWhenOpen")] - public bool IsCollidableWhenOpen; - - /// - /// If true, it opens the storage when the entity inside of it moves - /// If false, it prevents the storage from opening when the entity inside of it moves. - /// This is for objects that you want the player to move while inside, like large cardboard boxes, without opening the storage. - /// - [ViewVariables(VVAccess.ReadWrite)] - [DataField("openOnMove")] - public bool OpenOnMove = true; - - //The offset for where items are emptied/vacuumed for the EntityStorage. - [DataField("enteringOffset")] - public Vector2 EnteringOffset = new(0, 0); - - //The collision groups checked, so that items are depositied or grabbed from inside walls. - [DataField("enteringOffsetCollisionFlags")] - public readonly CollisionGroup EnteringOffsetCollisionFlags = CollisionGroup.Impassable | CollisionGroup.MidImpassable; - - [DataField("enteringRange")] - public float EnteringRange = 0.18f; - - [DataField("showContents")] - public bool ShowContents; - - [DataField("occludesLight")] - public bool OccludesLight = true; - - [DataField("deleteContentsOnDestruction"), ViewVariables(VVAccess.ReadWrite)] - public bool DeleteContentsOnDestruction = false; - - /// - /// Whether or not the container is sealed and traps air inside of it - /// - [DataField("airtight"), ViewVariables(VVAccess.ReadWrite)] - public bool Airtight = true; - - [DataField("open")] - public bool Open; - - [DataField("closeSound")] - public SoundSpecifier CloseSound = new SoundPathSpecifier("/Audio/Effects/closetclose.ogg"); - - [DataField("openSound")] - public SoundSpecifier OpenSound = new SoundPathSpecifier("/Audio/Effects/closetopen.ogg"); - - /// - /// Whitelist for what entities are allowed to be inserted into this container. If this is not null, the - /// standard requirement that the entity must be an item or mob is waived. - /// - [DataField("whitelist")] - public EntityWhitelist? Whitelist; - - [ViewVariables] - public Container Contents = default!; - - [ViewVariables(VVAccess.ReadWrite)] - public bool IsWeldedShut; - /// /// Gas currently contained in this entity storage. /// None while open. Grabs gas from the atmosphere when closed, and exposes any entities inside to it. diff --git a/Content.Server/Storage/EntitySystems/EntityStorageSystem.cs b/Content.Server/Storage/EntitySystems/EntityStorageSystem.cs index 3d2bb019cc..ebefd304f7 100644 --- a/Content.Server/Storage/EntitySystems/EntityStorageSystem.cs +++ b/Content.Server/Storage/EntitySystems/EntityStorageSystem.cs @@ -1,93 +1,50 @@ -using System.Linq; using Content.Server.Atmos.EntitySystems; using Content.Server.Construction; using Content.Server.Construction.Components; -using Content.Server.Popups; using Content.Server.Storage.Components; using Content.Server.Tools.Systems; -using Content.Shared.Body.Components; -using Content.Shared.Destructible; -using Content.Shared.Hands.Components; -using Content.Shared.Interaction; -using Content.Shared.Item; -using Content.Shared.Lock; -using Content.Shared.Placeable; -using Content.Shared.Storage; using Content.Shared.Storage.Components; -using Content.Shared.Wall; -using Content.Shared.Whitelist; -using Robust.Server.Containers; +using Content.Shared.Storage.EntitySystems; using Robust.Shared.Containers; using Robust.Shared.Map; -using Robust.Shared.Physics; -using Robust.Shared.Physics.Components; -using Robust.Shared.Physics.Systems; namespace Content.Server.Storage.EntitySystems; -public sealed class EntityStorageSystem : EntitySystem +public sealed class EntityStorageSystem : SharedEntityStorageSystem { - [Dependency] private readonly SharedAudioSystem _audio = default!; - [Dependency] private readonly SharedAppearanceSystem _appearance = default!; [Dependency] private readonly ConstructionSystem _construction = default!; - [Dependency] private readonly ContainerSystem _container = default!; - [Dependency] private readonly EntityLookupSystem _lookup = default!; - [Dependency] private readonly SharedInteractionSystem _interactionSystem = default!; - [Dependency] private readonly PlaceableSurfaceSystem _placeableSurface = default!; - [Dependency] private readonly PopupSystem _popupSystem = default!; [Dependency] private readonly AtmosphereSystem _atmos = default!; - [Dependency] private readonly SharedPhysicsSystem _physics = default!; [Dependency] private readonly IMapManager _map = default!; - public const string ContainerName = "entity_storage"; - public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(OnInit); - SubscribeLocalEvent(OnInteract); SubscribeLocalEvent(OnWeldableAttempt); SubscribeLocalEvent(OnWelded); - SubscribeLocalEvent(OnLockToggleAttempt); - SubscribeLocalEvent(OnDestruction); - SubscribeLocalEvent(OnRemoved); SubscribeLocalEvent(OnInsideInhale); SubscribeLocalEvent(OnInsideExhale); SubscribeLocalEvent(OnInsideExposed); + SubscribeLocalEvent(OnRemoved); } - private void OnInit(EntityUid uid, EntityStorageComponent component, ComponentInit args) + protected override void OnInit(EntityUid uid, SharedEntityStorageComponent component, ComponentInit args) { - component.Contents = _container.EnsureContainer(uid, ContainerName); - component.Contents.ShowContents = component.ShowContents; - component.Contents.OccludesLight = component.OccludesLight; + base.OnInit(uid, component, args); if (TryComp(uid, out var construction)) _construction.AddContainer(uid, ContainerName, construction); - if (TryComp(uid, out var placeable)) - _placeableSurface.SetPlaceable(uid, component.Open, placeable); - if (!component.Open) { // If we're closed on spawn, we need to pull some air into our environment from where we spawned, // so that we have -something-. For example, if you bought an animal crate or something. - TakeGas(uid, component); + TakeGas(uid, (EntityStorageComponent) component); } } - private void OnInteract(EntityUid uid, EntityStorageComponent component, ActivateInWorldEvent args) - { - if (args.Handled) - return; - - args.Handled = true; - ToggleOpen(args.User, uid, component); - } - private void OnWeldableAttempt(EntityUid uid, EntityStorageComponent component, WeldableAttemptEvent args) { if (component.Open) @@ -99,7 +56,7 @@ public sealed class EntityStorageSystem : EntitySystem if (component.Contents.Contains(args.User)) { var msg = Loc.GetString("entity-storage-component-already-contains-user-message"); - _popupSystem.PopupEntity(msg, args.User, args.User); + Popup.PopupEntity(msg, args.User, args.User); args.Cancel(); } } @@ -107,334 +64,36 @@ public sealed class EntityStorageSystem : EntitySystem private void OnWelded(EntityUid uid, EntityStorageComponent component, WeldableChangedEvent args) { component.IsWeldedShut = args.IsWelded; + Dirty(component); } - private void OnLockToggleAttempt(EntityUid uid, EntityStorageComponent target, ref LockToggleAttemptEvent args) - { - // Cannot (un)lock open lockers. - if (target.Open) - args.Cancelled = true; - - // Cannot (un)lock from the inside. Maybe a bad idea? Security jocks could trap nerds in lockers? - if (target.Contents.Contains(args.User)) - args.Cancelled = true; - } - - private void OnDestruction(EntityUid uid, EntityStorageComponent component, DestructionEventArgs args) - { - component.Open = true; - if (!component.DeleteContentsOnDestruction) - { - EmptyContents(uid, component); - return; - } - - foreach (var ent in new List(component.Contents.ContainedEntities)) - { - EntityManager.DeleteEntity(ent); - } - } - - public void ToggleOpen(EntityUid user, EntityUid target, EntityStorageComponent? component = null) - { - if (!Resolve(target, ref component)) - return; - - if (component.Open) - { - TryCloseStorage(target); - } - else - { - TryOpenStorage(user, target); - } - } - - public void EmptyContents(EntityUid uid, EntityStorageComponent? component = null) - { - if (!Resolve(uid, ref component)) - return; - - var uidXform = Transform(uid); - var containedArr = component.Contents.ContainedEntities.ToArray(); - foreach (var contained in containedArr) - { - Remove(contained, uid, component, uidXform); - } - } - - public void OpenStorage(EntityUid uid, EntityStorageComponent? component = null) - { - if (!Resolve(uid, ref component)) - return; - - var beforeev = new StorageBeforeOpenEvent(); - RaiseLocalEvent(uid, ref beforeev); - component.Open = true; - EmptyContents(uid, component); - ModifyComponents(uid, component); - _audio.PlayPvs(component.OpenSound, uid); - ReleaseGas(uid, component); - var afterev = new StorageAfterOpenEvent(); - RaiseLocalEvent(uid, ref afterev); - } - - public void CloseStorage(EntityUid uid, EntityStorageComponent? component = null) - { - if (!Resolve(uid, ref component)) - return; - component.Open = false; - - var targetCoordinates = new EntityCoordinates(uid, component.EnteringOffset); - - var entities = _lookup.GetEntitiesInRange(targetCoordinates, component.EnteringRange, LookupFlags.Approximate | LookupFlags.Dynamic | LookupFlags.Sundries); - - var ev = new StorageBeforeCloseEvent(entities, new()); - RaiseLocalEvent(uid, ref ev); - var count = 0; - foreach (var entity in ev.Contents) - { - if (!ev.BypassChecks.Contains(entity)) - { - if (!CanFit(entity, uid, component.Whitelist)) - continue; - } - - if (!AddToContents(entity, uid, component)) - continue; - - count++; - if (count >= component.Capacity) - break; - } - - TakeGas(uid, component); - ModifyComponents(uid, component); - _audio.PlayPvs(component.CloseSound, uid); - component.LastInternalOpenAttempt = default; - var afterev = new StorageAfterCloseEvent(); - RaiseLocalEvent(uid, ref afterev); - } - - public bool Insert(EntityUid toInsert, EntityUid container, EntityStorageComponent? component = null) - { - if (!Resolve(container, ref component)) - return false; - - if (component.Open) - { - Transform(toInsert).WorldPosition = Transform(container).WorldPosition; - return true; - } - - var inside = EnsureComp(toInsert); - inside.Storage = container; - return component.Contents.Insert(toInsert, EntityManager); - } - - public bool Remove(EntityUid toRemove, EntityUid container, EntityStorageComponent? component = null, TransformComponent? xform = null) - { - if (!Resolve(container, ref component, ref xform, false)) - return false; - - RemComp(toRemove); - component.Contents.Remove(toRemove, EntityManager); - Transform(toRemove).WorldPosition = xform.WorldPosition + xform.WorldRotation.RotateVec(component.EnteringOffset); - return true; - } - - public bool CanInsert(EntityUid container, EntityStorageComponent? component = null) - { - if (!Resolve(container, ref component)) - return false; - - if (component.Open) - return true; - - if (component.Contents.ContainedEntities.Count >= component.Capacity) - return false; - - return true; - } - - public bool TryOpenStorage(EntityUid user, EntityUid target, bool silent = false) - { - if (!CanOpen(user, target, silent)) - return false; - - OpenStorage(target); - return true; - } - - public bool TryCloseStorage(EntityUid target) - { - if (!CanClose(target)) - { - return false; - } - - CloseStorage(target); - return true; - } - - public bool CanOpen(EntityUid user, EntityUid target, bool silent = false, EntityStorageComponent? component = null) - { - if (!Resolve(target, ref component)) - return false; - - if (!HasComp(user)) - return false; - - if (component.IsWeldedShut) - { - if (!silent && !component.Contents.Contains(user)) - _popupSystem.PopupEntity(Loc.GetString("entity-storage-component-welded-shut-message"), target); - - return false; - } - - //Checks to see if the opening position, if offset, is inside of a wall. - if (component.EnteringOffset != (0, 0) && !HasComp(target)) //if the entering position is offset - { - var newCoords = new EntityCoordinates(target, component.EnteringOffset); - if (!_interactionSystem.InRangeUnobstructed(target, newCoords, 0, collisionMask: component.EnteringOffsetCollisionFlags)) - { - if (!silent) - _popupSystem.PopupEntity(Loc.GetString("entity-storage-component-cannot-open-no-space"), target); - return false; - } - } - - var ev = new StorageOpenAttemptEvent(silent); - RaiseLocalEvent(target, ref ev, true); - - return !ev.Cancelled; - } - - public bool CanClose(EntityUid target, bool silent = false) - { - var ev = new StorageCloseAttemptEvent(); - RaiseLocalEvent(target, ref ev, silent); - - return !ev.Cancelled; - } - - public bool AddToContents(EntityUid toAdd, EntityUid container, EntityStorageComponent? component = null) - { - if (!Resolve(container, ref component)) - return false; - - if (toAdd == container) - return false; - - if (TryComp(toAdd, out var phys)) - { - var aabb = _physics.GetWorldAABB(toAdd, body: phys); - - if (component.MaxSize < aabb.Size.X || component.MaxSize < aabb.Size.Y) - return false; - } - - return Insert(toAdd, container, component); - } - - public bool CanFit(EntityUid toInsert, EntityUid container, EntityWhitelist? whitelist) - { - // conditions are complicated because of pizzabox-related issues, so follow this guide - // 0. Accomplish your goals at all costs. - // 1. AddToContents can block anything - // 2. maximum item count can block anything - // 3. ghosts can NEVER be eaten - // 4. items can always be eaten unless a previous law prevents it - // 5. if this is NOT AN ITEM, then mobs can always be eaten unless a previous - // law prevents it - // 6. if this is an item, then mobs must only be eaten if some other component prevents - // pick-up interactions while a mob is inside (e.g. foldable) - var attemptEvent = new InsertIntoEntityStorageAttemptEvent(); - RaiseLocalEvent(toInsert, ref attemptEvent); - if (attemptEvent.Cancelled) - return false; - - var targetIsMob = HasComp(toInsert); - var storageIsItem = HasComp(container); - var allowedToEat = whitelist?.IsValid(toInsert) ?? HasComp(toInsert); - - // BEFORE REPLACING THIS WITH, I.E. A PROPERTY: - // Make absolutely 100% sure you have worked out how to stop people ending up in backpacks. - // Seriously, it is insanely hacky and weird to get someone out of a backpack once they end up in there. - // And to be clear, they should NOT be in there. - // For the record, what you need to do is empty the backpack onto a PlacableSurface (table, rack) - if (targetIsMob) - { - if (!storageIsItem) - allowedToEat = true; - else - { - var storeEv = new StoreMobInItemContainerAttemptEvent(); - RaiseLocalEvent(container, ref storeEv); - allowedToEat = storeEv.Handled && !storeEv.Cancelled; - } - } - - return allowedToEat; - } - - public void ModifyComponents(EntityUid uid, EntityStorageComponent? component = null) - { - if (!Resolve(uid, ref component)) - return; - - if (!component.IsCollidableWhenOpen && TryComp(uid, out var fixtures) && fixtures.Fixtures.Count > 0) - { - // currently only works for single-fixture entities. If they have more than one fixture, then - // RemovedMasks needs to be tracked separately for each fixture, using a fixture Id Dictionary. Also the - // fixture IDs probably cant be automatically generated without causing issues, unless there is some - // guarantee that they will get deserialized with the same auto-generated ID when saving+loading the map. - var fixture = fixtures.Fixtures.Values.First(); - - if (component.Open) - { - component.RemovedMasks = fixture.CollisionLayer & component.MasksToRemove; - _physics.SetCollisionLayer(uid, fixture, fixture.CollisionLayer & ~component.MasksToRemove, manager: fixtures); - } - else - { - _physics.SetCollisionLayer(uid, fixture, fixture.CollisionLayer | component.RemovedMasks, manager: fixtures); - component.RemovedMasks = 0; - } - } - - if (TryComp(uid, out var surface)) - _placeableSurface.SetPlaceable(uid, component.Open, surface); - - _appearance.SetData(uid, StorageVisuals.Open, component.Open); - _appearance.SetData(uid, StorageVisuals.HasContents, component.Contents.ContainedEntities.Count > 0); - } - - private void TakeGas(EntityUid uid, EntityStorageComponent component) + protected override void TakeGas(EntityUid uid, SharedEntityStorageComponent component) { if (!component.Airtight) return; - var tile = GetOffsetTileRef(uid, component); + var serverComp = (EntityStorageComponent) component; + var tile = GetOffsetTileRef(uid, serverComp); if (tile != null && _atmos.GetTileMixture(tile.Value.GridUid, null, tile.Value.GridIndices, true) is {} environment) { - _atmos.Merge(component.Air, environment.RemoveVolume(EntityStorageComponent.GasMixVolume)); + _atmos.Merge(serverComp.Air, environment.RemoveVolume(SharedEntityStorageComponent.GasMixVolume)); } } - public void ReleaseGas(EntityUid uid, EntityStorageComponent component) + public override void ReleaseGas(EntityUid uid, SharedEntityStorageComponent component) { - if (!component.Airtight) + var serverComp = (EntityStorageComponent) component; + + if (!serverComp.Airtight) return; - var tile = GetOffsetTileRef(uid, component); + var tile = GetOffsetTileRef(uid, serverComp); if (tile != null && _atmos.GetTileMixture(tile.Value.GridUid, null, tile.Value.GridIndices, true) is {} environment) { - _atmos.Merge(environment, component.Air); - component.Air.Clear(); + _atmos.Merge(environment, serverComp.Air); + serverComp.Air.Clear(); } } diff --git a/Content.Server/Storage/EntitySystems/StorageSystem.cs b/Content.Server/Storage/EntitySystems/StorageSystem.cs index 42ebca5515..91510e339a 100644 --- a/Content.Server/Storage/EntitySystems/StorageSystem.cs +++ b/Content.Server/Storage/EntitySystems/StorageSystem.cs @@ -77,9 +77,6 @@ namespace Content.Server.Storage.EntitySystems SubscribeLocalEvent>(OnDoAfter); - SubscribeLocalEvent>(AddToggleOpenVerb); - SubscribeLocalEvent(OnRelayMovement); - SubscribeLocalEvent(OnStorageFillMapInit); } @@ -95,41 +92,6 @@ namespace Content.Server.Storage.EntitySystems UpdateStorageUI(uid, storageComp); } - private void OnRelayMovement(EntityUid uid, EntityStorageComponent component, ref ContainerRelayMovementEntityEvent args) - { - if (!EntityManager.HasComponent(args.Entity) || _gameTiming.CurTime < component.LastInternalOpenAttempt + EntityStorageComponent.InternalOpenAttemptDelay) - return; - - component.LastInternalOpenAttempt = _gameTiming.CurTime; - if (component.OpenOnMove) - { - _entityStorage.TryOpenStorage(args.Entity, component.Owner); - } - } - - - private void AddToggleOpenVerb(EntityUid uid, EntityStorageComponent component, GetVerbsEvent args) - { - if (!args.CanAccess || !args.CanInteract || !_entityStorage.CanOpen(args.User, args.Target, silent: true, component)) - return; - - InteractionVerb verb = new(); - if (component.Open) - { - verb.Text = Loc.GetString("verb-common-close"); - verb.Icon = new SpriteSpecifier.Texture( - new ResourcePath("/Textures/Interface/VerbIcons/close.svg.192dpi.png")); - } - else - { - verb.Text = Loc.GetString("verb-common-open"); - verb.Icon = new SpriteSpecifier.Texture( - new ResourcePath("/Textures/Interface/VerbIcons/open.svg.192dpi.png")); - } - verb.Act = () => _entityStorage.ToggleOpen(args.User, args.Target, component); - args.Verbs.Add(verb); - } - private void AddOpenUiVerb(EntityUid uid, ServerStorageComponent component, GetVerbsEvent args) { if (!args.CanAccess || !args.CanInteract || TryComp(uid, out var lockComponent) && lockComponent.Locked) diff --git a/Content.Shared/Lock/LockSystem.cs b/Content.Shared/Lock/LockSystem.cs index 4c27252db8..5c01ae0702 100644 --- a/Content.Shared/Lock/LockSystem.cs +++ b/Content.Shared/Lock/LockSystem.cs @@ -86,7 +86,7 @@ public sealed class LockSystem : EntitySystem { if (!component.Locked) return; - if (!args.Silent) + if (!args.Silent && _net.IsServer) _sharedPopupSystem.PopupEntity(Loc.GetString("entity-storage-component-locked-message"), uid); args.Cancelled = true; @@ -118,12 +118,12 @@ public sealed class LockSystem : EntitySystem if (!HasUserAccess(uid, user, quiet: false)) return false; - if (_net.IsClient && _timing.IsFirstTimePredicted) + if (_net.IsServer) { _sharedPopupSystem.PopupEntity(Loc.GetString("lock-comp-do-lock-success", ("entityName", Identity.Name(uid, EntityManager))), uid, user); - _audio.PlayPvs(_audio.GetSound(lockComp.LockSound), uid, AudioParams.Default.WithVolume(-5)); } + _audio.PlayPredicted(lockComp.LockSound, uid, user, AudioParams.Default.WithVolume(-5)); lockComp.Locked = true; _appearanceSystem.SetData(uid, StorageVisuals.Locked, true); @@ -145,15 +145,15 @@ public sealed class LockSystem : EntitySystem if (!Resolve(uid, ref lockComp)) return; - if (_net.IsClient && _timing.IsFirstTimePredicted) + if (_net.IsServer) { if (user is { Valid: true }) { _sharedPopupSystem.PopupEntity(Loc.GetString("lock-comp-do-unlock-success", ("entityName", Identity.Name(uid, EntityManager))), uid, user.Value); } - _audio.PlayPvs(_audio.GetSound(lockComp.UnlockSound), uid, AudioParams.Default.WithVolume(-5)); } + _audio.PlayPredicted(lockComp.UnlockSound, uid, user, AudioParams.Default.WithVolume(-5)); lockComp.Locked = false; _appearanceSystem.SetData(uid, StorageVisuals.Locked, false); @@ -236,10 +236,7 @@ public sealed class LockSystem : EntitySystem { if (!component.Locked) return; - if (_net.IsClient && _timing.IsFirstTimePredicted) - { - _audio.PlayPvs(_audio.GetSound(component.UnlockSound), uid, AudioParams.Default.WithVolume(-5)); - } + _audio.PlayPredicted(component.UnlockSound, uid, null, AudioParams.Default.WithVolume(-5)); _appearanceSystem.SetData(uid, StorageVisuals.Locked, false); RemComp(uid); //Literally destroys the lock as a tell it was emagged args.Handled = true; diff --git a/Content.Shared/Placeable/PlaceableSurfaceSystem.cs b/Content.Shared/Placeable/PlaceableSurfaceSystem.cs index 8ea0940763..1f5c64d37f 100644 --- a/Content.Shared/Placeable/PlaceableSurfaceSystem.cs +++ b/Content.Shared/Placeable/PlaceableSurfaceSystem.cs @@ -25,7 +25,7 @@ namespace Content.Shared.Placeable public void SetPlaceable(EntityUid uid, bool isPlaceable, PlaceableSurfaceComponent? surface = null) { - if (!Resolve(uid, ref surface)) + if (!Resolve(uid, ref surface, false)) return; surface.IsPlaceable = isPlaceable; diff --git a/Content.Server/Storage/Components/InsideEntityStorageComponent.cs b/Content.Shared/Storage/Components/InsideEntityStorageComponent.cs similarity index 82% rename from Content.Server/Storage/Components/InsideEntityStorageComponent.cs rename to Content.Shared/Storage/Components/InsideEntityStorageComponent.cs index 4a4ef0f016..802cf195c3 100644 --- a/Content.Server/Storage/Components/InsideEntityStorageComponent.cs +++ b/Content.Shared/Storage/Components/InsideEntityStorageComponent.cs @@ -1,4 +1,4 @@ -namespace Content.Server.Storage.Components; +namespace Content.Shared.Storage.Components; /// /// Added to entities contained within entity storage, for directed event purposes. diff --git a/Content.Shared/Storage/Components/SharedEntityStorage.cs b/Content.Shared/Storage/Components/SharedEntityStorage.cs deleted file mode 100644 index a3399e2926..0000000000 --- a/Content.Shared/Storage/Components/SharedEntityStorage.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace Content.Shared.Storage.Components; - -[ByRefEvent] -public record struct InsertIntoEntityStorageAttemptEvent(bool Cancelled = false); - -[ByRefEvent] -public record struct StoreMobInItemContainerAttemptEvent(bool Handled, bool Cancelled = false); - -[ByRefEvent] -public record struct StorageOpenAttemptEvent(bool Silent, bool Cancelled = false); - -[ByRefEvent] -public readonly record struct StorageBeforeOpenEvent; - -[ByRefEvent] -public readonly record struct StorageAfterOpenEvent; - -[ByRefEvent] -public record struct StorageCloseAttemptEvent(bool Cancelled = false); - -[ByRefEvent] -public readonly record struct StorageBeforeCloseEvent(HashSet Contents, HashSet BypassChecks); - -[ByRefEvent] -public readonly record struct StorageAfterCloseEvent; diff --git a/Content.Shared/Storage/Components/SharedEntityStorageComponent.cs b/Content.Shared/Storage/Components/SharedEntityStorageComponent.cs new file mode 100644 index 0000000000..bbabde8e7d --- /dev/null +++ b/Content.Shared/Storage/Components/SharedEntityStorageComponent.cs @@ -0,0 +1,178 @@ +using Content.Shared.Physics; +using Content.Shared.Whitelist; +using Robust.Shared.Audio; +using Robust.Shared.Containers; +using Robust.Shared.GameStates; +using Robust.Shared.Serialization; + +namespace Content.Shared.Storage.Components; + +[NetworkedComponent] +public abstract class SharedEntityStorageComponent : Component +{ + public readonly float MaxSize = 1.0f; // maximum width or height of an entity allowed inside the storage. + public const float GasMixVolume = 70f; + + public static readonly TimeSpan InternalOpenAttemptDelay = TimeSpan.FromSeconds(0.5); + public TimeSpan LastInternalOpenAttempt; + + /// + /// Collision masks that get removed when the storage gets opened. + /// + public readonly int MasksToRemove = (int) ( + CollisionGroup.MidImpassable | + CollisionGroup.HighImpassable | + CollisionGroup.LowImpassable); + + /// + /// Collision masks that were removed from ANY layer when the storage was opened; + /// + [DataField("removedMasks")] + public int RemovedMasks; + + /// + /// The total amount of items that can fit in one entitystorage + /// + [DataField("capacity")] + public int Capacity = 30; + + /// + /// Whether or not the entity still has collision when open + /// + [DataField("isCollidableWhenOpen")] + public bool IsCollidableWhenOpen; + + /// + /// If true, it opens the storage when the entity inside of it moves + /// If false, it prevents the storage from opening when the entity inside of it moves. + /// This is for objects that you want the player to move while inside, like large cardboard boxes, without opening the storage. + /// + [ViewVariables(VVAccess.ReadWrite)] + [DataField("openOnMove")] + public bool OpenOnMove = true; + + //The offset for where items are emptied/vacuumed for the EntityStorage. + [DataField("enteringOffset")] + public Vector2 EnteringOffset = new(0, 0); + + //The collision groups checked, so that items are depositied or grabbed from inside walls. + [DataField("enteringOffsetCollisionFlags")] + public readonly CollisionGroup EnteringOffsetCollisionFlags = CollisionGroup.Impassable | CollisionGroup.MidImpassable; + + /// + /// How close you have to be to the "entering" spot to be able to enter + /// + [DataField("enteringRange")] + public float EnteringRange = 0.18f; + + /// + /// Whether or not to show the contents when the storage is closed + /// + [DataField("showContents")] + public bool ShowContents; + + /// + /// Whether or not light is occluded by the storage + /// + [DataField("occludesLight")] + public bool OccludesLight = true; + + /// + /// Whether or not all the contents stored should be deleted with the entitystorage + /// + [DataField("deleteContentsOnDestruction"), ViewVariables(VVAccess.ReadWrite)] + public bool DeleteContentsOnDestruction; + + /// + /// Whether or not the container is sealed and traps air inside of it + /// + [DataField("airtight"), ViewVariables(VVAccess.ReadWrite)] + public bool Airtight = true; + + /// + /// Whether or not the entitystorage is open or closed + /// + [DataField("open")] + public bool Open; + + /// + /// The sound made when closed + /// + [DataField("closeSound")] + public SoundSpecifier CloseSound = new SoundPathSpecifier("/Audio/Effects/closetclose.ogg"); + + /// + /// The sound made when open + /// + [DataField("openSound")] + public SoundSpecifier OpenSound = new SoundPathSpecifier("/Audio/Effects/closetopen.ogg"); + + /// + /// Whitelist for what entities are allowed to be inserted into this container. If this is not null, the + /// standard requirement that the entity must be an item or mob is waived. + /// + [DataField("whitelist")] + public EntityWhitelist? Whitelist; + + /// + /// The contents of the storage + /// + [ViewVariables] + public Container Contents = default!; + + /// + /// Whether or not the storage has been welded shut + /// + [DataField("isWeldedShut"), ViewVariables(VVAccess.ReadWrite)] + public bool IsWeldedShut; +} + +[Serializable, NetSerializable] +public sealed class EntityStorageComponentState : ComponentState +{ + public bool Open; + + public int Capacity; + + public bool IsCollidableWhenOpen; + + public bool OpenOnMove; + + public float EnteringRange; + + public bool IsWeldedShut; + + public EntityStorageComponentState(bool open, int capacity, bool isCollidableWhenOpen, bool openOnMove, float enteringRange, bool isWeldedShut) + { + Open = open; + Capacity = capacity; + IsCollidableWhenOpen = isCollidableWhenOpen; + OpenOnMove = openOnMove; + EnteringRange = enteringRange; + IsWeldedShut = isWeldedShut; + } +} + +[ByRefEvent] +public record struct InsertIntoEntityStorageAttemptEvent(bool Cancelled = false); + +[ByRefEvent] +public record struct StoreMobInItemContainerAttemptEvent(bool Handled, bool Cancelled = false); + +[ByRefEvent] +public record struct StorageOpenAttemptEvent(bool Silent, bool Cancelled = false); + +[ByRefEvent] +public readonly record struct StorageBeforeOpenEvent; + +[ByRefEvent] +public readonly record struct StorageAfterOpenEvent; + +[ByRefEvent] +public record struct StorageCloseAttemptEvent(bool Cancelled = false); + +[ByRefEvent] +public readonly record struct StorageBeforeCloseEvent(HashSet Contents, HashSet BypassChecks); + +[ByRefEvent] +public readonly record struct StorageAfterCloseEvent; diff --git a/Content.Shared/Storage/EntitySystems/SharedEntityStorageSystem.cs b/Content.Shared/Storage/EntitySystems/SharedEntityStorageSystem.cs new file mode 100644 index 0000000000..cc2a627141 --- /dev/null +++ b/Content.Shared/Storage/EntitySystems/SharedEntityStorageSystem.cs @@ -0,0 +1,455 @@ +using System.Linq; +using Content.Shared.Body.Components; +using Content.Shared.Destructible; +using Content.Shared.Hands.Components; +using Content.Shared.Interaction; +using Content.Shared.Item; +using Content.Shared.Lock; +using Content.Shared.Movement.Events; +using Content.Shared.Placeable; +using Content.Shared.Popups; +using Content.Shared.Storage.Components; +using Content.Shared.Verbs; +using Content.Shared.Wall; +using Content.Shared.Whitelist; +using Robust.Shared.Containers; +using Robust.Shared.GameStates; +using Robust.Shared.Map; +using Robust.Shared.Network; +using Robust.Shared.Physics; +using Robust.Shared.Physics.Components; +using Robust.Shared.Physics.Systems; +using Robust.Shared.Timing; +using Robust.Shared.Utility; + +namespace Content.Shared.Storage.EntitySystems; + +public abstract class SharedEntityStorageSystem : EntitySystem +{ + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly INetManager _net = default!; + [Dependency] private readonly EntityLookupSystem _lookup = default!; + [Dependency] private readonly PlaceableSurfaceSystem _placeableSurface = default!; + [Dependency] private readonly SharedAppearanceSystem _appearance = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedContainerSystem _container = default!; + [Dependency] private readonly SharedInteractionSystem _interaction = default!; + [Dependency] private readonly SharedPhysicsSystem _physics = default!; + [Dependency] protected readonly SharedPopupSystem Popup = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; + + public const string ContainerName = "entity_storage"; + + /// + public override void Initialize() + { + SubscribeLocalEvent(OnInit); + SubscribeLocalEvent(OnInteract, after: new[]{typeof(LockSystem)}); + SubscribeLocalEvent(OnLockToggleAttempt); + SubscribeLocalEvent(OnDestruction); + SubscribeLocalEvent>(AddToggleOpenVerb); + SubscribeLocalEvent(OnRelayMovement); + + SubscribeLocalEvent(OnGetState); + SubscribeLocalEvent(OnHandleState); + } + + private void OnGetState(EntityUid uid, SharedEntityStorageComponent component, ref ComponentGetState args) + { + args.State = new EntityStorageComponentState(component.Open, + component.Capacity, + component.IsCollidableWhenOpen, + component.OpenOnMove, + component.EnteringRange, + component.IsWeldedShut); + } + + private void OnHandleState(EntityUid uid, SharedEntityStorageComponent component, ref ComponentHandleState args) + { + if (args.Current is not EntityStorageComponentState state) + return; + component.Open = state.Open; + component.Capacity = state.Capacity; + component.IsCollidableWhenOpen = state.IsCollidableWhenOpen; + component.OpenOnMove = state.OpenOnMove; + component.EnteringRange = state.EnteringRange; + component.IsWeldedShut = state.IsWeldedShut; + } + + protected virtual void OnInit(EntityUid uid, SharedEntityStorageComponent component, ComponentInit args) + { + component.Contents = _container.EnsureContainer(uid, ContainerName); + component.Contents.ShowContents = component.ShowContents; + component.Contents.OccludesLight = component.OccludesLight; + } + + private void OnInteract(EntityUid uid, SharedEntityStorageComponent component, ActivateInWorldEvent args) + { + if (args.Handled) + return; + + args.Handled = true; + ToggleOpen(args.User, uid, component); + } + + private void OnLockToggleAttempt(EntityUid uid, SharedEntityStorageComponent target, ref LockToggleAttemptEvent args) + { + // Cannot (un)lock open lockers. + if (target.Open) + args.Cancelled = true; + + // Cannot (un)lock from the inside. Maybe a bad idea? Security jocks could trap nerds in lockers? + if (target.Contents.Contains(args.User)) + args.Cancelled = true; + } + + private void OnDestruction(EntityUid uid, SharedEntityStorageComponent component, DestructionEventArgs args) + { + component.Open = true; + Dirty(component); + if (!component.DeleteContentsOnDestruction) + { + EmptyContents(uid, component); + return; + } + + foreach (var ent in new List(component.Contents.ContainedEntities)) + { + Del(ent); + } + } + + private void OnRelayMovement(EntityUid uid, SharedEntityStorageComponent component, ref ContainerRelayMovementEntityEvent args) + { + if (!HasComp(args.Entity)) + return; + + if (_timing.CurTime < component.LastInternalOpenAttempt + SharedEntityStorageComponent.InternalOpenAttemptDelay) + return; + + component.LastInternalOpenAttempt = _timing.CurTime; + if (component.OpenOnMove) + { + TryOpenStorage(args.Entity, uid); + } + } + + private void AddToggleOpenVerb(EntityUid uid, SharedEntityStorageComponent component, GetVerbsEvent args) + { + if (!args.CanAccess || !args.CanInteract) + return; + + if (!CanOpen(args.User, args.Target, silent: true, component)) + return; + + InteractionVerb verb = new(); + if (component.Open) + { + verb.Text = Loc.GetString("verb-common-close"); + verb.Icon = new SpriteSpecifier.Texture(new ResourcePath("/Textures/Interface/VerbIcons/close.svg.192dpi.png")); + } + else + { + verb.Text = Loc.GetString("verb-common-open"); + verb.Icon = new SpriteSpecifier.Texture( + new ResourcePath("/Textures/Interface/VerbIcons/open.svg.192dpi.png")); + } + verb.Act = () => ToggleOpen(args.User, args.Target, component); + args.Verbs.Add(verb); + } + + + public void ToggleOpen(EntityUid user, EntityUid target, SharedEntityStorageComponent? component = null) + { + if (!Resolve(target, ref component)) + return; + + if (component.Open) + { + TryCloseStorage(target); + } + else + { + TryOpenStorage(user, target); + } + } + + public void EmptyContents(EntityUid uid, SharedEntityStorageComponent? component = null) + { + if (!Resolve(uid, ref component)) + return; + + var uidXform = Transform(uid); + var containedArr = component.Contents.ContainedEntities.ToArray(); + foreach (var contained in containedArr) + { + Remove(contained, uid, component, uidXform); + } + } + + public void OpenStorage(EntityUid uid, SharedEntityStorageComponent? component = null) + { + if (!Resolve(uid, ref component)) + return; + + var beforeev = new StorageBeforeOpenEvent(); + RaiseLocalEvent(uid, ref beforeev); + component.Open = true; + Dirty(component); + EmptyContents(uid, component); + ModifyComponents(uid, component); + if (_net.IsClient && _timing.IsFirstTimePredicted) + _audio.PlayPvs(component.OpenSound, uid); + ReleaseGas(uid, component); + var afterev = new StorageAfterOpenEvent(); + RaiseLocalEvent(uid, ref afterev); + } + + public void CloseStorage(EntityUid uid, SharedEntityStorageComponent? component = null) + { + if (!Resolve(uid, ref component)) + return; + component.Open = false; + Dirty(component); + + var targetCoordinates = new EntityCoordinates(uid, component.EnteringOffset); + + var entities = _lookup.GetEntitiesInRange(targetCoordinates, component.EnteringRange, LookupFlags.Approximate | LookupFlags.Dynamic | LookupFlags.Sundries); + + var ev = new StorageBeforeCloseEvent(entities, new()); + RaiseLocalEvent(uid, ref ev); + var count = 0; + foreach (var entity in ev.Contents) + { + if (!ev.BypassChecks.Contains(entity)) + { + if (!CanFit(entity, uid, component.Whitelist)) + continue; + } + + if (!AddToContents(entity, uid, component)) + continue; + + count++; + if (count >= component.Capacity) + break; + } + + TakeGas(uid, component); + ModifyComponents(uid, component); + if (_net.IsClient && _timing.IsFirstTimePredicted) + _audio.PlayPvs(component.CloseSound, uid); + component.LastInternalOpenAttempt = default; + var afterev = new StorageAfterCloseEvent(); + RaiseLocalEvent(uid, ref afterev); + } + + public bool Insert(EntityUid toInsert, EntityUid container, SharedEntityStorageComponent? component = null) + { + if (!Resolve(container, ref component)) + return false; + + if (component.Open) + { + _transform.SetWorldPosition(toInsert, _transform.GetWorldPosition(container)); + return true; + } + + var inside = EnsureComp(toInsert); + inside.Storage = container; + return component.Contents.Insert(toInsert, EntityManager); + } + + public bool Remove(EntityUid toRemove, EntityUid container, SharedEntityStorageComponent? component = null, TransformComponent? xform = null) + { + if (!Resolve(container, ref component, ref xform, false)) + return false; + + RemComp(toRemove); + component.Contents.Remove(toRemove, EntityManager); + var pos = _transform.GetWorldPosition(xform) + _transform.GetWorldRotation(xform).RotateVec(component.EnteringOffset); + _transform.SetWorldPosition(toRemove, pos); + return true; + } + + public bool CanInsert(EntityUid container, SharedEntityStorageComponent? component = null) + { + if (!Resolve(container, ref component)) + return false; + + if (component.Open) + return true; + + if (component.Contents.ContainedEntities.Count >= component.Capacity) + return false; + + return true; + } + + public bool TryOpenStorage(EntityUid user, EntityUid target, bool silent = false) + { + if (!CanOpen(user, target, silent)) + return false; + + OpenStorage(target); + return true; + } + + public bool TryCloseStorage(EntityUid target) + { + if (!CanClose(target)) + { + return false; + } + + CloseStorage(target); + return true; + } + + public bool CanOpen(EntityUid user, EntityUid target, bool silent = false, SharedEntityStorageComponent? component = null) + { + if (!Resolve(target, ref component)) + return false; + + if (!HasComp(user)) + return false; + + if (component.IsWeldedShut) + { + if (!silent && !component.Contents.Contains(user) && _net.IsServer) + Popup.PopupEntity(Loc.GetString("entity-storage-component-welded-shut-message"), target); + + return false; + } + + //Checks to see if the opening position, if offset, is inside of a wall. + if (component.EnteringOffset != (0, 0) && !HasComp(target)) //if the entering position is offset + { + var newCoords = new EntityCoordinates(target, component.EnteringOffset); + if (!_interaction.InRangeUnobstructed(target, newCoords, 0, collisionMask: component.EnteringOffsetCollisionFlags)) + { + if (!silent && _net.IsServer) + Popup.PopupEntity(Loc.GetString("entity-storage-component-cannot-open-no-space"), target); + return false; + } + } + + var ev = new StorageOpenAttemptEvent(silent); + RaiseLocalEvent(target, ref ev, true); + + return !ev.Cancelled; + } + + public bool CanClose(EntityUid target, bool silent = false) + { + var ev = new StorageCloseAttemptEvent(); + RaiseLocalEvent(target, ref ev, silent); + + return !ev.Cancelled; + } + + public bool AddToContents(EntityUid toAdd, EntityUid container, SharedEntityStorageComponent? component = null) + { + if (!Resolve(container, ref component)) + return false; + + if (toAdd == container) + return false; + + if (TryComp(toAdd, out var phys)) + { + var aabb = _physics.GetWorldAABB(toAdd, body: phys); + + if (component.MaxSize < aabb.Size.X || component.MaxSize < aabb.Size.Y) + return false; + } + + return Insert(toAdd, container, component); + } + + public bool CanFit(EntityUid toInsert, EntityUid container, EntityWhitelist? whitelist) + { + // conditions are complicated because of pizzabox-related issues, so follow this guide + // 0. Accomplish your goals at all costs. + // 1. AddToContents can block anything + // 2. maximum item count can block anything + // 3. ghosts can NEVER be eaten + // 4. items can always be eaten unless a previous law prevents it + // 5. if this is NOT AN ITEM, then mobs can always be eaten unless a previous + // law prevents it + // 6. if this is an item, then mobs must only be eaten if some other component prevents + // pick-up interactions while a mob is inside (e.g. foldable) + var attemptEvent = new InsertIntoEntityStorageAttemptEvent(); + RaiseLocalEvent(toInsert, ref attemptEvent); + if (attemptEvent.Cancelled) + return false; + + var targetIsMob = HasComp(toInsert); + var storageIsItem = HasComp(container); + var allowedToEat = whitelist?.IsValid(toInsert) ?? HasComp(toInsert); + + // BEFORE REPLACING THIS WITH, I.E. A PROPERTY: + // Make absolutely 100% sure you have worked out how to stop people ending up in backpacks. + // Seriously, it is insanely hacky and weird to get someone out of a backpack once they end up in there. + // And to be clear, they should NOT be in there. + // For the record, what you need to do is empty the backpack onto a PlacableSurface (table, rack) + if (targetIsMob) + { + if (!storageIsItem) + allowedToEat = true; + else + { + var storeEv = new StoreMobInItemContainerAttemptEvent(); + RaiseLocalEvent(container, ref storeEv); + allowedToEat = storeEv is { Handled: true, Cancelled: false }; + } + } + + return allowedToEat; + } + + private void ModifyComponents(EntityUid uid, SharedEntityStorageComponent? component = null) + { + if (!Resolve(uid, ref component)) + return; + + if (!component.IsCollidableWhenOpen && TryComp(uid, out var fixtures) && + fixtures.Fixtures.Count > 0) + { + // currently only works for single-fixture entities. If they have more than one fixture, then + // RemovedMasks needs to be tracked separately for each fixture, using a fixture Id Dictionary. Also the + // fixture IDs probably cant be automatically generated without causing issues, unless there is some + // guarantee that they will get deserialized with the same auto-generated ID when saving+loading the map. + var fixture = fixtures.Fixtures.Values.First(); + + if (component.Open) + { + component.RemovedMasks = fixture.CollisionLayer & component.MasksToRemove; + _physics.SetCollisionLayer(uid, fixture, fixture.CollisionLayer & ~component.MasksToRemove, + manager: fixtures); + } + else + { + _physics.SetCollisionLayer(uid, fixture, fixture.CollisionLayer | component.RemovedMasks, + manager: fixtures); + component.RemovedMasks = 0; + } + } + + if (TryComp(uid, out var surface)) + _placeableSurface.SetPlaceable(uid, component.Open, surface); + + _appearance.SetData(uid, StorageVisuals.Open, component.Open); + _appearance.SetData(uid, StorageVisuals.HasContents, component.Contents.ContainedEntities.Count > 0); + } + + protected virtual void TakeGas(EntityUid uid, SharedEntityStorageComponent component) + { + + } + + public virtual void ReleaseGas(EntityUid uid, SharedEntityStorageComponent component) + { + + } +}