--- /dev/null
+using Content.Shared.Storage.Components;
+using Robust.Shared.GameStates;
+
+namespace Content.Client.Storage.Components;
+
+[RegisterComponent, ComponentReference(typeof(SharedEntityStorageComponent))]
+public sealed class EntityStorageComponent : SharedEntityStorageComponent
+{
+
+}
--- /dev/null
+using Content.Shared.Storage.EntitySystems;
+
+namespace Content.Client.Storage.Systems;
+
+public sealed class EntityStorageSystem : SharedEntityStorageSystem
+{
+
+}
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;
-
- /// <summary>
- /// Collision masks that get removed when the storage gets opened.
- /// </summary>
- public readonly int MasksToRemove = (int) (
- CollisionGroup.MidImpassable |
- CollisionGroup.HighImpassable |
- CollisionGroup.LowImpassable);
-
- /// <summary>
- /// Collision masks that were removed from ANY layer when the storage was opened;
- /// </summary>
- [DataField("removedMasks")]
- public int RemovedMasks;
-
- [DataField("capacity")]
- public int Capacity = 30;
-
- [DataField("isCollidableWhenOpen")]
- public bool IsCollidableWhenOpen;
-
- /// <summary>
- /// 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.
- /// </summary>
- [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;
-
- /// <summary>
- /// Whether or not the container is sealed and traps air inside of it
- /// </summary>
- [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");
-
- /// <summary>
- /// 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.
- /// </summary>
- [DataField("whitelist")]
- public EntityWhitelist? Whitelist;
-
- [ViewVariables]
- public Container Contents = default!;
-
- [ViewVariables(VVAccess.ReadWrite)]
- public bool IsWeldedShut;
-
/// <summary>
/// Gas currently contained in this entity storage.
/// None while open. Grabs gas from the atmosphere when closed, and exposes any entities inside to it.
-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<EntityStorageComponent, ComponentInit>(OnInit);
- SubscribeLocalEvent<EntityStorageComponent, ActivateInWorldEvent>(OnInteract);
SubscribeLocalEvent<EntityStorageComponent, WeldableAttemptEvent>(OnWeldableAttempt);
SubscribeLocalEvent<EntityStorageComponent, WeldableChangedEvent>(OnWelded);
- SubscribeLocalEvent<EntityStorageComponent, LockToggleAttemptEvent>(OnLockToggleAttempt);
- SubscribeLocalEvent<EntityStorageComponent, DestructionEventArgs>(OnDestruction);
- SubscribeLocalEvent<InsideEntityStorageComponent, EntGotRemovedFromContainerMessage>(OnRemoved);
SubscribeLocalEvent<InsideEntityStorageComponent, InhaleLocationEvent>(OnInsideInhale);
SubscribeLocalEvent<InsideEntityStorageComponent, ExhaleLocationEvent>(OnInsideExhale);
SubscribeLocalEvent<InsideEntityStorageComponent, AtmosExposedGetAirEvent>(OnInsideExposed);
+ SubscribeLocalEvent<InsideEntityStorageComponent, EntGotRemovedFromContainerMessage>(OnRemoved);
}
- private void OnInit(EntityUid uid, EntityStorageComponent component, ComponentInit args)
+ protected override void OnInit(EntityUid uid, SharedEntityStorageComponent component, ComponentInit args)
{
- component.Contents = _container.EnsureContainer<Container>(uid, ContainerName);
- component.Contents.ShowContents = component.ShowContents;
- component.Contents.OccludesLight = component.OccludesLight;
+ base.OnInit(uid, component, args);
if (TryComp<ConstructionComponent>(uid, out var construction))
_construction.AddContainer(uid, ContainerName, construction);
- if (TryComp<PlaceableSurfaceComponent>(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)
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();
}
}
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<EntityUid>(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<InsideEntityStorageComponent>(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<InsideEntityStorageComponent>(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<SharedHandsComponent>(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<WallMountComponent>(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<PhysicsComponent>(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<BodyComponent>(toInsert);
- var storageIsItem = HasComp<ItemComponent>(container);
- var allowedToEat = whitelist?.IsValid(toInsert) ?? HasComp<ItemComponent>(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<FixturesComponent>(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<PlaceableSurfaceComponent>(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();
}
}
SubscribeLocalEvent<ServerStorageComponent, DoAfterEvent<StorageData>>(OnDoAfter);
- SubscribeLocalEvent<EntityStorageComponent, GetVerbsEvent<InteractionVerb>>(AddToggleOpenVerb);
- SubscribeLocalEvent<EntityStorageComponent, ContainerRelayMovementEntityEvent>(OnRelayMovement);
-
SubscribeLocalEvent<StorageFillComponent, MapInitEvent>(OnStorageFillMapInit);
}
UpdateStorageUI(uid, storageComp);
}
- private void OnRelayMovement(EntityUid uid, EntityStorageComponent component, ref ContainerRelayMovementEntityEvent args)
- {
- if (!EntityManager.HasComponent<HandsComponent>(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<InteractionVerb> 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<ActivationVerb> args)
{
if (!args.CanAccess || !args.CanInteract || TryComp<LockComponent>(uid, out var lockComponent) && lockComponent.Locked)
{
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;
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);
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);
{
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<LockComponent>(uid); //Literally destroys the lock as a tell it was emagged
args.Handled = true;
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;
-namespace Content.Server.Storage.Components;
+namespace Content.Shared.Storage.Components;
/// <summary>
/// Added to entities contained within entity storage, for directed event purposes.
+++ /dev/null
-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<EntityUid> Contents, HashSet<EntityUid> BypassChecks);
-
-[ByRefEvent]
-public readonly record struct StorageAfterCloseEvent;
--- /dev/null
+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;
+
+ /// <summary>
+ /// Collision masks that get removed when the storage gets opened.
+ /// </summary>
+ public readonly int MasksToRemove = (int) (
+ CollisionGroup.MidImpassable |
+ CollisionGroup.HighImpassable |
+ CollisionGroup.LowImpassable);
+
+ /// <summary>
+ /// Collision masks that were removed from ANY layer when the storage was opened;
+ /// </summary>
+ [DataField("removedMasks")]
+ public int RemovedMasks;
+
+ /// <summary>
+ /// The total amount of items that can fit in one entitystorage
+ /// </summary>
+ [DataField("capacity")]
+ public int Capacity = 30;
+
+ /// <summary>
+ /// Whether or not the entity still has collision when open
+ /// </summary>
+ [DataField("isCollidableWhenOpen")]
+ public bool IsCollidableWhenOpen;
+
+ /// <summary>
+ /// 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.
+ /// </summary>
+ [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;
+
+ /// <summary>
+ /// How close you have to be to the "entering" spot to be able to enter
+ /// </summary>
+ [DataField("enteringRange")]
+ public float EnteringRange = 0.18f;
+
+ /// <summary>
+ /// Whether or not to show the contents when the storage is closed
+ /// </summary>
+ [DataField("showContents")]
+ public bool ShowContents;
+
+ /// <summary>
+ /// Whether or not light is occluded by the storage
+ /// </summary>
+ [DataField("occludesLight")]
+ public bool OccludesLight = true;
+
+ /// <summary>
+ /// Whether or not all the contents stored should be deleted with the entitystorage
+ /// </summary>
+ [DataField("deleteContentsOnDestruction"), ViewVariables(VVAccess.ReadWrite)]
+ public bool DeleteContentsOnDestruction;
+
+ /// <summary>
+ /// Whether or not the container is sealed and traps air inside of it
+ /// </summary>
+ [DataField("airtight"), ViewVariables(VVAccess.ReadWrite)]
+ public bool Airtight = true;
+
+ /// <summary>
+ /// Whether or not the entitystorage is open or closed
+ /// </summary>
+ [DataField("open")]
+ public bool Open;
+
+ /// <summary>
+ /// The sound made when closed
+ /// </summary>
+ [DataField("closeSound")]
+ public SoundSpecifier CloseSound = new SoundPathSpecifier("/Audio/Effects/closetclose.ogg");
+
+ /// <summary>
+ /// The sound made when open
+ /// </summary>
+ [DataField("openSound")]
+ public SoundSpecifier OpenSound = new SoundPathSpecifier("/Audio/Effects/closetopen.ogg");
+
+ /// <summary>
+ /// 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.
+ /// </summary>
+ [DataField("whitelist")]
+ public EntityWhitelist? Whitelist;
+
+ /// <summary>
+ /// The contents of the storage
+ /// </summary>
+ [ViewVariables]
+ public Container Contents = default!;
+
+ /// <summary>
+ /// Whether or not the storage has been welded shut
+ /// </summary>
+ [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<EntityUid> Contents, HashSet<EntityUid> BypassChecks);
+
+[ByRefEvent]
+public readonly record struct StorageAfterCloseEvent;
--- /dev/null
+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";
+
+ /// <inheritdoc/>
+ public override void Initialize()
+ {
+ SubscribeLocalEvent<SharedEntityStorageComponent, ComponentInit>(OnInit);
+ SubscribeLocalEvent<SharedEntityStorageComponent, ActivateInWorldEvent>(OnInteract, after: new[]{typeof(LockSystem)});
+ SubscribeLocalEvent<SharedEntityStorageComponent, LockToggleAttemptEvent>(OnLockToggleAttempt);
+ SubscribeLocalEvent<SharedEntityStorageComponent, DestructionEventArgs>(OnDestruction);
+ SubscribeLocalEvent<SharedEntityStorageComponent, GetVerbsEvent<InteractionVerb>>(AddToggleOpenVerb);
+ SubscribeLocalEvent<SharedEntityStorageComponent, ContainerRelayMovementEntityEvent>(OnRelayMovement);
+
+ SubscribeLocalEvent<SharedEntityStorageComponent, ComponentGetState>(OnGetState);
+ SubscribeLocalEvent<SharedEntityStorageComponent, ComponentHandleState>(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<Container>(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<EntityUid>(component.Contents.ContainedEntities))
+ {
+ Del(ent);
+ }
+ }
+
+ private void OnRelayMovement(EntityUid uid, SharedEntityStorageComponent component, ref ContainerRelayMovementEntityEvent args)
+ {
+ if (!HasComp<SharedHandsComponent>(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<InteractionVerb> 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<InsideEntityStorageComponent>(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<InsideEntityStorageComponent>(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<SharedHandsComponent>(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<WallMountComponent>(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<PhysicsComponent>(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<BodyComponent>(toInsert);
+ var storageIsItem = HasComp<ItemComponent>(container);
+ var allowedToEat = whitelist?.IsValid(toInsert) ?? HasComp<ItemComponent>(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<FixturesComponent>(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<PlaceableSurfaceComponent>(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)
+ {
+
+ }
+}