2023-12-27 17:50:49 -05:00
using System.Collections.Frozen ;
2023-12-04 18:04:39 -05:00
using System.Diagnostics.CodeAnalysis ;
2023-09-11 21:20:46 +10:00
using System.Linq ;
using Content.Shared.ActionBlocker ;
2024-08-07 12:15:41 +12:00
using Content.Shared.Administration.Logs ;
2025-02-08 17:17:55 +11:00
using Content.Shared.CCVar ;
2023-09-11 21:20:46 +10:00
using Content.Shared.Containers.ItemSlots ;
2024-08-07 12:15:41 +12:00
using Content.Shared.Database ;
2023-09-11 21:20:46 +10:00
using Content.Shared.Destructible ;
using Content.Shared.DoAfter ;
using Content.Shared.Hands.Components ;
using Content.Shared.Hands.EntitySystems ;
using Content.Shared.Implants.Components ;
2024-04-19 23:23:45 -07:00
using Content.Shared.Input ;
2023-09-11 21:20:46 +10:00
using Content.Shared.Interaction ;
2024-05-23 18:23:55 -07:00
using Content.Shared.Interaction.Components ;
2024-04-19 23:23:45 -07:00
using Content.Shared.Inventory ;
2023-09-11 21:20:46 +10:00
using Content.Shared.Item ;
using Content.Shared.Lock ;
2024-03-19 03:36:21 +01:00
using Content.Shared.Materials ;
2023-09-11 21:20:46 +10:00
using Content.Shared.Placeable ;
using Content.Shared.Popups ;
using Content.Shared.Stacks ;
using Content.Shared.Storage.Components ;
2025-03-27 18:42:57 +01:00
using Content.Shared.Tag ;
2023-09-11 21:20:46 +10:00
using Content.Shared.Timing ;
2024-08-10 19:29:44 -07:00
using Content.Shared.Storage.Events ;
2023-09-11 21:20:46 +10:00
using Content.Shared.Verbs ;
2024-05-25 13:07:37 -07:00
using Content.Shared.Whitelist ;
2024-07-22 02:54:15 -07:00
using Robust.Shared.Audio ;
2023-11-27 22:12:34 +11:00
using Robust.Shared.Audio.Systems ;
2025-02-08 17:17:55 +11:00
using Robust.Shared.Configuration ;
2023-09-11 21:20:46 +10:00
using Robust.Shared.Containers ;
2024-01-09 21:47:51 +11:00
using Robust.Shared.GameStates ;
2024-04-19 23:23:45 -07:00
using Robust.Shared.Input.Binding ;
2023-09-11 21:20:46 +10:00
using Robust.Shared.Map ;
2024-04-19 23:23:45 -07:00
using Robust.Shared.Player ;
2023-11-06 02:20:50 -05:00
using Robust.Shared.Prototypes ;
2023-09-11 21:20:46 +10:00
using Robust.Shared.Random ;
2024-01-09 21:47:51 +11:00
using Robust.Shared.Serialization ;
2025-02-08 17:17:55 +11:00
using Robust.Shared.Timing ;
2024-04-26 18:16:24 +10:00
using Robust.Shared.Utility ;
2023-09-11 21:20:46 +10:00
namespace Content.Shared.Storage.EntitySystems ;
public abstract class SharedStorageSystem : EntitySystem
{
2025-02-08 17:17:55 +11:00
[Dependency] private readonly IConfigurationManager _cfg = default ! ;
[Dependency] protected readonly IGameTiming Timing = default ! ;
[Dependency] private readonly IPrototypeManager _prototype = default ! ;
2023-09-11 21:20:46 +10:00
[Dependency] protected readonly IRobustRandom Random = default ! ;
2025-02-08 17:17:55 +11:00
[Dependency] private readonly ISharedAdminLogManager _adminLog = default ! ;
2024-01-09 21:47:51 +11:00
[Dependency] protected readonly ActionBlockerSystem ActionBlocker = default ! ;
2025-02-08 17:17:55 +11:00
[Dependency] private readonly EntityLookupSystem _entityLookupSystem = default ! ;
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default ! ;
[Dependency] private readonly InventorySystem _inventory = default ! ;
[Dependency] private readonly SharedAppearanceSystem _appearance = default ! ;
2024-01-09 21:47:51 +11:00
[Dependency] protected readonly SharedAudioSystem Audio = default ! ;
2025-02-08 17:17:55 +11:00
[Dependency] protected readonly SharedContainerSystem ContainerSystem = default ! ;
[Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default ! ;
2023-09-11 21:20:46 +10:00
[Dependency] protected readonly SharedEntityStorageSystem EntityStorage = default ! ;
2025-02-08 17:17:55 +11:00
[Dependency] private readonly SharedInteractionSystem _interactionSystem = default ! ;
2023-12-04 18:04:39 -05:00
[Dependency] protected readonly SharedItemSystem ItemSystem = default ! ;
2025-02-08 17:17:55 +11:00
[Dependency] private readonly SharedPopupSystem _popupSystem = default ! ;
[Dependency] private readonly SharedHandsSystem _sharedHandsSystem = default ! ;
[Dependency] private readonly SharedStackSystem _stack = default ! ;
2024-01-09 21:47:51 +11:00
[Dependency] protected readonly SharedTransformSystem TransformSystem = default ! ;
2025-02-08 17:17:55 +11:00
[Dependency] protected readonly SharedUserInterfaceSystem UI = default ! ;
2025-03-27 18:42:57 +01:00
[Dependency] private readonly TagSystem _tag = default ! ;
2023-09-11 21:20:46 +10:00
[Dependency] protected readonly UseDelaySystem UseDelay = default ! ;
private EntityQuery < ItemComponent > _itemQuery ;
private EntityQuery < StackComponent > _stackQuery ;
private EntityQuery < TransformComponent > _xformQuery ;
2025-02-08 17:17:55 +11:00
private EntityQuery < UserInterfaceUserComponent > _userQuery ;
/// <summary>
/// Whether we're allowed to go up-down storage via UI.
/// </summary>
public bool NestedStorage = true ;
2023-09-11 21:20:46 +10:00
2023-11-06 02:20:50 -05:00
[ValidatePrototypeId<ItemSizePrototype>]
public const string DefaultStorageMaxItemSize = "Normal" ;
2023-10-30 23:55:55 -04:00
2024-03-02 08:57:44 -05:00
public const float AreaInsertDelayPerItem = 0.075f ;
2024-07-22 02:54:15 -07:00
private static AudioParams _audioParams = AudioParams . Default
. WithMaxDistance ( 7f )
. WithVolume ( - 2f ) ;
2024-03-02 08:57:44 -05:00
2023-12-27 17:50:49 -05:00
private ItemSizePrototype _defaultStorageMaxItemSize = default ! ;
2025-02-08 17:17:55 +11:00
/// <summary>
/// Flag for whether we're checking for nested storage interactions.
/// </summary>
private bool _nestedCheck ;
2023-12-11 04:26:19 -05:00
public bool CheckingCanInsert ;
2025-02-08 17:17:55 +11:00
private readonly List < EntityUid > _entList = new ( ) ;
private readonly HashSet < EntityUid > _entSet = new ( ) ;
2024-04-20 11:51:16 +10:00
2023-12-27 17:50:49 -05:00
private readonly List < ItemSizePrototype > _sortedSizes = new ( ) ;
private FrozenDictionary < string , ItemSizePrototype > _nextSmallest = FrozenDictionary < string , ItemSizePrototype > . Empty ;
2024-04-25 22:25:52 -04:00
private const string QuickInsertUseDelayID = "quickInsert" ;
2024-04-26 18:16:24 +10:00
private const string OpenUiUseDelayID = "storage" ;
2024-04-25 22:25:52 -04:00
2025-02-08 17:17:55 +11:00
/// <summary>
/// How many storage windows are allowed to be open at once.
/// </summary>
private int _openStorageLimit = - 1 ;
2024-04-18 20:24:13 -07:00
protected readonly List < string > CantFillReasons = [ ] ;
2023-09-11 21:20:46 +10:00
/// <inheritdoc />
public override void Initialize ( )
{
base . Initialize ( ) ;
_itemQuery = GetEntityQuery < ItemComponent > ( ) ;
_stackQuery = GetEntityQuery < StackComponent > ( ) ;
_xformQuery = GetEntityQuery < TransformComponent > ( ) ;
2025-02-08 17:17:55 +11:00
_userQuery = GetEntityQuery < UserInterfaceUserComponent > ( ) ;
2023-12-27 17:50:49 -05:00
_prototype . PrototypesReloaded + = OnPrototypesReloaded ;
2023-09-11 21:20:46 +10:00
2025-02-08 17:17:55 +11:00
Subs . CVar ( _cfg , CCVars . StorageLimit , OnStorageLimitChanged , true ) ;
2024-04-26 18:16:24 +10:00
Subs . BuiEvents < StorageComponent > ( StorageComponent . StorageUiKey . Key , subs = >
{
subs . Event < BoundUIClosedEvent > ( OnBoundUIClosed ) ;
} ) ;
2024-06-26 07:11:51 -07:00
SubscribeLocalEvent < StorageComponent , ComponentRemove > ( OnRemove ) ;
2024-04-25 22:25:52 -04:00
SubscribeLocalEvent < StorageComponent , MapInitEvent > ( OnMapInit ) ;
2024-04-26 18:16:24 +10:00
SubscribeLocalEvent < StorageComponent , GetVerbsEvent < ActivationVerb > > ( AddUiVerb ) ;
2024-01-09 21:47:51 +11:00
SubscribeLocalEvent < StorageComponent , ComponentGetState > ( OnStorageGetState ) ;
2023-09-11 21:20:46 +10:00
SubscribeLocalEvent < StorageComponent , ComponentInit > ( OnComponentInit , before : new [ ] { typeof ( SharedContainerSystem ) } ) ;
SubscribeLocalEvent < StorageComponent , GetVerbsEvent < UtilityVerb > > ( AddTransferVerbs ) ;
SubscribeLocalEvent < StorageComponent , InteractUsingEvent > ( OnInteractUsing , after : new [ ] { typeof ( ItemSlotsSystem ) } ) ;
SubscribeLocalEvent < StorageComponent , ActivateInWorldEvent > ( OnActivate ) ;
SubscribeLocalEvent < StorageComponent , OpenStorageImplantEvent > ( OnImplantActivate ) ;
SubscribeLocalEvent < StorageComponent , AfterInteractEvent > ( AfterInteract ) ;
SubscribeLocalEvent < StorageComponent , DestructionEventArgs > ( OnDestroy ) ;
2025-02-08 17:17:55 +11:00
SubscribeLocalEvent < BoundUserInterfaceMessageAttempt > ( OnBoundUIAttempt ) ;
2023-09-11 21:20:46 +10:00
SubscribeLocalEvent < StorageComponent , BoundUIOpenedEvent > ( OnBoundUIOpen ) ;
2024-05-09 01:50:50 -05:00
SubscribeLocalEvent < StorageComponent , LockToggledEvent > ( OnLockToggled ) ;
2023-11-13 23:43:03 +11:00
SubscribeLocalEvent < MetaDataComponent , StackCountChangedEvent > ( OnStackCountChanged ) ;
2023-09-11 21:20:46 +10:00
2023-12-04 18:04:39 -05:00
SubscribeLocalEvent < StorageComponent , EntInsertedIntoContainerMessage > ( OnEntInserted ) ;
SubscribeLocalEvent < StorageComponent , EntRemovedFromContainerMessage > ( OnEntRemoved ) ;
2023-11-13 23:43:03 +11:00
SubscribeLocalEvent < StorageComponent , ContainerIsInsertingAttemptEvent > ( OnInsertAttempt ) ;
2023-09-11 21:20:46 +10:00
SubscribeLocalEvent < StorageComponent , AreaPickupDoAfterEvent > ( OnDoAfter ) ;
2025-02-08 17:17:55 +11:00
SubscribeAllEvent < OpenNestedStorageEvent > ( OnStorageNested ) ;
SubscribeAllEvent < StorageTransferItemEvent > ( OnStorageTransfer ) ;
2023-12-04 18:04:39 -05:00
SubscribeAllEvent < StorageInteractWithItemEvent > ( OnInteractWithItem ) ;
SubscribeAllEvent < StorageSetItemLocationEvent > ( OnSetItemLocation ) ;
SubscribeAllEvent < StorageInsertItemIntoLocationEvent > ( OnInsertItemIntoLocation ) ;
2024-03-28 06:31:47 +00:00
SubscribeAllEvent < StorageSaveItemLocationEvent > ( OnSaveItemLocation ) ;
2024-03-19 03:36:21 +01:00
SubscribeLocalEvent < StorageComponent , GotReclaimedEvent > ( OnReclaimed ) ;
2024-04-19 23:23:45 -07:00
CommandBinds . Builder
2024-04-20 11:24:52 -07:00
. Bind ( ContentKeyFunctions . OpenBackpack , InputCmdHandler . FromDelegate ( HandleOpenBackpack , handle : false ) )
. Bind ( ContentKeyFunctions . OpenBelt , InputCmdHandler . FromDelegate ( HandleOpenBelt , handle : false ) )
2024-04-19 23:23:45 -07:00
. Register < SharedStorageSystem > ( ) ;
2025-02-08 17:17:55 +11:00
Subs . CVar ( _cfg , CCVars . NestedStorage , OnNestedStorageCvar , true ) ;
2023-12-27 17:50:49 -05:00
UpdatePrototypeCache ( ) ;
}
2025-02-08 17:17:55 +11:00
private void OnNestedStorageCvar ( bool obj )
{
NestedStorage = obj ;
}
private void OnStorageLimitChanged ( int obj )
{
_openStorageLimit = obj ;
}
2024-06-26 07:11:51 -07:00
private void OnRemove ( Entity < StorageComponent > entity , ref ComponentRemove args )
{
2025-02-08 17:17:55 +11:00
UI . CloseUi ( entity . Owner , StorageComponent . StorageUiKey . Key ) ;
2024-06-26 07:11:51 -07:00
}
2024-04-26 18:16:24 +10:00
private void OnMapInit ( Entity < StorageComponent > entity , ref MapInitEvent args )
2024-04-25 22:25:52 -04:00
{
2024-05-02 08:16:16 -04:00
UseDelay . SetLength ( entity . Owner , entity . Comp . QuickInsertCooldown , QuickInsertUseDelayID ) ;
UseDelay . SetLength ( entity . Owner , entity . Comp . OpenUiCooldown , OpenUiUseDelayID ) ;
2024-04-25 22:25:52 -04:00
}
2024-01-09 21:47:51 +11:00
private void OnStorageGetState ( EntityUid uid , StorageComponent component , ref ComponentGetState args )
{
var storedItems = new Dictionary < NetEntity , ItemStorageLocation > ( ) ;
foreach ( var ( ent , location ) in component . StoredItems )
{
storedItems [ GetNetEntity ( ent ) ] = location ;
}
args . State = new StorageComponentState ( )
{
2024-01-10 20:07:35 +11:00
Grid = new List < Box2i > ( component . Grid ) ,
2024-01-09 21:47:51 +11:00
MaxItemSize = component . MaxItemSize ,
2024-03-28 06:31:47 +00:00
StoredItems = storedItems ,
2024-05-25 13:07:37 -07:00
SavedLocations = component . SavedLocations ,
Whitelist = component . Whitelist ,
Blacklist = component . Blacklist
2024-01-09 21:47:51 +11:00
} ;
}
2023-12-27 17:50:49 -05:00
public override void Shutdown ( )
{
_prototype . PrototypesReloaded - = OnPrototypesReloaded ;
}
private void OnPrototypesReloaded ( PrototypesReloadedEventArgs args )
{
if ( args . ByType . ContainsKey ( typeof ( ItemSizePrototype ) )
| | ( args . Removed ? . ContainsKey ( typeof ( ItemSizePrototype ) ) ? ? false ) )
{
UpdatePrototypeCache ( ) ;
}
}
private void UpdatePrototypeCache ( )
{
_defaultStorageMaxItemSize = _prototype . Index < ItemSizePrototype > ( DefaultStorageMaxItemSize ) ;
_sortedSizes . Clear ( ) ;
_sortedSizes . AddRange ( _prototype . EnumeratePrototypes < ItemSizePrototype > ( ) ) ;
_sortedSizes . Sort ( ) ;
var nextSmallest = new KeyValuePair < string , ItemSizePrototype > [ _sortedSizes . Count ] ;
for ( var i = 0 ; i < _sortedSizes . Count ; i + + )
{
var k = _sortedSizes [ i ] . ID ;
var v = _sortedSizes [ Math . Max ( i - 1 , 0 ) ] ;
nextSmallest [ i ] = new ( k , v ) ;
}
_nextSmallest = nextSmallest . ToFrozenDictionary ( ) ;
2023-09-11 21:20:46 +10:00
}
private void OnComponentInit ( EntityUid uid , StorageComponent storageComp , ComponentInit args )
{
2025-02-08 17:17:55 +11:00
storageComp . Container = ContainerSystem . EnsureContainer < Container > ( uid , StorageComponent . ContainerId ) ;
2023-11-13 23:43:03 +11:00
UpdateAppearance ( ( uid , storageComp , null ) ) ;
2023-09-11 21:20:46 +10:00
}
2024-04-26 18:16:24 +10:00
/// <summary>
/// If the user has nested-UIs open (e.g., PDA UI open when pda is in a backpack), close them.
/// </summary>
private void CloseNestedInterfaces ( EntityUid uid , EntityUid actor , StorageComponent ? storageComp = null )
{
if ( ! Resolve ( uid , ref storageComp ) )
return ;
2023-09-11 21:20:46 +10:00
2024-04-26 18:16:24 +10:00
// for each containing thing
// if it has a storage comp
// ensure unsubscribe from session
// if it has a ui component
// close ui
foreach ( var entity in storageComp . Container . ContainedEntities )
{
2025-02-08 17:17:55 +11:00
UI . CloseUis ( entity , actor ) ;
2024-04-26 18:16:24 +10:00
}
}
private void OnBoundUIClosed ( EntityUid uid , StorageComponent storageComp , BoundUIClosedEvent args )
{
CloseNestedInterfaces ( uid , args . Actor , storageComp ) ;
// If UI is closed for everyone
2025-02-08 17:17:55 +11:00
if ( ! UI . IsUiOpen ( uid , args . UiKey ) )
2024-04-26 18:16:24 +10:00
{
UpdateAppearance ( ( uid , storageComp , null ) ) ;
2025-03-27 18:42:57 +01:00
if ( ! _tag . HasTag ( args . Actor , storageComp . SilentStorageUserTag ) )
Audio . PlayPredicted ( storageComp . StorageCloseSound , uid , args . Actor ) ;
2024-04-26 18:16:24 +10:00
}
}
private void AddUiVerb ( EntityUid uid , StorageComponent component , GetVerbsEvent < ActivationVerb > args )
{
2025-03-01 10:03:27 -08:00
if ( component . ShowVerb = = false | | ! CanInteract ( args . User , ( uid , component ) , args . CanAccess & & args . CanInteract ) )
2024-05-23 18:23:55 -07:00
return ;
2024-04-26 18:16:24 +10:00
// Does this player currently have the storage UI open?
2025-02-08 17:17:55 +11:00
var uiOpen = UI . IsUiOpen ( uid , StorageComponent . StorageUiKey . Key , args . User ) ;
2024-04-26 18:16:24 +10:00
ActivationVerb verb = new ( )
{
Act = ( ) = >
{
if ( uiOpen )
{
2025-02-08 17:17:55 +11:00
UI . CloseUi ( uid , StorageComponent . StorageUiKey . Key , args . User ) ;
2024-04-26 18:16:24 +10:00
}
else
{
2025-02-23 03:01:33 +01:00
OpenStorageUI ( uid , args . User , component , false ) ;
2024-04-26 18:16:24 +10:00
}
}
} ;
if ( uiOpen )
{
verb . Text = Loc . GetString ( "comp-storage-verb-close-storage" ) ;
verb . Icon = new SpriteSpecifier . Texture (
new ( "/Textures/Interface/VerbIcons/close.svg.192dpi.png" ) ) ;
}
else
{
verb . Text = Loc . GetString ( "comp-storage-verb-open-storage" ) ;
verb . Icon = new SpriteSpecifier . Texture (
new ( "/Textures/Interface/VerbIcons/open.svg.192dpi.png" ) ) ;
}
args . Verbs . Add ( verb ) ;
}
2025-02-17 19:24:34 +11:00
public void OpenStorageUI ( EntityUid uid , EntityUid actor , StorageComponent ? storageComp = null , bool silent = true )
{
// Handle recursively opening nested storages.
if ( ContainerSystem . TryGetContainingContainer ( uid , out var container ) & &
UI . IsUiOpen ( container . Owner , StorageComponent . StorageUiKey . Key , actor ) )
{
_nestedCheck = true ;
HideStorageWindow ( container . Owner , actor ) ;
OpenStorageUIInternal ( uid , actor , storageComp , silent : true ) ;
_nestedCheck = false ;
}
else
{
// If you need something more sophisticated for multi-UI you'll need to code some smarter
// interactions.
if ( _openStorageLimit = = 1 )
UI . CloseUserUis < StorageComponent . StorageUiKey > ( actor ) ;
OpenStorageUIInternal ( uid , actor , storageComp , silent : silent ) ;
}
}
2024-04-26 18:16:24 +10:00
/// <summary>
/// Opens the storage UI for an entity
/// </summary>
/// <param name="entity">The entity to open the UI for</param>
2025-02-17 19:24:34 +11:00
private void OpenStorageUIInternal ( EntityUid uid , EntityUid entity , StorageComponent ? storageComp = null , bool silent = true )
2024-04-26 18:16:24 +10:00
{
if ( ! Resolve ( uid , ref storageComp , false ) )
return ;
// prevent spamming bag open / honkerton honk sound
2024-06-06 10:14:25 +12:00
silent | = TryComp < UseDelayComponent > ( uid , out var useDelay ) & & UseDelay . IsDelayed ( ( uid , useDelay ) , id : OpenUiUseDelayID ) ;
2024-05-23 18:23:55 -07:00
if ( ! CanInteract ( entity , ( uid , storageComp ) , silent : silent ) )
return ;
2025-02-08 17:17:55 +11:00
if ( ! UI . TryOpenUi ( uid , StorageComponent . StorageUiKey . Key , entity ) )
return ;
2025-03-27 18:42:57 +01:00
if ( ! silent & & ! _tag . HasTag ( entity , storageComp . SilentStorageUserTag ) )
2024-04-26 18:16:24 +10:00
{
2025-02-08 17:17:55 +11:00
Audio . PlayPredicted ( storageComp . StorageOpenSound , uid , entity ) ;
2024-04-26 18:16:24 +10:00
if ( useDelay ! = null )
2024-06-06 10:14:25 +12:00
UseDelay . TryResetDelay ( ( uid , useDelay ) , id : OpenUiUseDelayID ) ;
2024-04-26 18:16:24 +10:00
}
}
public virtual void UpdateUI ( Entity < StorageComponent ? > entity ) { }
2023-09-11 21:20:46 +10:00
private void AddTransferVerbs ( EntityUid uid , StorageComponent component , GetVerbsEvent < UtilityVerb > args )
{
if ( ! args . CanAccess | | ! args . CanInteract )
return ;
var entities = component . Container . ContainedEntities ;
2024-05-23 18:23:55 -07:00
if ( entities . Count = = 0 | | ! CanInteract ( args . User , ( uid , component ) ) )
2023-09-11 21:20:46 +10:00
return ;
// if the target is storage, add a verb to transfer storage.
if ( TryComp ( args . Target , out StorageComponent ? targetStorage )
2023-11-13 23:43:03 +11:00
& & ( ! TryComp ( args . Target , out LockComponent ? targetLock ) | | ! targetLock . Locked ) )
2023-09-11 21:20:46 +10:00
{
UtilityVerb verb = new ( )
{
Text = Loc . GetString ( "storage-component-transfer-verb" ) ,
IconEntity = GetNetEntity ( args . Using ) ,
2024-05-23 18:23:55 -07:00
Act = ( ) = > TransferEntities ( uid , args . Target , args . User , component , null , targetStorage , targetLock )
2023-09-11 21:20:46 +10:00
} ;
args . Verbs . Add ( verb ) ;
}
}
/// <summary>
/// Inserts storable entities into this storage container if possible, otherwise return to the hand of the user
/// </summary>
/// <returns>true if inserted, false otherwise</returns>
private void OnInteractUsing ( EntityUid uid , StorageComponent storageComp , InteractUsingEvent args )
{
2024-12-18 01:12:35 +01:00
if ( args . Handled | | ! storageComp . ClickInsert | | ! CanInteract ( args . User , ( uid , storageComp ) , silent : false ) )
2023-09-11 21:20:46 +10:00
return ;
2024-09-19 00:42:49 -07:00
var attemptEv = new StorageInteractUsingAttemptEvent ( ) ;
RaiseLocalEvent ( uid , ref attemptEv ) ;
if ( attemptEv . Cancelled )
2023-09-11 21:20:46 +10:00
return ;
2024-08-10 19:29:44 -07:00
PlayerInsertHeldEntity ( ( uid , storageComp ) , args . User ) ;
2023-09-11 21:20:46 +10:00
// Always handle it, even if insertion fails.
// We don't want to trigger any AfterInteract logic here.
2023-12-04 18:04:39 -05:00
// Example issue would be placing wires if item doesn't fit in backpack.
2023-09-11 21:20:46 +10:00
args . Handled = true ;
}
/// <summary>
/// Sends a message to open the storage UI
/// </summary>
private void OnActivate ( EntityUid uid , StorageComponent storageComp , ActivateInWorldEvent args )
{
2024-12-18 01:12:35 +01:00
if ( args . Handled | | ! args . Complex | | ! storageComp . OpenOnActivate | | ! CanInteract ( args . User , ( uid , storageComp ) ) )
2023-09-11 21:20:46 +10:00
return ;
2024-04-26 18:16:24 +10:00
// Toggle
2025-02-08 17:17:55 +11:00
if ( UI . IsUiOpen ( uid , StorageComponent . StorageUiKey . Key , args . User ) )
2024-04-26 18:16:24 +10:00
{
2025-02-08 17:17:55 +11:00
UI . CloseUi ( uid , StorageComponent . StorageUiKey . Key , args . User ) ;
2024-04-26 18:16:24 +10:00
}
else
{
2025-02-23 03:01:33 +01:00
OpenStorageUI ( uid , args . User , storageComp , false ) ;
2024-04-26 18:16:24 +10:00
}
2023-12-04 18:04:39 -05:00
args . Handled = true ;
2023-09-11 21:20:46 +10:00
}
2025-02-08 17:17:55 +11:00
protected virtual void HideStorageWindow ( EntityUid uid , EntityUid actor )
{
}
protected virtual void ShowStorageWindow ( EntityUid uid , EntityUid actor )
{
}
2023-09-11 21:20:46 +10:00
/// <summary>
/// Specifically for storage implants.
/// </summary>
private void OnImplantActivate ( EntityUid uid , StorageComponent storageComp , OpenStorageImplantEvent args )
{
2023-12-04 18:04:39 -05:00
if ( args . Handled )
2023-09-11 21:20:46 +10:00
return ;
2025-02-08 17:17:55 +11:00
var uiOpen = UI . IsUiOpen ( uid , StorageComponent . StorageUiKey . Key , args . Performer ) ;
2024-08-24 03:31:02 +02:00
if ( uiOpen )
2025-02-08 17:17:55 +11:00
UI . CloseUi ( uid , StorageComponent . StorageUiKey . Key , args . Performer ) ;
2024-08-24 03:31:02 +02:00
else
OpenStorageUI ( uid , args . Performer , storageComp , false ) ;
2023-12-04 18:04:39 -05:00
args . Handled = true ;
2023-09-11 21:20:46 +10:00
}
/// <summary>
/// Allows a user to pick up entities by clicking them, or pick up all entities in a certain radius
/// around a click.
/// </summary>
/// <returns></returns>
private void AfterInteract ( EntityUid uid , StorageComponent storageComp , AfterInteractEvent args )
{
2024-04-25 22:25:52 -04:00
if ( args . Handled | | ! args . CanReach | | ! UseDelay . TryResetDelay ( uid , checkDelayed : true , id : QuickInsertUseDelayID ) )
2023-09-11 21:20:46 +10:00
return ;
// Pick up all entities in a radius around the clicked location.
// The last half of the if is because carpets exist and this is terrible
if ( storageComp . AreaInsert & & ( args . Target = = null | | ! HasComp < ItemComponent > ( args . Target . Value ) ) )
{
2024-04-20 11:51:16 +10:00
_entList . Clear ( ) ;
_entSet . Clear ( ) ;
_entityLookupSystem . GetEntitiesInRange ( args . ClickLocation , storageComp . AreaInsertRadius , _entSet , LookupFlags . Dynamic | LookupFlags . Sundries ) ;
2024-03-02 08:57:44 -05:00
var delay = 0f ;
2023-09-11 21:20:46 +10:00
2024-04-20 11:51:16 +10:00
foreach ( var entity in _entSet )
2023-09-11 21:20:46 +10:00
{
if ( entity = = args . User
2024-04-20 11:51:16 +10:00
| | ! _itemQuery . TryGetComponent ( entity , out var itemComp ) // Need comp to get item size to get weight
2024-03-02 08:57:44 -05:00
| | ! _prototype . TryIndex ( itemComp . Size , out var itemSize )
2024-04-20 11:51:16 +10:00
| | ! CanInsert ( uid , entity , out _ , storageComp , item : itemComp )
2023-09-11 21:20:46 +10:00
| | ! _interactionSystem . InRangeUnobstructed ( args . User , entity ) )
{
continue ;
}
2024-04-20 11:51:16 +10:00
_entList . Add ( entity ) ;
2024-03-02 08:57:44 -05:00
delay + = itemSize . Weight * AreaInsertDelayPerItem ;
2024-04-20 11:51:16 +10:00
if ( _entList . Count > = StorageComponent . AreaPickupLimit )
break ;
2023-09-11 21:20:46 +10:00
}
//If there's only one then let's be generous
2024-09-24 17:23:48 +03:00
if ( _entList . Count > = 1 )
2023-09-11 21:20:46 +10:00
{
2024-04-20 11:51:16 +10:00
var doAfterArgs = new DoAfterArgs ( EntityManager , args . User , delay , new AreaPickupDoAfterEvent ( GetNetEntityList ( _entList ) ) , uid , target : uid )
2023-09-11 21:20:46 +10:00
{
BreakOnDamage = true ,
2024-03-19 12:09:00 +02:00
BreakOnMove = true ,
2024-08-08 04:39:46 -07:00
NeedHand = true ,
2023-09-11 21:20:46 +10:00
} ;
_doAfterSystem . TryStartDoAfter ( doAfterArgs ) ;
2023-11-13 23:43:03 +11:00
args . Handled = true ;
2023-09-11 21:20:46 +10:00
}
return ;
}
// Pick up the clicked entity
if ( storageComp . QuickInsert )
{
if ( args . Target is not { Valid : true } target )
return ;
2025-02-08 17:17:55 +11:00
if ( ContainerSystem . IsEntityInContainer ( target )
2023-09-11 21:20:46 +10:00
| | target = = args . User
2024-04-20 11:51:16 +10:00
| | ! _itemQuery . HasComponent ( target ) )
2023-09-11 21:20:46 +10:00
{
return ;
}
2024-05-21 17:40:35 +12:00
if ( TryComp ( uid , out TransformComponent ? transformOwner ) & & TryComp ( target , out TransformComponent ? transformEnt ) )
2023-09-11 21:20:46 +10:00
{
var parent = transformOwner . ParentUid ;
2025-02-20 07:03:42 -05:00
var position = TransformSystem . ToCoordinates (
2023-09-11 21:20:46 +10:00
parent . IsValid ( ) ? parent : uid ,
2025-02-20 07:03:42 -05:00
TransformSystem . GetMapCoordinates ( transformEnt )
2023-09-11 21:20:46 +10:00
) ;
2023-11-13 23:43:03 +11:00
args . Handled = true ;
2023-10-30 23:55:55 -04:00
if ( PlayerInsertEntityInWorld ( ( uid , storageComp ) , args . User , target ) )
2023-09-11 21:20:46 +10:00
{
2024-04-20 11:51:16 +10:00
EntityManager . RaiseSharedEvent ( new AnimateInsertingEntitiesEvent ( GetNetEntity ( uid ) ,
2023-09-11 21:20:46 +10:00
new List < NetEntity > { GetNetEntity ( target ) } ,
new List < NetCoordinates > { GetNetCoordinates ( position ) } ,
2024-04-20 11:51:16 +10:00
new List < Angle > { transformOwner . LocalRotation } ) , args . User ) ;
2023-09-11 21:20:46 +10:00
}
}
}
}
private void OnDoAfter ( EntityUid uid , StorageComponent component , AreaPickupDoAfterEvent args )
{
if ( args . Handled | | args . Cancelled )
return ;
2023-11-13 23:43:03 +11:00
args . Handled = true ;
2023-09-11 21:20:46 +10:00
var successfullyInserted = new List < EntityUid > ( ) ;
var successfullyInsertedPositions = new List < EntityCoordinates > ( ) ;
var successfullyInsertedAngles = new List < Angle > ( ) ;
2024-04-20 11:51:16 +10:00
if ( ! _xformQuery . TryGetComponent ( uid , out var xform ) )
2023-09-11 21:20:46 +10:00
{
2024-04-20 11:51:16 +10:00
return ;
}
var entCount = Math . Min ( StorageComponent . AreaPickupLimit , args . Entities . Count ) ;
for ( var i = 0 ; i < entCount ; i + + )
{
var entity = GetEntity ( args . Entities [ i ] ) ;
2023-09-11 21:20:46 +10:00
// Check again, situation may have changed for some entities, but we'll still pick up any that are valid
2025-02-08 17:17:55 +11:00
if ( ContainerSystem . IsEntityInContainer ( entity )
2023-09-11 21:20:46 +10:00
| | entity = = args . Args . User
| | ! _itemQuery . HasComponent ( entity ) )
2024-04-20 11:51:16 +10:00
{
2023-09-11 21:20:46 +10:00
continue ;
2024-04-20 11:51:16 +10:00
}
2023-09-11 21:20:46 +10:00
2024-04-20 11:51:16 +10:00
if ( ! _xformQuery . TryGetComponent ( entity , out var targetXform ) | |
2023-09-11 21:20:46 +10:00
targetXform . MapID ! = xform . MapID )
{
continue ;
}
2025-02-20 07:03:42 -05:00
var position = TransformSystem . ToCoordinates (
2023-09-11 21:20:46 +10:00
xform . ParentUid . IsValid ( ) ? xform . ParentUid : uid ,
2025-02-20 07:03:42 -05:00
new MapCoordinates ( TransformSystem . GetWorldPosition ( targetXform ) , targetXform . MapID )
2023-09-11 21:20:46 +10:00
) ;
var angle = targetXform . LocalRotation ;
2024-09-27 17:09:17 +10:00
if ( PlayerInsertEntityInWorld ( ( uid , component ) , args . Args . User , entity , playSound : false ) )
2023-09-11 21:20:46 +10:00
{
successfullyInserted . Add ( entity ) ;
successfullyInsertedPositions . Add ( position ) ;
successfullyInsertedAngles . Add ( angle ) ;
}
}
2023-12-04 18:04:39 -05:00
// If we picked up at least one thing, play a sound and do a cool animation!
2023-09-11 21:20:46 +10:00
if ( successfullyInserted . Count > 0 )
{
2025-03-27 18:42:57 +01:00
if ( ! _tag . HasTag ( args . User , component . SilentStorageUserTag ) )
Audio . PlayPredicted ( component . StorageInsertSound , uid , args . User , _audioParams ) ;
2024-04-20 11:51:16 +10:00
EntityManager . RaiseSharedEvent ( new AnimateInsertingEntitiesEvent (
2023-09-11 21:20:46 +10:00
GetNetEntity ( uid ) ,
GetNetEntityList ( successfullyInserted ) ,
GetNetCoordinatesList ( successfullyInsertedPositions ) ,
2024-04-20 11:51:16 +10:00
successfullyInsertedAngles ) , args . User ) ;
2023-09-11 21:20:46 +10:00
}
args . Handled = true ;
}
2024-03-19 03:36:21 +01:00
private void OnReclaimed ( EntityUid uid , StorageComponent storageComp , GotReclaimedEvent args )
{
2025-02-08 17:17:55 +11:00
ContainerSystem . EmptyContainer ( storageComp . Container , destination : args . ReclaimerCoordinates ) ;
2024-03-19 03:36:21 +01:00
}
2023-09-11 21:20:46 +10:00
private void OnDestroy ( EntityUid uid , StorageComponent storageComp , DestructionEventArgs args )
{
2023-12-04 18:04:39 -05:00
var coordinates = TransformSystem . GetMoverCoordinates ( uid ) ;
2023-09-11 21:20:46 +10:00
// Being destroyed so need to recalculate.
2025-02-08 17:17:55 +11:00
ContainerSystem . EmptyContainer ( storageComp . Container , destination : coordinates ) ;
2023-09-11 21:20:46 +10:00
}
/// <summary>
/// This function gets called when the user clicked on an item in the storage UI. This will either place the
/// item in the user's hand if it is currently empty, or interact with the item using the user's currently
/// held item.
/// </summary>
2023-12-04 18:04:39 -05:00
private void OnInteractWithItem ( StorageInteractWithItemEvent msg , EntitySessionEventArgs args )
2023-09-11 21:20:46 +10:00
{
2024-08-07 12:15:41 +12:00
if ( ! ValidateInput ( args , msg . StorageUid , msg . InteractedItemUid , out var player , out var storage , out var item ) )
2023-09-11 21:20:46 +10:00
return ;
// If the user's active hand is empty, try pick up the item.
2024-08-07 12:15:41 +12:00
if ( player . Comp . ActiveHandEntity = = null )
2023-09-11 21:20:46 +10:00
{
2024-08-07 12:15:41 +12:00
_adminLog . Add (
LogType . Storage ,
LogImpact . Low ,
$"{ToPrettyString(player):player} is attempting to take {ToPrettyString(item):item} out of {ToPrettyString(storage):storage}" ) ;
if ( _sharedHandsSystem . TryPickupAnyHand ( player , item , handsComp : player . Comp )
2025-03-27 18:42:57 +01:00
& & storage . Comp . StorageRemoveSound ! = null
& & ! _tag . HasTag ( player , storage . Comp . SilentStorageUserTag ) )
2023-09-11 21:20:46 +10:00
{
2024-08-07 12:15:41 +12:00
Audio . PlayPredicted ( storage . Comp . StorageRemoveSound , storage , player , _audioParams ) ;
2023-09-11 21:20:46 +10:00
}
2024-08-07 12:15:41 +12:00
return ;
2023-09-11 21:20:46 +10:00
}
2024-08-07 12:15:41 +12:00
_adminLog . Add (
LogType . Storage ,
LogImpact . Low ,
$"{ToPrettyString(player):player} is interacting with {ToPrettyString(item):item} while it is stored in {ToPrettyString(storage):storage} using {ToPrettyString(player.Comp.ActiveHandEntity):used}" ) ;
2023-09-11 21:20:46 +10:00
// Else, interact using the held item
2024-08-10 19:29:44 -07:00
if ( _interactionSystem . InteractUsing ( player ,
player . Comp . ActiveHandEntity . Value ,
item ,
Transform ( item ) . Coordinates ,
checkCanInteract : false ) )
return ;
var failedEv = new StorageInsertFailedEvent ( ( storage , storage . Comp ) , ( player , player . Comp ) ) ;
RaiseLocalEvent ( storage , ref failedEv ) ;
2023-09-11 21:20:46 +10:00
}
2023-12-04 18:04:39 -05:00
private void OnSetItemLocation ( StorageSetItemLocationEvent msg , EntitySessionEventArgs args )
{
2024-08-07 12:15:41 +12:00
if ( ! ValidateInput ( args , msg . StorageEnt , msg . ItemEnt , out var player , out var storage , out var item ) )
2023-12-04 18:04:39 -05:00
return ;
2024-08-07 12:15:41 +12:00
_adminLog . Add (
LogType . Storage ,
LogImpact . Low ,
$"{ToPrettyString(player):player} is updating the location of {ToPrettyString(item):item} within {ToPrettyString(storage):storage}" ) ;
2023-12-04 18:04:39 -05:00
2024-08-07 12:15:41 +12:00
TrySetItemStorageLocation ( item ! , storage ! , msg . Location ) ;
2023-12-04 18:04:39 -05:00
}
2025-02-08 17:17:55 +11:00
private void OnStorageNested ( OpenNestedStorageEvent msg , EntitySessionEventArgs args )
{
if ( ! NestedStorage )
return ;
if ( ! TryGetEntity ( msg . InteractedItemUid , out var itemEnt ) )
return ;
_nestedCheck = true ;
var result = ValidateInput ( args ,
msg . StorageUid ,
msg . InteractedItemUid ,
out var player ,
out var storage ,
out var item ) ;
if ( ! result )
{
_nestedCheck = false ;
return ;
}
HideStorageWindow ( storage . Owner , player . Owner ) ;
OpenStorageUI ( item . Owner , player . Owner , silent : true ) ;
_nestedCheck = false ;
}
private void OnStorageTransfer ( StorageTransferItemEvent msg , EntitySessionEventArgs args )
{
if ( ! TryGetEntity ( msg . ItemEnt , out var itemEnt ) )
return ;
var localPlayer = args . SenderSession . AttachedEntity ;
if ( ! TryComp ( localPlayer , out HandsComponent ? handsComp ) | | ! _sharedHandsSystem . TryPickup ( localPlayer . Value , itemEnt . Value , handsComp : handsComp , animate : false ) )
return ;
if ( ! ValidateInput ( args , msg . StorageEnt , msg . ItemEnt , out var player , out var storage , out var item , held : true ) )
return ;
_adminLog . Add (
LogType . Storage ,
LogImpact . Low ,
$"{ToPrettyString(player):player} is inserting {ToPrettyString(item):item} into {ToPrettyString(storage):storage}" ) ;
InsertAt ( storage ! , item ! , msg . Location , out _ , player , stackAutomatically : false ) ;
}
2023-12-04 18:04:39 -05:00
private void OnInsertItemIntoLocation ( StorageInsertItemIntoLocationEvent msg , EntitySessionEventArgs args )
2023-09-11 21:20:46 +10:00
{
2024-08-07 12:15:41 +12:00
if ( ! ValidateInput ( args , msg . StorageEnt , msg . ItemEnt , out var player , out var storage , out var item , held : true ) )
2023-12-04 18:04:39 -05:00
return ;
2024-08-07 12:15:41 +12:00
_adminLog . Add (
LogType . Storage ,
LogImpact . Low ,
$"{ToPrettyString(player):player} is inserting {ToPrettyString(item):item} into {ToPrettyString(storage):storage}" ) ;
InsertAt ( storage ! , item ! , msg . Location , out _ , player , stackAutomatically : false ) ;
2023-09-11 21:20:46 +10:00
}
2024-03-28 06:31:47 +00:00
private void OnSaveItemLocation ( StorageSaveItemLocationEvent msg , EntitySessionEventArgs args )
{
2024-10-17 05:00:52 +01:00
if ( ! ValidateInput ( args , msg . Storage , msg . Item , out var player , out var storage , out var item ) )
2024-03-28 06:31:47 +00:00
return ;
2024-08-07 12:15:41 +12:00
SaveItemLocation ( storage ! , item . Owner ) ;
2024-03-28 06:31:47 +00:00
}
2025-02-08 17:17:55 +11:00
private void OnBoundUIOpen ( Entity < StorageComponent > ent , ref BoundUIOpenedEvent args )
2025-01-27 21:29:51 +11:00
{
2025-02-08 17:17:55 +11:00
UpdateAppearance ( ( ent . Owner , ent . Comp , null ) ) ;
}
private void OnBoundUIAttempt ( BoundUserInterfaceMessageAttempt args )
{
if ( args . UiKey is not StorageComponent . StorageUiKey . Key | |
_openStorageLimit = = - 1 | |
_nestedCheck | |
args . Message is not OpenBoundInterfaceMessage )
return ;
var uid = args . Target ;
var actor = args . Actor ;
var count = 0 ;
if ( _userQuery . TryComp ( actor , out var userComp ) )
{
foreach ( var ( ui , keys ) in userComp . OpenInterfaces )
{
if ( ui = = uid )
continue ;
foreach ( var key in keys )
{
if ( key is not StorageComponent . StorageUiKey )
continue ;
count + + ;
if ( count > = _openStorageLimit )
{
args . Cancel ( ) ;
}
break ;
}
}
}
2023-09-11 21:20:46 +10:00
}
2023-12-04 18:04:39 -05:00
private void OnEntInserted ( Entity < StorageComponent > entity , ref EntInsertedIntoContainerMessage args )
2023-09-11 21:20:46 +10:00
{
2023-11-13 23:43:03 +11:00
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
2023-12-04 18:04:39 -05:00
if ( entity . Comp . Container = = null )
2023-11-13 23:43:03 +11:00
return ;
2023-09-11 21:20:46 +10:00
2023-11-13 23:43:03 +11:00
if ( args . Container . ID ! = StorageComponent . ContainerId )
return ;
2024-01-09 21:47:51 +11:00
if ( ! entity . Comp . StoredItems . ContainsKey ( args . Entity ) )
2023-12-04 18:04:39 -05:00
{
if ( ! TryGetAvailableGridSpace ( ( entity . Owner , entity . Comp ) , ( args . Entity , null ) , out var location ) )
{
2025-02-08 17:17:55 +11:00
ContainerSystem . Remove ( args . Entity , args . Container , force : true ) ;
2023-12-04 18:04:39 -05:00
return ;
}
2024-01-09 21:47:51 +11:00
entity . Comp . StoredItems [ args . Entity ] = location . Value ;
2023-12-04 18:04:39 -05:00
Dirty ( entity , entity . Comp ) ;
}
UpdateAppearance ( ( entity , entity . Comp , null ) ) ;
UpdateUI ( ( entity , entity . Comp ) ) ;
}
private void OnEntRemoved ( Entity < StorageComponent > entity , ref EntRemovedFromContainerMessage args )
{
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
if ( entity . Comp . Container = = null )
return ;
if ( args . Container . ID ! = StorageComponent . ContainerId )
return ;
2024-01-09 21:47:51 +11:00
entity . Comp . StoredItems . Remove ( args . Entity ) ;
2023-12-04 18:04:39 -05:00
Dirty ( entity , entity . Comp ) ;
UpdateAppearance ( ( entity , entity . Comp , null ) ) ;
UpdateUI ( ( entity , entity . Comp ) ) ;
2023-09-11 21:20:46 +10:00
}
2023-11-13 23:43:03 +11:00
private void OnInsertAttempt ( EntityUid uid , StorageComponent component , ContainerIsInsertingAttemptEvent args )
2023-09-11 21:20:46 +10:00
{
2023-11-13 23:43:03 +11:00
if ( args . Cancelled | | args . Container . ID ! = StorageComponent . ContainerId )
2023-09-11 21:20:46 +10:00
return ;
2023-12-11 04:26:19 -05:00
// don't run cyclical CanInsert() loops
if ( CheckingCanInsert )
return ;
2024-04-18 20:24:13 -07:00
if ( ! CanInsert ( uid , args . EntityUid , out var reason , component , ignoreStacks : true ) )
{
#if DEBUG
if ( reason ! = null )
CantFillReasons . Add ( reason ) ;
#endif
2023-11-13 23:43:03 +11:00
args . Cancel ( ) ;
2024-04-18 20:24:13 -07:00
}
2023-09-11 21:20:46 +10:00
}
2023-11-13 23:43:03 +11:00
public void UpdateAppearance ( Entity < StorageComponent ? , AppearanceComponent ? > entity )
2023-09-11 21:20:46 +10:00
{
2023-11-13 23:43:03 +11:00
// TODO STORAGE remove appearance data and just use the data on the component.
var ( uid , storage , appearance ) = entity ;
if ( ! Resolve ( uid , ref storage , ref appearance , false ) )
return ;
2023-11-19 15:10:27 +13:00
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
if ( storage . Container = = null )
return ; // component hasn't yet been initialized.
2023-12-04 18:04:39 -05:00
var capacity = storage . Grid . GetArea ( ) ;
var used = GetCumulativeItemAreas ( ( uid , storage ) ) ;
2023-11-13 23:43:03 +11:00
2025-02-08 17:17:55 +11:00
var isOpen = UI . IsUiOpen ( entity . Owner , StorageComponent . StorageUiKey . Key ) ;
2024-04-26 18:16:24 +10:00
2023-11-13 23:43:03 +11:00
_appearance . SetData ( uid , StorageVisuals . StorageUsed , used , appearance ) ;
_appearance . SetData ( uid , StorageVisuals . Capacity , capacity , appearance ) ;
2024-04-26 18:16:24 +10:00
_appearance . SetData ( uid , StorageVisuals . Open , isOpen , appearance ) ;
_appearance . SetData ( uid , SharedBagOpenVisuals . BagState , isOpen ? SharedBagState . Open : SharedBagState . Closed , appearance ) ;
2024-07-21 07:49:48 +02:00
// HideClosedStackVisuals true sets the StackVisuals.Hide to the open state of the storage.
// This is for containers that only show their contents when open. (e.g. donut boxes)
if ( storage . HideStackVisualsWhenClosed )
_appearance . SetData ( uid , StackVisuals . Hide , ! isOpen , appearance ) ;
2023-09-11 21:20:46 +10:00
}
/// <summary>
/// Move entities from one storage to another.
/// </summary>
public void TransferEntities ( EntityUid source , EntityUid target , EntityUid ? user = null ,
StorageComponent ? sourceComp = null , LockComponent ? sourceLock = null ,
StorageComponent ? targetComp = null , LockComponent ? targetLock = null )
{
if ( ! Resolve ( source , ref sourceComp ) | | ! Resolve ( target , ref targetComp ) )
return ;
var entities = sourceComp . Container . ContainedEntities ;
if ( entities . Count = = 0 )
return ;
if ( Resolve ( source , ref sourceLock , false ) & & sourceLock . Locked
| | Resolve ( target , ref targetLock , false ) & & targetLock . Locked )
return ;
foreach ( var entity in entities . ToArray ( ) )
{
2023-09-12 22:34:04 +10:00
Insert ( target , entity , out _ , user : user , targetComp , playSound : false ) ;
2023-09-11 21:20:46 +10:00
}
2025-03-27 18:42:57 +01:00
if ( user ! = null
& & ( ! _tag . HasTag ( user . Value , sourceComp . SilentStorageUserTag )
| | ! _tag . HasTag ( user . Value , targetComp . SilentStorageUserTag ) ) )
Audio . PlayPredicted ( sourceComp . StorageInsertSound , target , user , _audioParams ) ;
2023-09-11 21:20:46 +10:00
}
/// <summary>
/// Verifies if an entity can be stored and if it fits
/// </summary>
/// <param name="uid">The entity to check</param>
2023-10-30 23:55:55 -04:00
/// <param name="insertEnt"></param>
2023-09-11 21:20:46 +10:00
/// <param name="reason">If returning false, the reason displayed to the player</param>
2023-10-30 23:55:55 -04:00
/// <param name="storageComp"></param>
/// <param name="item"></param>
2023-12-04 18:04:39 -05:00
/// <param name="ignoreStacks"></param>
/// <param name="ignoreLocation"></param>
2023-09-11 21:20:46 +10:00
/// <returns>true if it can be inserted, false otherwise</returns>
2023-12-04 18:04:39 -05:00
public bool CanInsert (
EntityUid uid ,
EntityUid insertEnt ,
out string? reason ,
StorageComponent ? storageComp = null ,
ItemComponent ? item = null ,
bool ignoreStacks = false ,
2023-12-11 04:26:19 -05:00
bool ignoreLocation = false )
2023-09-11 21:20:46 +10:00
{
2023-11-13 23:43:03 +11:00
if ( ! Resolve ( uid , ref storageComp ) | | ! Resolve ( insertEnt , ref item , false ) )
2023-09-11 21:20:46 +10:00
{
reason = null ;
return false ;
}
2023-10-30 23:55:55 -04:00
if ( Transform ( insertEnt ) . Anchored )
2023-09-11 21:20:46 +10:00
{
reason = "comp-storage-anchored-failure" ;
return false ;
}
2024-06-01 20:10:24 -07:00
if ( _whitelistSystem . IsWhitelistFail ( storageComp . Whitelist , insertEnt ) | |
_whitelistSystem . IsBlacklistPass ( storageComp . Blacklist , insertEnt ) )
2023-09-11 21:20:46 +10:00
{
reason = "comp-storage-invalid-container" ;
return false ;
}
2023-11-13 23:43:03 +11:00
if ( ! ignoreStacks
& & _stackQuery . TryGetComponent ( insertEnt , out var stack )
& & HasSpaceInStacks ( ( uid , storageComp ) , stack . StackTypeId ) )
2023-09-11 21:20:46 +10:00
{
2023-11-13 23:43:03 +11:00
reason = null ;
return true ;
}
2023-10-30 23:55:55 -04:00
2023-12-27 17:50:49 -05:00
var maxSize = GetMaxItemSize ( ( uid , storageComp ) ) ;
2023-12-04 18:04:39 -05:00
if ( ItemSystem . GetSizePrototype ( item . Size ) > maxSize )
2023-11-13 23:43:03 +11:00
{
reason = "comp-storage-too-big" ;
return false ;
}
2023-09-11 21:20:46 +10:00
2023-11-13 23:43:03 +11:00
if ( TryComp < StorageComponent > ( insertEnt , out var insertStorage )
2023-12-27 17:50:49 -05:00
& & GetMaxItemSize ( ( insertEnt , insertStorage ) ) > = maxSize )
2023-11-13 23:43:03 +11:00
{
reason = "comp-storage-too-big" ;
return false ;
}
2024-01-09 21:47:51 +11:00
if ( ! ignoreLocation & & ! storageComp . StoredItems . ContainsKey ( insertEnt ) )
2023-11-13 23:43:03 +11:00
{
2023-12-04 18:04:39 -05:00
if ( ! TryGetAvailableGridSpace ( ( uid , storageComp ) , ( insertEnt , item ) , out _ ) )
2023-10-30 23:55:55 -04:00
{
reason = "comp-storage-insufficient-capacity" ;
return false ;
}
}
2023-09-11 21:20:46 +10:00
2023-12-11 04:26:19 -05:00
CheckingCanInsert = true ;
2025-02-08 17:17:55 +11:00
if ( ! ContainerSystem . CanInsert ( insertEnt , storageComp . Container ) )
2023-12-05 18:38:10 -05:00
{
2023-12-11 04:26:19 -05:00
CheckingCanInsert = false ;
2023-12-05 18:38:10 -05:00
reason = null ;
return false ;
}
2023-12-11 04:26:19 -05:00
CheckingCanInsert = false ;
2023-12-05 18:38:10 -05:00
2023-09-11 21:20:46 +10:00
reason = null ;
return true ;
}
2023-12-04 18:04:39 -05:00
/// <summary>
/// Inserts into the storage container at a given location
/// </summary>
/// <returns>true if the entity was inserted, false otherwise. This will also return true if a stack was partially
/// inserted.</returns>
public bool InsertAt (
Entity < StorageComponent ? > uid ,
Entity < ItemComponent ? > insertEnt ,
ItemStorageLocation location ,
out EntityUid ? stackedEntity ,
EntityUid ? user = null ,
2023-12-05 18:38:10 -05:00
bool playSound = true ,
bool stackAutomatically = true )
2023-12-04 18:04:39 -05:00
{
stackedEntity = null ;
if ( ! Resolve ( uid , ref uid . Comp ) )
return false ;
if ( ! ItemFitsInGridLocation ( insertEnt , uid , location ) )
return false ;
2024-01-09 21:47:51 +11:00
uid . Comp . StoredItems [ insertEnt ] = location ;
2023-12-04 18:04:39 -05:00
Dirty ( uid , uid . Comp ) ;
2023-12-05 18:38:10 -05:00
if ( Insert ( uid ,
insertEnt ,
out stackedEntity ,
out _ ,
user : user ,
storageComp : uid . Comp ,
playSound : playSound ,
stackAutomatically : stackAutomatically ) )
{
return true ;
}
2024-01-09 21:47:51 +11:00
uid . Comp . StoredItems . Remove ( insertEnt ) ;
2023-12-05 18:38:10 -05:00
return false ;
2023-12-04 18:04:39 -05:00
}
2023-09-11 21:20:46 +10:00
/// <summary>
/// Inserts into the storage container
/// </summary>
2023-11-13 23:43:03 +11:00
/// <returns>true if the entity was inserted, false otherwise. This will also return true if a stack was partially
/// inserted.</returns>
2023-10-30 23:55:55 -04:00
public bool Insert (
EntityUid uid ,
EntityUid insertEnt ,
out EntityUid ? stackedEntity ,
EntityUid ? user = null ,
StorageComponent ? storageComp = null ,
2023-12-05 18:38:10 -05:00
bool playSound = true ,
bool stackAutomatically = true )
2023-10-30 23:55:55 -04:00
{
2023-12-05 18:38:10 -05:00
return Insert ( uid , insertEnt , out stackedEntity , out _ , user : user , storageComp : storageComp , playSound : playSound , stackAutomatically : stackAutomatically ) ;
2023-10-30 23:55:55 -04:00
}
/// <summary>
/// Inserts into the storage container
/// </summary>
2023-11-13 23:43:03 +11:00
/// <returns>true if the entity was inserted, false otherwise. This will also return true if a stack was partially
/// inserted</returns>
2023-10-30 23:55:55 -04:00
public bool Insert (
EntityUid uid ,
EntityUid insertEnt ,
out EntityUid ? stackedEntity ,
out string? reason ,
EntityUid ? user = null ,
StorageComponent ? storageComp = null ,
2023-12-05 18:38:10 -05:00
bool playSound = true ,
bool stackAutomatically = true )
2023-09-11 21:20:46 +10:00
{
2023-09-12 22:34:04 +10:00
stackedEntity = null ;
2023-10-30 23:55:55 -04:00
reason = null ;
2023-09-12 22:34:04 +10:00
2023-11-13 23:43:03 +11:00
if ( ! Resolve ( uid , ref storageComp ) )
2023-09-11 21:20:46 +10:00
return false ;
/ *
* 1. If the inserted thing is stackable then try to stack it to existing stacks
* 2. If anything remains insert whatever is possible .
* 3. If insertion is not possible then leave the stack as is .
* At either rate still play the insertion sound
*
* For now we just treat items as always being the same size regardless of stack count .
* /
2025-03-27 18:42:57 +01:00
// Check if the sound is expected to play.
// If there is an user, the sound will not play if they have the SilentStorageUserTag
// If there is no user, only playSound is checked.
var canPlaySound = playSound & & ( user = = null | | ! _tag . HasTag ( user . Value , storageComp . SilentStorageUserTag ) ) ;
2023-12-05 18:38:10 -05:00
if ( ! stackAutomatically | | ! _stackQuery . TryGetComponent ( insertEnt , out var insertStack ) )
2023-09-11 21:20:46 +10:00
{
2025-02-08 17:17:55 +11:00
if ( ! ContainerSystem . Insert ( insertEnt , storageComp . Container ) )
2023-11-13 23:43:03 +11:00
return false ;
2023-09-11 21:20:46 +10:00
2025-03-27 18:42:57 +01:00
if ( canPlaySound )
2024-07-22 02:54:15 -07:00
Audio . PlayPredicted ( storageComp . StorageInsertSound , uid , user , _audioParams ) ;
2023-09-11 21:20:46 +10:00
2023-11-13 23:43:03 +11:00
return true ;
}
2023-09-11 21:20:46 +10:00
2023-11-13 23:43:03 +11:00
var toInsertCount = insertStack . Count ;
2023-09-11 21:20:46 +10:00
2023-11-13 23:43:03 +11:00
foreach ( var ent in storageComp . Container . ContainedEntities )
{
if ( ! _stackQuery . TryGetComponent ( ent , out var containedStack ) )
continue ;
2023-09-11 21:20:46 +10:00
2023-11-13 23:43:03 +11:00
if ( ! _stack . TryAdd ( insertEnt , ent , insertStack , containedStack ) )
continue ;
2023-11-01 19:19:41 -04:00
2023-11-13 23:43:03 +11:00
stackedEntity = ent ;
if ( insertStack . Count = = 0 )
break ;
2023-09-11 21:20:46 +10:00
}
2023-11-13 23:43:03 +11:00
// Still stackable remaining
if ( insertStack . Count > 0
2025-02-08 17:17:55 +11:00
& & ! ContainerSystem . Insert ( insertEnt , storageComp . Container )
2023-11-13 23:43:03 +11:00
& & toInsertCount = = insertStack . Count )
2023-09-11 21:20:46 +10:00
{
2023-11-13 23:43:03 +11:00
// Failed to insert anything.
2023-09-11 21:20:46 +10:00
return false ;
}
2025-03-27 18:42:57 +01:00
if ( canPlaySound )
2024-07-22 02:54:15 -07:00
Audio . PlayPredicted ( storageComp . StorageInsertSound , uid , user , _audioParams ) ;
2023-09-11 21:20:46 +10:00
return true ;
}
/// <summary>
/// Inserts an entity into storage from the player's active hand
/// </summary>
2024-08-10 19:29:44 -07:00
/// <param name="ent">The storage entity and component to insert into.</param>
/// <param name="player">The player and hands component to insert the held entity from.</param>
/// <returns>True if inserted, otherwise false.</returns>
public bool PlayerInsertHeldEntity ( Entity < StorageComponent ? > ent , Entity < HandsComponent ? > player )
2023-09-11 21:20:46 +10:00
{
2024-08-10 19:29:44 -07:00
if ( ! Resolve ( ent . Owner , ref ent . Comp )
| | ! Resolve ( player . Owner , ref player . Comp )
| | player . Comp . ActiveHandEntity = = null )
2023-09-11 21:20:46 +10:00
return false ;
2024-08-10 19:29:44 -07:00
var toInsert = player . Comp . ActiveHandEntity ;
2023-09-11 21:20:46 +10:00
2024-08-10 19:29:44 -07:00
if ( ! CanInsert ( ent , toInsert . Value , out var reason , ent . Comp ) )
2023-09-11 21:20:46 +10:00
{
2024-08-10 19:29:44 -07:00
_popupSystem . PopupClient ( Loc . GetString ( reason ? ? "comp-storage-cant-insert" ) , ent , player ) ;
2023-09-11 21:20:46 +10:00
return false ;
}
2024-08-10 19:29:44 -07:00
if ( ! _sharedHandsSystem . CanDrop ( player , toInsert . Value , player . Comp ) )
2023-09-11 21:20:46 +10:00
{
2024-08-10 19:29:44 -07:00
_popupSystem . PopupClient ( Loc . GetString ( "comp-storage-cant-drop" , ( "entity" , toInsert . Value ) ) , ent , player ) ;
2023-09-11 21:20:46 +10:00
return false ;
}
2024-08-10 19:29:44 -07:00
return PlayerInsertEntityInWorld ( ( ent , ent . Comp ) , player , toInsert . Value ) ;
2023-09-11 21:20:46 +10:00
}
/// <summary>
/// Inserts an Entity (<paramref name="toInsert"/>) in the world into storage, informing <paramref name="player"/> if it fails.
2024-08-10 19:29:44 -07:00
/// <paramref name="toInsert"/> is *NOT* held, see <see cref="PlayerInsertHeldEntity(Entity{StorageComponent?},Entity{HandsComponent?})"/>.
2023-09-11 21:20:46 +10:00
/// </summary>
2023-10-30 23:55:55 -04:00
/// <param name="uid"></param>
2023-09-11 21:20:46 +10:00
/// <param name="player">The player to insert an entity with</param>
2023-10-30 23:55:55 -04:00
/// <param name="toInsert"></param>
2023-09-11 21:20:46 +10:00
/// <returns>true if inserted, false otherwise</returns>
2024-09-27 17:09:17 +10:00
public bool PlayerInsertEntityInWorld ( Entity < StorageComponent ? > uid , EntityUid player , EntityUid toInsert , bool playSound = true )
2023-09-11 21:20:46 +10:00
{
2024-05-24 17:03:03 +12:00
if ( ! Resolve ( uid , ref uid . Comp ) | | ! _interactionSystem . InRangeUnobstructed ( player , uid . Owner ) )
2023-09-11 21:20:46 +10:00
return false ;
2024-09-27 17:09:17 +10:00
if ( ! Insert ( uid , toInsert , out _ , user : player , uid . Comp , playSound : playSound ) )
2023-09-11 21:20:46 +10:00
{
_popupSystem . PopupClient ( Loc . GetString ( "comp-storage-cant-insert" ) , uid , player ) ;
return false ;
}
return true ;
}
2023-09-12 22:34:04 +10:00
2023-10-30 23:55:55 -04:00
/// <summary>
2023-12-04 18:04:39 -05:00
/// Attempts to set the location of an item already inside of a storage container.
2023-10-30 23:55:55 -04:00
/// </summary>
2023-12-04 18:04:39 -05:00
public bool TrySetItemStorageLocation ( Entity < ItemComponent ? > itemEnt , Entity < StorageComponent ? > storageEnt , ItemStorageLocation location )
2023-10-30 23:55:55 -04:00
{
2023-12-04 18:04:39 -05:00
if ( ! Resolve ( itemEnt , ref itemEnt . Comp ) | | ! Resolve ( storageEnt , ref storageEnt . Comp ) )
return false ;
if ( ! storageEnt . Comp . Container . ContainedEntities . Contains ( itemEnt ) )
return false ;
if ( ! ItemFitsInGridLocation ( itemEnt , storageEnt , location . Position , location . Rotation ) )
return false ;
2024-01-09 21:47:51 +11:00
storageEnt . Comp . StoredItems [ itemEnt ] = location ;
2025-02-08 17:17:55 +11:00
UpdateUI ( storageEnt ) ;
2023-12-04 18:04:39 -05:00
Dirty ( storageEnt , storageEnt . Comp ) ;
return true ;
}
/// <summary>
/// Tries to find the first available spot on a storage grid.
/// starts at the top-left and goes right and down.
/// </summary>
public bool TryGetAvailableGridSpace (
Entity < StorageComponent ? > storageEnt ,
Entity < ItemComponent ? > itemEnt ,
[NotNullWhen(true)] out ItemStorageLocation ? storageLocation )
{
storageLocation = null ;
if ( ! Resolve ( storageEnt , ref storageEnt . Comp ) | | ! Resolve ( itemEnt , ref itemEnt . Comp ) )
return false ;
2024-03-28 06:31:47 +00:00
// if the item has an available saved location, use that
if ( FindSavedLocation ( storageEnt , itemEnt , out storageLocation ) )
return true ;
2023-12-04 18:04:39 -05:00
var storageBounding = storageEnt . Comp . Grid . GetBoundingBox ( ) ;
2024-01-05 22:19:01 -05:00
Angle startAngle ;
if ( storageEnt . Comp . DefaultStorageOrientation = = null )
{
startAngle = Angle . FromDegrees ( - itemEnt . Comp . StoredRotation ) ;
}
else
{
if ( storageBounding . Width < storageBounding . Height )
{
startAngle = storageEnt . Comp . DefaultStorageOrientation = = StorageDefaultOrientation . Horizontal
? Angle . Zero
: Angle . FromDegrees ( 90 ) ;
}
else
{
startAngle = storageEnt . Comp . DefaultStorageOrientation = = StorageDefaultOrientation . Vertical
? Angle . Zero
: Angle . FromDegrees ( 90 ) ;
}
}
2023-12-04 18:04:39 -05:00
for ( var y = storageBounding . Bottom ; y < = storageBounding . Top ; y + + )
{
for ( var x = storageBounding . Left ; x < = storageBounding . Right ; x + + )
{
2024-01-05 22:19:01 -05:00
for ( var angle = startAngle ; angle < = Angle . FromDegrees ( 360 - startAngle ) ; angle + = Math . PI / 2f )
2023-12-04 18:04:39 -05:00
{
var location = new ItemStorageLocation ( angle , ( x , y ) ) ;
if ( ItemFitsInGridLocation ( itemEnt , storageEnt , location ) )
{
storageLocation = location ;
return true ;
}
}
}
}
return false ;
}
2024-03-28 06:31:47 +00:00
/// <summary>
/// Tries to find a saved location for an item from its name.
/// If none are saved or they are all blocked nothing is returned.
/// </summary>
public bool FindSavedLocation (
Entity < StorageComponent ? > ent ,
Entity < ItemComponent ? > item ,
[NotNullWhen(true)] out ItemStorageLocation ? storageLocation )
{
storageLocation = null ;
if ( ! Resolve ( ent , ref ent . Comp ) )
return false ;
var name = Name ( item ) ;
if ( ! ent . Comp . SavedLocations . TryGetValue ( name , out var list ) )
return false ;
foreach ( var location in list )
{
if ( ItemFitsInGridLocation ( item , ent , location ) )
{
storageLocation = location ;
return true ;
}
}
return false ;
}
/// <summary>
/// Saves an item's location in the grid for later insertion to use.
/// </summary>
public void SaveItemLocation ( Entity < StorageComponent ? > ent , Entity < MetaDataComponent ? > item )
{
if ( ! Resolve ( ent , ref ent . Comp ) )
return ;
// needs to actually be stored in it somewhere to save it
if ( ! ent . Comp . StoredItems . TryGetValue ( item , out var location ) )
return ;
var name = Name ( item , item . Comp ) ;
if ( ent . Comp . SavedLocations . TryGetValue ( name , out var list ) )
{
// iterate to make sure its not already been saved
for ( int i = 0 ; i < list . Count ; i + + )
{
var saved = list [ i ] ;
2024-04-18 20:24:13 -07:00
2024-03-28 06:31:47 +00:00
if ( saved = = location )
{
list . Remove ( location ) ;
return ;
}
}
list . Add ( location ) ;
}
else
{
list = new List < ItemStorageLocation > ( )
{
location
} ;
ent . Comp . SavedLocations [ name ] = list ;
}
Dirty ( ent , ent . Comp ) ;
2025-02-08 17:17:55 +11:00
UpdateUI ( ( ent . Owner , ent . Comp ) ) ;
2024-03-28 06:31:47 +00:00
}
2023-12-04 18:04:39 -05:00
/// <summary>
/// Checks if an item fits into a specific spot on a storage grid.
/// </summary>
public bool ItemFitsInGridLocation (
Entity < ItemComponent ? > itemEnt ,
Entity < StorageComponent ? > storageEnt ,
ItemStorageLocation location )
{
return ItemFitsInGridLocation ( itemEnt , storageEnt , location . Position , location . Rotation ) ;
}
/// <summary>
/// Checks if an item fits into a specific spot on a storage grid.
/// </summary>
public bool ItemFitsInGridLocation (
Entity < ItemComponent ? > itemEnt ,
Entity < StorageComponent ? > storageEnt ,
Vector2i position ,
Angle rotation )
{
if ( ! Resolve ( itemEnt , ref itemEnt . Comp ) | | ! Resolve ( storageEnt , ref storageEnt . Comp ) )
2023-10-30 23:55:55 -04:00
return false ;
2023-12-04 18:04:39 -05:00
var gridBounds = storageEnt . Comp . Grid . GetBoundingBox ( ) ;
if ( ! gridBounds . Contains ( position ) )
return false ;
var itemShape = ItemSystem . GetAdjustedItemShape ( itemEnt , rotation , position ) ;
foreach ( var box in itemShape )
2023-10-30 23:55:55 -04:00
{
2023-12-04 18:04:39 -05:00
for ( var offsetY = box . Bottom ; offsetY < = box . Top ; offsetY + + )
{
for ( var offsetX = box . Left ; offsetX < = box . Right ; offsetX + + )
{
var pos = ( offsetX , offsetY ) ;
if ( ! IsGridSpaceEmpty ( itemEnt , storageEnt , pos ) )
return false ;
}
}
2023-11-01 19:19:41 -04:00
}
2023-12-04 18:04:39 -05:00
return true ;
}
/// <summary>
/// Checks if a space on a grid is valid and not occupied by any other pieces.
/// </summary>
public bool IsGridSpaceEmpty ( Entity < ItemComponent ? > itemEnt , Entity < StorageComponent ? > storageEnt , Vector2i location )
{
if ( ! Resolve ( storageEnt , ref storageEnt . Comp ) )
return false ;
var validGrid = false ;
foreach ( var grid in storageEnt . Comp . Grid )
{
if ( grid . Contains ( location ) )
{
validGrid = true ;
break ;
}
}
if ( ! validGrid )
return false ;
2024-01-09 21:47:51 +11:00
foreach ( var ( ent , storedItem ) in storageEnt . Comp . StoredItems )
2023-12-04 18:04:39 -05:00
{
if ( ent = = itemEnt . Owner )
continue ;
if ( ! _itemQuery . TryGetComponent ( ent , out var itemComp ) )
continue ;
var adjustedShape = ItemSystem . GetAdjustedItemShape ( ( ent , itemComp ) , storedItem ) ;
foreach ( var box in adjustedShape )
{
if ( box . Contains ( location ) )
return false ;
}
}
return true ;
}
/// <summary>
/// Returns true if there is enough space to theoretically fit another item.
/// </summary>
public bool HasSpace ( Entity < StorageComponent ? > uid )
{
if ( ! Resolve ( uid , ref uid . Comp ) )
return false ;
return GetCumulativeItemAreas ( uid ) < uid . Comp . Grid . GetArea ( ) | | HasSpaceInStacks ( uid ) ;
2023-11-01 19:19:41 -04:00
}
private bool HasSpaceInStacks ( Entity < StorageComponent ? > uid , string? stackType = null )
{
if ( ! Resolve ( uid , ref uid . Comp ) )
return false ;
foreach ( var contained in uid . Comp . Container . ContainedEntities )
{
if ( ! _stackQuery . TryGetComponent ( contained , out var stack ) )
continue ;
if ( stackType ! = null & & ! stack . StackTypeId . Equals ( stackType ) )
continue ;
if ( _stack . GetAvailableSpace ( stack ) = = 0 )
continue ;
2023-10-30 23:55:55 -04:00
2023-11-01 19:19:41 -04:00
return true ;
2023-10-30 23:55:55 -04:00
}
2023-11-01 19:19:41 -04:00
return false ;
2023-10-30 23:55:55 -04:00
}
/// <summary>
/// Returns the sum of all the ItemSizes of the items inside of a storage.
/// </summary>
2023-12-04 18:04:39 -05:00
public int GetCumulativeItemAreas ( Entity < StorageComponent ? > entity )
2023-10-30 23:55:55 -04:00
{
2023-12-04 18:04:39 -05:00
if ( ! Resolve ( entity , ref entity . Comp ) )
2023-10-30 23:55:55 -04:00
return 0 ;
var sum = 0 ;
2023-12-04 18:04:39 -05:00
foreach ( var item in entity . Comp . Container . ContainedEntities )
2023-10-30 23:55:55 -04:00
{
if ( ! _itemQuery . TryGetComponent ( item , out var itemComp ) )
continue ;
2023-12-04 18:04:39 -05:00
sum + = ItemSystem . GetItemShape ( ( item , itemComp ) ) . GetArea ( ) ;
2023-10-30 23:55:55 -04:00
}
return sum ;
}
2023-12-27 17:50:49 -05:00
public ItemSizePrototype GetMaxItemSize ( Entity < StorageComponent ? > uid )
2023-10-30 23:55:55 -04:00
{
if ( ! Resolve ( uid , ref uid . Comp ) )
2023-12-27 17:50:49 -05:00
return _defaultStorageMaxItemSize ;
2023-10-30 23:55:55 -04:00
// If we specify a max item size, use that
if ( uid . Comp . MaxItemSize ! = null )
2024-06-20 20:16:43 -04:00
{
if ( _prototype . TryIndex ( uid . Comp . MaxItemSize . Value , out var proto ) )
return proto ;
Log . Error ( $"{ToPrettyString(uid.Owner)} tried to get invalid item size prototype: {uid.Comp.MaxItemSize.Value}. Stack trace:\\n{Environment.StackTrace}" ) ;
}
2023-10-30 23:55:55 -04:00
if ( ! _itemQuery . TryGetComponent ( uid , out var item ) )
2023-12-27 17:50:49 -05:00
return _defaultStorageMaxItemSize ;
2023-10-30 23:55:55 -04:00
// if there is no max item size specified, the value used
2023-12-27 17:50:49 -05:00
// is one below the item size of the storage entity.
return _nextSmallest [ item . Size ] ;
2023-10-30 23:55:55 -04:00
}
2024-05-09 01:50:50 -05:00
/// <summary>
2024-05-23 18:23:55 -07:00
/// Checks if a storage's UI is open by anyone when locked, and closes it.
2024-05-09 01:50:50 -05:00
/// </summary>
private void OnLockToggled ( EntityUid uid , StorageComponent component , ref LockToggledEvent args )
{
if ( ! args . Locked )
return ;
// Gets everyone looking at the UI
2025-02-08 17:17:55 +11:00
foreach ( var actor in UI . GetActors ( uid , StorageComponent . StorageUiKey . Key ) . ToList ( ) )
2024-05-09 01:50:50 -05:00
{
2024-05-23 18:23:55 -07:00
if ( ! CanInteract ( actor , ( uid , component ) ) )
2025-02-08 17:17:55 +11:00
UI . CloseUi ( uid , StorageComponent . StorageUiKey . Key , actor ) ;
2024-05-09 01:50:50 -05:00
}
}
2023-11-13 23:43:03 +11:00
private void OnStackCountChanged ( EntityUid uid , MetaDataComponent component , StackCountChangedEvent args )
{
2025-02-08 17:17:55 +11:00
if ( ContainerSystem . TryGetContainingContainer ( ( uid , null , component ) , out var container ) & &
2023-11-13 23:43:03 +11:00
container . ID = = StorageComponent . ContainerId )
{
UpdateAppearance ( container . Owner ) ;
UpdateUI ( container . Owner ) ;
}
}
2024-04-19 23:23:45 -07:00
private void HandleOpenBackpack ( ICommonSession ? session )
{
2024-05-12 17:03:07 -07:00
HandleToggleSlotUI ( session , "back" ) ;
2024-04-19 23:23:45 -07:00
}
private void HandleOpenBelt ( ICommonSession ? session )
{
2024-05-12 17:03:07 -07:00
HandleToggleSlotUI ( session , "belt" ) ;
2024-04-19 23:23:45 -07:00
}
2024-05-12 17:03:07 -07:00
private void HandleToggleSlotUI ( ICommonSession ? session , string slot )
2024-04-19 23:23:45 -07:00
{
if ( session is not { } playerSession )
return ;
2024-08-24 03:31:02 +02:00
if ( playerSession . AttachedEntity is not { Valid : true } playerEnt | | ! Exists ( playerEnt ) )
2024-04-19 23:23:45 -07:00
return ;
if ( ! _inventory . TryGetSlotEntity ( playerEnt , slot , out var storageEnt ) )
return ;
if ( ! ActionBlocker . CanInteract ( playerEnt , storageEnt ) )
return ;
2025-02-08 17:17:55 +11:00
if ( ! UI . IsUiOpen ( storageEnt . Value , StorageComponent . StorageUiKey . Key , playerEnt ) )
2024-05-12 17:03:07 -07:00
{
2024-05-23 18:23:55 -07:00
OpenStorageUI ( storageEnt . Value , playerEnt , silent : false ) ;
2024-05-12 17:03:07 -07:00
}
else
{
2025-02-08 17:17:55 +11:00
UI . CloseUi ( storageEnt . Value , StorageComponent . StorageUiKey . Key , playerEnt ) ;
2024-05-12 17:03:07 -07:00
}
2024-04-19 23:23:45 -07:00
}
2024-04-18 20:24:13 -07:00
protected void ClearCantFillReasons ( )
{
#if DEBUG
CantFillReasons . Clear ( ) ;
#endif
}
2024-05-23 18:23:55 -07:00
private bool CanInteract ( EntityUid user , Entity < StorageComponent > storage , bool canInteract = true , bool silent = true )
{
if ( HasComp < BypassInteractionChecksComponent > ( user ) )
return true ;
if ( ! canInteract )
return false ;
var ev = new StorageInteractAttemptEvent ( silent ) ;
RaiseLocalEvent ( storage , ref ev ) ;
return ! ev . Cancelled ;
}
2023-09-12 22:34:04 +10:00
/// <summary>
/// Plays a clientside pickup animation for the specified uid.
/// </summary>
public abstract void PlayPickupAnimation ( EntityUid uid , EntityCoordinates initialCoordinates ,
EntityCoordinates finalCoordinates , Angle initialRotation , EntityUid ? user = null ) ;
2024-01-09 21:47:51 +11:00
2024-08-07 12:15:41 +12:00
private bool ValidateInput (
EntitySessionEventArgs args ,
NetEntity netStorage ,
out Entity < HandsComponent > player ,
out Entity < StorageComponent > storage )
{
player = default ;
storage = default ;
if ( args . SenderSession . AttachedEntity is not { } playerUid )
return false ;
if ( ! TryComp ( playerUid , out HandsComponent ? hands ) | | hands . Count = = 0 )
return false ;
if ( ! TryGetEntity ( netStorage , out var storageUid ) )
return false ;
if ( ! TryComp ( storageUid , out StorageComponent ? storageComp ) )
return false ;
// TODO STORAGE use BUI events
// This would automatically validate that the UI is open & that the user can interact.
// However, we still need to manually validate that items being used are in the users hands or in the storage.
2025-02-08 17:17:55 +11:00
if ( ! UI . IsUiOpen ( storageUid . Value , StorageComponent . StorageUiKey . Key , playerUid ) )
2024-08-07 12:15:41 +12:00
return false ;
if ( ! ActionBlocker . CanInteract ( playerUid , storageUid ) )
return false ;
player = new ( playerUid , hands ) ;
storage = new ( storageUid . Value , storageComp ) ;
return true ;
}
private bool ValidateInput ( EntitySessionEventArgs args ,
NetEntity netStorage ,
NetEntity netItem ,
out Entity < HandsComponent > player ,
out Entity < StorageComponent > storage ,
out Entity < ItemComponent > item ,
bool held = false )
{
item = default ! ;
if ( ! ValidateInput ( args , netStorage , out player , out storage ) )
return false ;
if ( ! TryGetEntity ( netItem , out var itemUid ) )
return false ;
if ( held )
{
if ( ! _sharedHandsSystem . IsHolding ( player , itemUid , out _ ) )
return false ;
}
else
{
if ( ! storage . Comp . Container . Contains ( itemUid . Value ) )
return false ;
DebugTools . Assert ( storage . Comp . StoredItems . ContainsKey ( itemUid . Value ) ) ;
}
if ( ! TryComp ( itemUid , out ItemComponent ? itemComp ) )
return false ;
if ( ! ActionBlocker . CanInteract ( player , itemUid ) )
return false ;
item = new ( itemUid . Value , itemComp ) ;
return true ;
}
2024-01-09 21:47:51 +11:00
[Serializable, NetSerializable]
protected sealed class StorageComponentState : ComponentState
{
public Dictionary < NetEntity , ItemStorageLocation > StoredItems = new ( ) ;
2024-03-28 06:31:47 +00:00
public Dictionary < string , List < ItemStorageLocation > > SavedLocations = new ( ) ;
2024-01-09 21:47:51 +11:00
public List < Box2i > Grid = new ( ) ;
public ProtoId < ItemSizePrototype > ? MaxItemSize ;
2024-05-25 13:07:37 -07:00
public EntityWhitelist ? Whitelist ;
public EntityWhitelist ? Blacklist ;
2024-01-09 21:47:51 +11:00
}
2023-09-11 21:20:46 +10:00
}