Files
crystall-punk-14/Content.Shared/Storage/EntitySystems/SharedStorageSystem.cs
Ed 50470c3aaa Ed 09 06 2024 upstream (#230)
* Revert "Buff the AME until somebody fixes engineering" (#28419)

* Automatic changelog update

* hand teleport portals now may start in the same grid. (#28556)

* Microwave recipes now uses stacktype id instead of entity prototype id for stacked entities (#28225)

* Automatic changelog update

* Add "fill level" sprites to mops and damp rag (#28590)

* Automatic changelog update

* Reword some criminal records text (#28597)

* Update criminal-records.ftl

* Update criminal-records.ftl

* Update criminal-records.ftl

* Remove obsolete VisibilitySystem functions (#28610)

Remove obsolete visibility functions

Co-authored-by: plykiya <plykiya@protonmail.com>

* Prayable datafield typo (#28622)

* notifiactionPrefix -> notificationPrefix

* notifiactionPrefix -> notificationPrefix

* Update engine to v224.1.0 (#28624)

* Use dummy sessions in NukeOpsTest (#28549)

* Add dummy sessions

* Update NukeOpsTest

* Fix PvsBenchmark

* Update engine to v224.1.1 (#28632)

* Add Job preference tests (#28625)

* Misc Job related changes

* Add JobTest

* A

* Aa

* Lets not confuse the yaml linter

* fixes

* a

* Add logs that provide session to player admin logs (#28628)

* Cluster Update (#28627)

done here

* Automatic changelog update

* minor banner changes (#28636)

* minor banner changes

* Uhrmm actchually it's you're, not your

* Update banners.yml

props Hyenh

* Revenant spell catalog locale (#28638)

locale

* Make cuff default range again (#28576)

* Make cuff default range again

* uncuff distance

* how about ONE

---------

Co-authored-by: plykiya <plykiya@protonmail.com>

* Automatic changelog update

* Machine-code cleanup (#28489)

* Make Projectiles Only Hit a Variety of Station Objects Unless Clicked on (#28571)

* Automatic changelog update

* Fix Smoke-grenade.ogg not being mono (#28593)

* Fix Cigars Sprites + YAML fix for Inhand unlit cigars/cigs (#28641)

* Automatic changelog update

* Clean up Eva and Hardsuit helm yml + Lets atmos firesuit helm work as a BreathMask (#28602)

* Shifts borgs hats to the right a bit (#28600)

* Fix mouse inhands (#28623)

* Adjust some touch reaction damage levels (#28591)

* Automatic changelog update

* Add closing storage UIs to StorageInteractionTest (#28633)

* Update rules (#28452)

* Add support for LocalizedDatasets to RandomMetadata (#28601)

* Fix Admin Object tab sorting and search (#28609)

* Automatic changelog update

* Internals are kept on as long as any breathing tool is on (#28595)

* Automatic changelog update

* Convert rules to use guidebook parsing (#28647)

* Gives Insulation and NoSlip to all bots (#28621)

* Gives Insulation and NoSlip to all bots

* remove NoSlip from children

* Automatic changelog update

* Nerfs welderbombing (#28650)

nerf welderbombing

* Automatic changelog update

* Give jobs & antags prototypes a guide field (#28614)

* Give jobs & antags prototypes a guide field

* A

* space

Co-authored-by: ShadowCommander <10494922+ShadowCommander@users.noreply.github.com>

* Add todo

* Fix merge errors

---------

Co-authored-by: ShadowCommander <10494922+ShadowCommander@users.noreply.github.com>

* Automatic changelog update

* Fix typo in Space Law's restricted gear (#28668)

This typo was included in the wiki as well.

Reported-by: Bobberson in the Discord

* Automatic changelog update

* fixed "silicones" typo in new rules (#28671)

fixed all appearances of silicone where it should have been silicon

* fix janitor not spawning with a survival box (#28669)

hi

* Automatic changelog update

* Fix typo in slime naming conventions (#28682)

* Flatpacker fixes (#28417)

* Return medicine recipe solid material costs to 1 (#28679)

Set material costs of medicine recipes to 1

* Automatic changelog update

* Update Core (#28689)

add

* Fixed the guidebook listing every single rule (#28680)

* Well i tried this way

* New approach (start)

* Did it

* makes spacelaw available, put it under sec

* Automatic changelog update

* rule fixes first pass (#28701)

update

* Add JobRequirementOverride prototypes (#28607)

* Add JobRequirementOverride prototypes

* a

* invert if

* Add override that takes in prototypes directly

* Tweak chapel salvage wreck (#28703)

add

* Fixes client having authority over rules popup cvars (#28655)

* Fixes client having authority over rules popup cvars

* Delete duplicate migration

* Pre-update

* Post-update

* Add locale support for booze and soda jugs labels (#28708)

* Swap some InRangeUnobstructed for InRangeUnoccluded (#28706)

Swap InRangeUnobstructed to InRangeUnoccluded

Co-authored-by: plykiya <plykiya@protonmail.com>

* Automatic changelog update

* Ports the singularity's values from vgstation (#28720)

* ports the singularity values from vgstation

* guidebook fix

* 5000 energy level 6 singulo

* Fix action icons when dragging an active action to another slot (#28692)

* Add DoPopup data field to OnUseTimerTrigger (#28691)

* Dropping Corpses Devoured by Space Dragons on Gib/Butcher. (#28709)

* Update DevourSystem.cs

* Update DevourSystem.cs

* Update Content.Server/Devour/DevourSystem.cs

---------

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>

* Automatic changelog update

* Improve grammar of various petting messages (#28715)

Improve grammar of various petting message

* Fix DamageOtherOnHit.OnDoHit when the target is terminating or deleted (#28690)

* Update Core (#28735)

add

* Fix flatpacker (#28736)

* Fix flatpacker

* a

* rn, atp (#28674)

* Update speech-chatsan.ftl

* Update word_replacements.yml

* Atm-at the moment

* Atm

* Update Resources/Locale/en-US/speech/speech-chatsan.ftl

* Update Resources/Prototypes/Accents/word_replacements.yml

---------

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>

* Add loadout group check for role proto (#28731)

MFW same bug twice at 2 layers because I'm stupid.

* Automatic changelog update

* Try fix RGBee (#28741)

* Automatic changelog update

* Ensure packager creates a release folder (#28426)

I was screaming at the github actions runner before i noticed something i had done before caused me to never have a release folder and thus fail.

* allow ' in character names (#28652)

* Automatic changelog update

* Make freeze, freeze & mute, and unmute verbs work on disconnected players. (#28664)

* Automatic changelog update

* fix singulo decay (#28743)

* Automatic changelog update

* Don't use invalid defaults for loadouts (#28729)

At the time it made more sense but now with species specific stuff it's better to have nothing.

* Maybe fix invalid loadout prototypes (#28734)

* Maybe fix invalid loadout prototypes

So if we have existing data SetDefault is not normally called iirc. So what I think is happening is that if we have old loadout groups that get saved to DB and loaded these get dropped entirely and nothing is used to replace the group unless the person specifically looks at their loadout.

Need someone affected to send me their loadout to confirm it's fixed.

* Better fix

* Automatic changelog update

* Fix null ref exception in PrayerSystem (#28712)

* Fix null ref exception in PrayerSystem

* Also check that prayable ent/comp still exist

* Guidebook Updates for the Amateur Spessman (#28603)

* Created NewPlayer.xml

* Created NewPlayer.yml and added it to guides.ftl

* shifted some controls from Space Station 14 to New? Start here!, as well as made a character creation xml to be written later

* switched some stuff between the New? entry and the Space Station 14 entry

* Made everything so nice!!!!!!!!!!!!!!

* fixed formatting inconsistencies

* added a How to use this guidebook section

* build correction and guidebook clarification for other servers

* wrote character creation ig probs fo shizzle

* added new terms to the glossary and alphabetized it

* meh this seems important enough to add

* okay no more shitsec bad idea

* I HATED Roleplaying.xml ANYWAY!!!

* I REALLY REALLY HATED IT ACTUALLYLLL

* Moved Controls and Radio into newplayer.yml, making meta.yml and radio.yml obsolete

* Separated the character creation bits that are just cosmetic from the ones that matter

* also put all the related new player xml files in their own folder

* expanded Radio.xml, kinda fixed survival.xml

* removed the line that mighta maybe sorta possibly could encourage self antag

* thought about this randomly but ICK OCK

* talking is no longer a key part of this game

* moves stuff around, a lot of stuff. basically moves everything but the jobs themselves around

* ah probably should make sure this works first also me when i lie on the internet

* don't be such a grammar nukie

Co-authored-by: Tayrtahn <tayrtahn@gmail.com>

* okay nevermind that's justified

Co-authored-by: Tayrtahn <tayrtahn@gmail.com>

* prepare. it is coming. the great reorganization...

* yesyes that's all well and goo- THE REORG IS COMING. THE REORG IS COMING. WAKE UP.

* rename that real quick

* step one begins. first, consolidate existing service entries into their own yaml. this makes botany.yml obsolete.

* update shiftandcrew.yml to only have departments as children also fuck it alphabetization

* consolidated salvage into cargo

* made a new entry for command

* gave salvage a home and service an existence

* made some XML files to be filled out later

* quick rename...

* took some stuff from Intro.txt and i think Gameplay.txt and put it in NewPlayer.xml

* The Great Writing about Departments (25XX, black and white)

* added a bunch of links everywhere

* biochemical is no longer a thing

* service formatinaaaaaaa

* shiny...,,,,,,,,, colo(u)rz..,,,,,,,,,,,,,

* let's get that fixed

* second time i made a typo there as well

* we hate fun around here

* grammar?!???/

* various fixes and more linkings

* oops

* wewlad lol!!

---------

Co-authored-by: Tayrtahn <tayrtahn@gmail.com>
Co-authored-by: AJCM <AJCM@tutanota.com>

* Automatic changelog update

* Add suffixes to excap survival boxes (#28755)

Update emergency.yml

* Add Jani lobby background (#28724)

* Add jani lobby background

* Actually make new lobby screen functional

* Fix license

* Automatic changelog update

* Strip markdown from silicon laws before saying them (#28596)

* saltern update (#28773)

Co-authored-by: deltanedas <@deltanedas:kde.org>

* revert Tornado regex

* Update HumanoidCharacterProfile.cs

* Update arenas.yml

* test

* fix locale

* Update options-menu.ftl

* Update species-names.ftl

* Update debug.yml

* aaaaaa

---------

Co-authored-by: Moony <moony@hellomouse.net>
Co-authored-by: PJBot <pieterjan.briers+bot@gmail.com>
Co-authored-by: icekot8 <93311212+icekot8@users.noreply.github.com>
Co-authored-by: blueDev2 <89804215+blueDev2@users.noreply.github.com>
Co-authored-by: Tayrtahn <tayrtahn@gmail.com>
Co-authored-by: TsjipTsjip <19798667+TsjipTsjip@users.noreply.github.com>
Co-authored-by: Plykiya <58439124+Plykiya@users.noreply.github.com>
Co-authored-by: plykiya <plykiya@protonmail.com>
Co-authored-by: Leon Friedrich <60421075+ElectroJr@users.noreply.github.com>
Co-authored-by: ShadowCommander <10494922+ShadowCommander@users.noreply.github.com>
Co-authored-by: Boaz1111 <149967078+Boaz1111@users.noreply.github.com>
Co-authored-by: Hmeister-real <118129069+Hmeister-real@users.noreply.github.com>
Co-authored-by: lapatison <100279397+lapatison@users.noreply.github.com>
Co-authored-by: Nemanja <98561806+EmoGarbage404@users.noreply.github.com>
Co-authored-by: Cojoke <83733158+Cojoke-dot@users.noreply.github.com>
Co-authored-by: DrSmugleaf <10968691+DrSmugleaf@users.noreply.github.com>
Co-authored-by: Ps3Moira <113228053+ps3moira@users.noreply.github.com>
Co-authored-by: Verm <32827189+Vermidia@users.noreply.github.com>
Co-authored-by: Chief-Engineer <119664036+Chief-Engineer@users.noreply.github.com>
Co-authored-by: Repo <47093363+Titian3@users.noreply.github.com>
Co-authored-by: Flareguy <78941145+Flareguy@users.noreply.github.com>
Co-authored-by: Thomas <87614336+Aeshus@users.noreply.github.com>
Co-authored-by: Moomoobeef <62638182+Moomoobeef@users.noreply.github.com>
Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com>
Co-authored-by: Ubaser <134914314+UbaserB@users.noreply.github.com>
Co-authored-by: AJCM-git <60196617+AJCM-git@users.noreply.github.com>
Co-authored-by: lzk <124214523+lzk228@users.noreply.github.com>
Co-authored-by: Lyndomen <49795619+Lyndomen@users.noreply.github.com>
Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
Co-authored-by: MerrytheManokit <167581110+MerrytheManokit@users.noreply.github.com>
Co-authored-by: Vasilis <vasilis@pikachu.systems>
Co-authored-by: Whisper <121047731+QuietlyWhisper@users.noreply.github.com>
Co-authored-by: nikthechampiongr <32041239+nikthechampiongr@users.noreply.github.com>
Co-authored-by: UBlueberry <161545003+UBlueberry@users.noreply.github.com>
Co-authored-by: AJCM <AJCM@tutanota.com>
Co-authored-by: Psychpsyo <60073468+Psychpsyo@users.noreply.github.com>
Co-authored-by: deltanedas <39013340+deltanedas@users.noreply.github.com>
2024-06-09 23:53:10 +03:00

1507 lines
53 KiB
C#

using System.Collections.Frozen;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Shared.ActionBlocker;
using Content.Shared.Administration;
using Content.Shared.Administration.Managers;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Destructible;
using Content.Shared.DoAfter;
using Content.Shared.Hands.Components;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Implants.Components;
using Content.Shared.Input;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Components;
using Content.Shared.Inventory;
using Content.Shared.Item;
using Content.Shared.Lock;
using Content.Shared.Materials;
using Content.Shared.Placeable;
using Content.Shared.Popups;
using Content.Shared.Stacks;
using Content.Shared.Storage.Components;
using Content.Shared.Timing;
using Content.Shared.Verbs;
using Content.Shared.Whitelist;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Containers;
using Robust.Shared.GameStates;
using Robust.Shared.Input.Binding;
using Robust.Shared.Map;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
namespace Content.Shared.Storage.EntitySystems;
public abstract class SharedStorageSystem : EntitySystem
{
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] protected readonly IRobustRandom Random = default!;
[Dependency] protected readonly ActionBlockerSystem ActionBlocker = default!;
[Dependency] private readonly EntityLookupSystem _entityLookupSystem = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] protected readonly SharedAudioSystem Audio = default!;
[Dependency] private readonly SharedContainerSystem _containerSystem = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
[Dependency] protected readonly SharedEntityStorageSystem EntityStorage = default!;
[Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
[Dependency] private readonly InventorySystem _inventory = default!;
[Dependency] protected readonly SharedItemSystem ItemSystem = default!;
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
[Dependency] private readonly SharedHandsSystem _sharedHandsSystem = default!;
[Dependency] private readonly SharedStackSystem _stack = default!;
[Dependency] protected readonly SharedTransformSystem TransformSystem = default!;
[Dependency] private readonly SharedUserInterfaceSystem _ui = default!;
[Dependency] protected readonly UseDelaySystem UseDelay = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
private EntityQuery<ItemComponent> _itemQuery;
private EntityQuery<StackComponent> _stackQuery;
private EntityQuery<TransformComponent> _xformQuery;
[ValidatePrototypeId<ItemSizePrototype>]
public const string DefaultStorageMaxItemSize = "Normal";
public const float AreaInsertDelayPerItem = 0.075f;
private ItemSizePrototype _defaultStorageMaxItemSize = default!;
public bool CheckingCanInsert;
private List<EntityUid> _entList = new();
private HashSet<EntityUid> _entSet = new();
private readonly List<ItemSizePrototype> _sortedSizes = new();
private FrozenDictionary<string, ItemSizePrototype> _nextSmallest = FrozenDictionary<string, ItemSizePrototype>.Empty;
private const string QuickInsertUseDelayID = "quickInsert";
private const string OpenUiUseDelayID = "storage";
protected readonly List<string> CantFillReasons = [];
/// <inheritdoc />
public override void Initialize()
{
base.Initialize();
_itemQuery = GetEntityQuery<ItemComponent>();
_stackQuery = GetEntityQuery<StackComponent>();
_xformQuery = GetEntityQuery<TransformComponent>();
_prototype.PrototypesReloaded += OnPrototypesReloaded;
Subs.BuiEvents<StorageComponent>(StorageComponent.StorageUiKey.Key, subs =>
{
subs.Event<BoundUIClosedEvent>(OnBoundUIClosed);
});
SubscribeLocalEvent<StorageComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<StorageComponent, GetVerbsEvent<ActivationVerb>>(AddUiVerb);
SubscribeLocalEvent<StorageComponent, ComponentGetState>(OnStorageGetState);
SubscribeLocalEvent<StorageComponent, ComponentHandleState>(OnStorageHandleState);
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);
SubscribeLocalEvent<StorageComponent, BoundUIOpenedEvent>(OnBoundUIOpen);
SubscribeLocalEvent<StorageComponent, LockToggledEvent>(OnLockToggled);
SubscribeLocalEvent<MetaDataComponent, StackCountChangedEvent>(OnStackCountChanged);
SubscribeLocalEvent<StorageComponent, EntInsertedIntoContainerMessage>(OnEntInserted);
SubscribeLocalEvent<StorageComponent, EntRemovedFromContainerMessage>(OnEntRemoved);
SubscribeLocalEvent<StorageComponent, ContainerIsInsertingAttemptEvent>(OnInsertAttempt);
SubscribeLocalEvent<StorageComponent, AreaPickupDoAfterEvent>(OnDoAfter);
SubscribeAllEvent<StorageInteractWithItemEvent>(OnInteractWithItem);
SubscribeAllEvent<StorageSetItemLocationEvent>(OnSetItemLocation);
SubscribeAllEvent<StorageInsertItemIntoLocationEvent>(OnInsertItemIntoLocation);
SubscribeAllEvent<StorageRemoveItemEvent>(OnRemoveItem);
SubscribeAllEvent<StorageSaveItemLocationEvent>(OnSaveItemLocation);
SubscribeLocalEvent<StorageComponent, GotReclaimedEvent>(OnReclaimed);
CommandBinds.Builder
.Bind(ContentKeyFunctions.OpenBackpack, InputCmdHandler.FromDelegate(HandleOpenBackpack, handle: false))
.Bind(ContentKeyFunctions.OpenBelt, InputCmdHandler.FromDelegate(HandleOpenBelt, handle: false))
.Register<SharedStorageSystem>();
UpdatePrototypeCache();
}
private void OnMapInit(Entity<StorageComponent> entity, ref MapInitEvent args)
{
UseDelay.SetLength(entity.Owner, entity.Comp.QuickInsertCooldown, QuickInsertUseDelayID);
UseDelay.SetLength(entity.Owner, entity.Comp.OpenUiCooldown, OpenUiUseDelayID);
}
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()
{
Grid = new List<Box2i>(component.Grid),
MaxItemSize = component.MaxItemSize,
StoredItems = storedItems,
SavedLocations = component.SavedLocations,
Whitelist = component.Whitelist,
Blacklist = component.Blacklist
};
}
private void OnStorageHandleState(EntityUid uid, StorageComponent component, ref ComponentHandleState args)
{
if (args.Current is not StorageComponentState state)
return;
component.Grid.Clear();
component.Grid.AddRange(state.Grid);
component.MaxItemSize = state.MaxItemSize;
component.Whitelist = state.Whitelist;
component.Blacklist = state.Blacklist;
component.StoredItems.Clear();
foreach (var (nent, location) in state.StoredItems)
{
var ent = EnsureEntity<StorageComponent>(nent, uid);
component.StoredItems[ent] = location;
}
component.SavedLocations = state.SavedLocations;
}
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();
}
private void OnComponentInit(EntityUid uid, StorageComponent storageComp, ComponentInit args)
{
storageComp.Container = _containerSystem.EnsureContainer<Container>(uid, StorageComponent.ContainerId);
UpdateAppearance((uid, storageComp, null));
}
/// <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;
// 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)
{
_ui.CloseUis(entity, actor);
}
}
private void OnBoundUIClosed(EntityUid uid, StorageComponent storageComp, BoundUIClosedEvent args)
{
CloseNestedInterfaces(uid, args.Actor, storageComp);
// If UI is closed for everyone
if (!_ui.IsUiOpen(uid, args.UiKey))
{
UpdateAppearance((uid, storageComp, null));
Audio.PlayPredicted(storageComp.StorageCloseSound, uid, args.Actor);
}
}
private void AddUiVerb(EntityUid uid, StorageComponent component, GetVerbsEvent<ActivationVerb> args)
{
if (!CanInteract(args.User, (uid, component), args.CanAccess && args.CanInteract))
return;
// Does this player currently have the storage UI open?
var uiOpen = _ui.IsUiOpen(uid, StorageComponent.StorageUiKey.Key, args.User);
ActivationVerb verb = new()
{
Act = () =>
{
if (uiOpen)
{
_ui.CloseUi(uid, StorageComponent.StorageUiKey.Key, args.User);
}
else
{
OpenStorageUI(uid, args.User, component);
}
}
};
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);
}
/// <summary>
/// Opens the storage UI for an entity
/// </summary>
/// <param name="entity">The entity to open the UI for</param>
public void OpenStorageUI(EntityUid uid, EntityUid entity, StorageComponent? storageComp = null, bool silent = true)
{
if (!Resolve(uid, ref storageComp, false))
return;
// prevent spamming bag open / honkerton honk sound
silent |= TryComp<UseDelayComponent>(uid, out var useDelay) && UseDelay.IsDelayed((uid, useDelay), id: OpenUiUseDelayID);
if (!CanInteract(entity, (uid, storageComp), silent: silent))
return;
if (!silent)
{
if (!_ui.IsUiOpen(uid, StorageComponent.StorageUiKey.Key))
Audio.PlayPredicted(storageComp.StorageOpenSound, uid, entity);
if (useDelay != null)
UseDelay.TryResetDelay((uid, useDelay), id: OpenUiUseDelayID);
}
_ui.OpenUi(uid, StorageComponent.StorageUiKey.Key, entity);
}
public virtual void UpdateUI(Entity<StorageComponent?> entity) {}
private void AddTransferVerbs(EntityUid uid, StorageComponent component, GetVerbsEvent<UtilityVerb> args)
{
if (!args.CanAccess || !args.CanInteract)
return;
var entities = component.Container.ContainedEntities;
if (entities.Count == 0 || !CanInteract(args.User, (uid, component)))
return;
// if the target is storage, add a verb to transfer storage.
if (TryComp(args.Target, out StorageComponent? targetStorage)
&& (!TryComp(args.Target, out LockComponent? targetLock) || !targetLock.Locked))
{
UtilityVerb verb = new()
{
Text = Loc.GetString("storage-component-transfer-verb"),
IconEntity = GetNetEntity(args.Using),
Act = () => TransferEntities(uid, args.Target, args.User, component, null, targetStorage, targetLock)
};
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)
{
if (args.Handled || !CanInteract(args.User, (uid, storageComp), storageComp.ClickInsert, false))
return;
if (HasComp<PlaceableSurfaceComponent>(uid))
return;
if (_whitelistSystem.IsWhitelistPass(storageComp.CP14Ignorelist, args.Used))
return;
PlayerInsertHeldEntity(uid, args.User, storageComp);
// Always handle it, even if insertion fails.
// We don't want to trigger any AfterInteract logic here.
// Example issue would be placing wires if item doesn't fit in backpack.
args.Handled = true;
}
/// <summary>
/// Sends a message to open the storage UI
/// </summary>
private void OnActivate(EntityUid uid, StorageComponent storageComp, ActivateInWorldEvent args)
{
if (args.Handled || !args.Complex || !CanInteract(args.User, (uid, storageComp), storageComp.ClickInsert))
return;
// Toggle
if (_ui.IsUiOpen(uid, StorageComponent.StorageUiKey.Key, args.User))
{
_ui.CloseUi(uid, StorageComponent.StorageUiKey.Key, args.User);
}
else
{
OpenStorageUI(uid, args.User, storageComp, false);
}
args.Handled = true;
}
/// <summary>
/// Specifically for storage implants.
/// </summary>
private void OnImplantActivate(EntityUid uid, StorageComponent storageComp, OpenStorageImplantEvent args)
{
if (args.Handled)
return;
OpenStorageUI(uid, args.Performer, storageComp, false);
args.Handled = true;
}
/// <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)
{
if (args.Handled || !args.CanReach || !UseDelay.TryResetDelay(uid, checkDelayed: true, id: QuickInsertUseDelayID))
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)))
{
_entList.Clear();
_entSet.Clear();
_entityLookupSystem.GetEntitiesInRange(args.ClickLocation, storageComp.AreaInsertRadius, _entSet, LookupFlags.Dynamic | LookupFlags.Sundries);
var delay = 0f;
foreach (var entity in _entSet)
{
if (entity == args.User
|| !_itemQuery.TryGetComponent(entity, out var itemComp) // Need comp to get item size to get weight
|| !_prototype.TryIndex(itemComp.Size, out var itemSize)
|| !CanInsert(uid, entity, out _, storageComp, item: itemComp)
|| !_interactionSystem.InRangeUnobstructed(args.User, entity))
{
continue;
}
_entList.Add(entity);
delay += itemSize.Weight * AreaInsertDelayPerItem;
if (_entList.Count >= StorageComponent.AreaPickupLimit)
break;
}
//If there's only one then let's be generous
if (_entList.Count > 1)
{
var doAfterArgs = new DoAfterArgs(EntityManager, args.User, delay, new AreaPickupDoAfterEvent(GetNetEntityList(_entList)), uid, target: uid)
{
BreakOnDamage = true,
BreakOnMove = true,
NeedHand = true
};
_doAfterSystem.TryStartDoAfter(doAfterArgs);
args.Handled = true;
}
return;
}
// Pick up the clicked entity
if (storageComp.QuickInsert)
{
if (args.Target is not { Valid: true } target)
return;
if (_containerSystem.IsEntityInContainer(target)
|| target == args.User
|| !_itemQuery.HasComponent(target))
{
return;
}
if (TryComp(uid, out TransformComponent? transformOwner) && TryComp(target, out TransformComponent? transformEnt))
{
var parent = transformOwner.ParentUid;
var position = EntityCoordinates.FromMap(
parent.IsValid() ? parent : uid,
TransformSystem.GetMapCoordinates(transformEnt),
TransformSystem
);
args.Handled = true;
if (PlayerInsertEntityInWorld((uid, storageComp), args.User, target))
{
EntityManager.RaiseSharedEvent(new AnimateInsertingEntitiesEvent(GetNetEntity(uid),
new List<NetEntity> { GetNetEntity(target) },
new List<NetCoordinates> { GetNetCoordinates(position) },
new List<Angle> { transformOwner.LocalRotation }), args.User);
}
}
}
}
private void OnDoAfter(EntityUid uid, StorageComponent component, AreaPickupDoAfterEvent args)
{
if (args.Handled || args.Cancelled)
return;
args.Handled = true;
var successfullyInserted = new List<EntityUid>();
var successfullyInsertedPositions = new List<EntityCoordinates>();
var successfullyInsertedAngles = new List<Angle>();
if (!_xformQuery.TryGetComponent(uid, out var xform))
{
return;
}
var entCount = Math.Min(StorageComponent.AreaPickupLimit, args.Entities.Count);
for (var i = 0; i < entCount; i++)
{
var entity = GetEntity(args.Entities[i]);
// Check again, situation may have changed for some entities, but we'll still pick up any that are valid
if (_containerSystem.IsEntityInContainer(entity)
|| entity == args.Args.User
|| !_itemQuery.HasComponent(entity))
{
continue;
}
if (!_xformQuery.TryGetComponent(entity, out var targetXform) ||
targetXform.MapID != xform.MapID)
{
continue;
}
var position = EntityCoordinates.FromMap(
xform.ParentUid.IsValid() ? xform.ParentUid : uid,
new MapCoordinates(TransformSystem.GetWorldPosition(targetXform), targetXform.MapID),
TransformSystem
);
var angle = targetXform.LocalRotation;
if (PlayerInsertEntityInWorld((uid, component), args.Args.User, entity))
{
successfullyInserted.Add(entity);
successfullyInsertedPositions.Add(position);
successfullyInsertedAngles.Add(angle);
}
}
// If we picked up at least one thing, play a sound and do a cool animation!
if (successfullyInserted.Count > 0)
{
Audio.PlayPredicted(component.StorageInsertSound, uid, args.User);
EntityManager.RaiseSharedEvent(new AnimateInsertingEntitiesEvent(
GetNetEntity(uid),
GetNetEntityList(successfullyInserted),
GetNetCoordinatesList(successfullyInsertedPositions),
successfullyInsertedAngles), args.User);
}
args.Handled = true;
}
private void OnReclaimed(EntityUid uid, StorageComponent storageComp, GotReclaimedEvent args)
{
_containerSystem.EmptyContainer(storageComp.Container, destination: args.ReclaimerCoordinates);
}
private void OnDestroy(EntityUid uid, StorageComponent storageComp, DestructionEventArgs args)
{
var coordinates = TransformSystem.GetMoverCoordinates(uid);
// Being destroyed so need to recalculate.
_containerSystem.EmptyContainer(storageComp.Container, destination: coordinates);
}
/// <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>
private void OnInteractWithItem(StorageInteractWithItemEvent msg, EntitySessionEventArgs args)
{
if (args.SenderSession.AttachedEntity is not { } player)
return;
var uid = GetEntity(msg.StorageUid);
var entity = GetEntity(msg.InteractedItemUid);
if (!TryComp<StorageComponent>(uid, out var storageComp))
return;
if (!_ui.IsUiOpen(uid, StorageComponent.StorageUiKey.Key, player))
return;
if (!Exists(entity))
{
Log.Error($"Player {args.SenderSession} interacted with non-existent item {msg.InteractedItemUid} stored in {ToPrettyString(uid)}");
return;
}
if (!ActionBlocker.CanInteract(player, entity) || !storageComp.Container.Contains(entity))
return;
// Does the player have hands?
if (!TryComp(player, out HandsComponent? hands) || hands.Count == 0)
return;
// If the user's active hand is empty, try pick up the item.
if (hands.ActiveHandEntity == null)
{
if (_sharedHandsSystem.TryPickupAnyHand(player, entity, handsComp: hands)
&& storageComp.StorageRemoveSound != null)
Audio.PlayPredicted(storageComp.StorageRemoveSound, uid, player);
{
return;
}
}
// Else, interact using the held item
_interactionSystem.InteractUsing(player, hands.ActiveHandEntity.Value, entity, Transform(entity).Coordinates, checkCanInteract: false);
}
private void OnSetItemLocation(StorageSetItemLocationEvent msg, EntitySessionEventArgs args)
{
if (args.SenderSession.AttachedEntity is not { } player)
return;
var storageEnt = GetEntity(msg.StorageEnt);
var itemEnt = GetEntity(msg.ItemEnt);
if (!TryComp<StorageComponent>(storageEnt, out var storageComp))
return;
if (!_ui.IsUiOpen(storageEnt, StorageComponent.StorageUiKey.Key, player))
return;
if (!Exists(itemEnt))
{
Log.Error($"Player {args.SenderSession} set location of non-existent item {msg.ItemEnt} stored in {ToPrettyString(storageEnt)}");
return;
}
if (!ActionBlocker.CanInteract(player, itemEnt))
return;
TrySetItemStorageLocation((itemEnt, null), (storageEnt, storageComp), msg.Location);
}
private void OnRemoveItem(StorageRemoveItemEvent msg, EntitySessionEventArgs args)
{
if (args.SenderSession.AttachedEntity is not { } player)
return;
var storageEnt = GetEntity(msg.StorageEnt);
var itemEnt = GetEntity(msg.ItemEnt);
if (!TryComp<StorageComponent>(storageEnt, out var storageComp))
return;
if (!_ui.IsUiOpen(storageEnt, StorageComponent.StorageUiKey.Key, player))
return;
if (!Exists(itemEnt))
{
Log.Error($"Player {args.SenderSession} set location of non-existent item {msg.ItemEnt} stored in {ToPrettyString(storageEnt)}");
return;
}
if (!ActionBlocker.CanInteract(player, itemEnt))
return;
TransformSystem.DropNextTo(itemEnt, player);
Audio.PlayPredicted(storageComp.StorageRemoveSound, storageEnt, player);
}
private void OnInsertItemIntoLocation(StorageInsertItemIntoLocationEvent msg, EntitySessionEventArgs args)
{
if (args.SenderSession.AttachedEntity is not { } player)
return;
var storageEnt = GetEntity(msg.StorageEnt);
var itemEnt = GetEntity(msg.ItemEnt);
if (!TryComp<StorageComponent>(storageEnt, out var storageComp))
return;
if (!_ui.IsUiOpen(storageEnt, StorageComponent.StorageUiKey.Key, player))
return;
if (!Exists(itemEnt))
{
Log.Error($"Player {args.SenderSession} set location of non-existent item {msg.ItemEnt} stored in {ToPrettyString(storageEnt)}");
return;
}
if (!ActionBlocker.CanInteract(player, itemEnt) || !_sharedHandsSystem.IsHolding(player, itemEnt, out _))
return;
InsertAt((storageEnt, storageComp), (itemEnt, null), msg.Location, out _, player, stackAutomatically: false);
}
// TODO: if/when someone cleans up this shitcode please make all these
// handlers use a shared helper for checking that the ui is open etc, thanks
private void OnSaveItemLocation(StorageSaveItemLocationEvent msg, EntitySessionEventArgs args)
{
if (args.SenderSession.AttachedEntity is not {} player)
return;
var storage = GetEntity(msg.Storage);
var item = GetEntity(msg.Item);
if (!HasComp<StorageComponent>(storage))
return;
if (!_ui.IsUiOpen(storage, StorageComponent.StorageUiKey.Key, player))
return;
if (!Exists(item))
{
Log.Error($"Player {args.SenderSession} saved location of non-existent item {msg.Item} stored in {ToPrettyString(storage)}");
return;
}
if (!ActionBlocker.CanInteract(player, item))
return;
SaveItemLocation(storage, item);
}
private void OnBoundUIOpen(EntityUid uid, StorageComponent storageComp, BoundUIOpenedEvent args)
{
UpdateAppearance((uid, storageComp, null));
}
private void OnEntInserted(Entity<StorageComponent> entity, ref EntInsertedIntoContainerMessage args)
{
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
if (entity.Comp.Container == null)
return;
if (args.Container.ID != StorageComponent.ContainerId)
return;
if (!entity.Comp.StoredItems.ContainsKey(args.Entity))
{
if (!TryGetAvailableGridSpace((entity.Owner, entity.Comp), (args.Entity, null), out var location))
{
_containerSystem.Remove(args.Entity, args.Container, force: true);
return;
}
entity.Comp.StoredItems[args.Entity] = location.Value;
Dirty(entity, entity.Comp);
}
UpdateAppearance((entity, entity.Comp, null));
UpdateUI((entity, entity.Comp));
}
private void OnEntRemoved(Entity<StorageComponent> entity, ref EntRemovedFromContainerMessage args)
{
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
if (entity.Comp.Container == null)
return;
if (args.Container.ID != StorageComponent.ContainerId)
return;
entity.Comp.StoredItems.Remove(args.Entity);
Dirty(entity, entity.Comp);
UpdateAppearance((entity, entity.Comp, null));
UpdateUI((entity, entity.Comp));
}
private void OnInsertAttempt(EntityUid uid, StorageComponent component, ContainerIsInsertingAttemptEvent args)
{
if (args.Cancelled || args.Container.ID != StorageComponent.ContainerId)
return;
// don't run cyclical CanInsert() loops
if (CheckingCanInsert)
return;
if (!CanInsert(uid, args.EntityUid, out var reason, component, ignoreStacks: true))
{
#if DEBUG
if (reason != null)
CantFillReasons.Add(reason);
#endif
args.Cancel();
}
}
public void UpdateAppearance(Entity<StorageComponent?, AppearanceComponent?> entity)
{
// TODO STORAGE remove appearance data and just use the data on the component.
var (uid, storage, appearance) = entity;
if (!Resolve(uid, ref storage, ref appearance, false))
return;
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
if (storage.Container == null)
return; // component hasn't yet been initialized.
var capacity = storage.Grid.GetArea();
var used = GetCumulativeItemAreas((uid, storage));
var isOpen = _ui.IsUiOpen(entity.Owner, StorageComponent.StorageUiKey.Key);
_appearance.SetData(uid, StorageVisuals.StorageUsed, used, appearance);
_appearance.SetData(uid, StorageVisuals.Capacity, capacity, appearance);
_appearance.SetData(uid, StorageVisuals.Open, isOpen, appearance);
_appearance.SetData(uid, SharedBagOpenVisuals.BagState, isOpen ? SharedBagState.Open : SharedBagState.Closed, appearance);
_appearance.SetData(uid, StackVisuals.Hide, !isOpen, appearance);
}
/// <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())
{
Insert(target, entity, out _, user: user, targetComp, playSound: false);
}
Audio.PlayPredicted(sourceComp.StorageInsertSound, target, user);
}
/// <summary>
/// Verifies if an entity can be stored and if it fits
/// </summary>
/// <param name="uid">The entity to check</param>
/// <param name="insertEnt"></param>
/// <param name="reason">If returning false, the reason displayed to the player</param>
/// <param name="storageComp"></param>
/// <param name="item"></param>
/// <param name="ignoreStacks"></param>
/// <param name="ignoreLocation"></param>
/// <returns>true if it can be inserted, false otherwise</returns>
public bool CanInsert(
EntityUid uid,
EntityUid insertEnt,
out string? reason,
StorageComponent? storageComp = null,
ItemComponent? item = null,
bool ignoreStacks = false,
bool ignoreLocation = false)
{
if (!Resolve(uid, ref storageComp) || !Resolve(insertEnt, ref item, false))
{
reason = null;
return false;
}
if (Transform(insertEnt).Anchored)
{
reason = "comp-storage-anchored-failure";
return false;
}
if (_whitelistSystem.IsWhitelistFail(storageComp.Whitelist, insertEnt) ||
_whitelistSystem.IsBlacklistPass(storageComp.Blacklist, insertEnt))
{
reason = "comp-storage-invalid-container";
return false;
}
if (!ignoreStacks
&& _stackQuery.TryGetComponent(insertEnt, out var stack)
&& HasSpaceInStacks((uid, storageComp), stack.StackTypeId))
{
reason = null;
return true;
}
var maxSize = GetMaxItemSize((uid, storageComp));
if (ItemSystem.GetSizePrototype(item.Size) > maxSize)
{
reason = "comp-storage-too-big";
return false;
}
if (TryComp<StorageComponent>(insertEnt, out var insertStorage)
&& GetMaxItemSize((insertEnt, insertStorage)) >= maxSize)
{
reason = "comp-storage-too-big";
return false;
}
if (!ignoreLocation && !storageComp.StoredItems.ContainsKey(insertEnt))
{
if (!TryGetAvailableGridSpace((uid, storageComp), (insertEnt, item), out _))
{
reason = "comp-storage-insufficient-capacity";
return false;
}
}
CheckingCanInsert = true;
if (!_containerSystem.CanInsert(insertEnt, storageComp.Container))
{
CheckingCanInsert = false;
reason = null;
return false;
}
CheckingCanInsert = false;
reason = null;
return true;
}
/// <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,
bool playSound = true,
bool stackAutomatically = true)
{
stackedEntity = null;
if (!Resolve(uid, ref uid.Comp))
return false;
if (!ItemFitsInGridLocation(insertEnt, uid, location))
return false;
uid.Comp.StoredItems[insertEnt] = location;
Dirty(uid, uid.Comp);
if (Insert(uid,
insertEnt,
out stackedEntity,
out _,
user: user,
storageComp: uid.Comp,
playSound: playSound,
stackAutomatically: stackAutomatically))
{
return true;
}
uid.Comp.StoredItems.Remove(insertEnt);
return false;
}
/// <summary>
/// Inserts into the storage container
/// </summary>
/// <returns>true if the entity was inserted, false otherwise. This will also return true if a stack was partially
/// inserted.</returns>
public bool Insert(
EntityUid uid,
EntityUid insertEnt,
out EntityUid? stackedEntity,
EntityUid? user = null,
StorageComponent? storageComp = null,
bool playSound = true,
bool stackAutomatically = true)
{
return Insert(uid, insertEnt, out stackedEntity, out _, user: user, storageComp: storageComp, playSound: playSound, stackAutomatically: stackAutomatically);
}
/// <summary>
/// Inserts into the storage container
/// </summary>
/// <returns>true if the entity was inserted, false otherwise. This will also return true if a stack was partially
/// inserted</returns>
public bool Insert(
EntityUid uid,
EntityUid insertEnt,
out EntityUid? stackedEntity,
out string? reason,
EntityUid? user = null,
StorageComponent? storageComp = null,
bool playSound = true,
bool stackAutomatically = true)
{
stackedEntity = null;
reason = null;
if (!Resolve(uid, ref storageComp))
return false;
/*
* 1. If the inserted thing is stackable then try to stack it to existing stacks
* 2. If anything remains insert whatever is possible.
* 3. If insertion is not possible then leave the stack as is.
* At either rate still play the insertion sound
*
* For now we just treat items as always being the same size regardless of stack count.
*/
if (!stackAutomatically || !_stackQuery.TryGetComponent(insertEnt, out var insertStack))
{
if (!_containerSystem.Insert(insertEnt, storageComp.Container))
return false;
if (playSound)
Audio.PlayPredicted(storageComp.StorageInsertSound, uid, user);
return true;
}
var toInsertCount = insertStack.Count;
foreach (var ent in storageComp.Container.ContainedEntities)
{
if (!_stackQuery.TryGetComponent(ent, out var containedStack))
continue;
if (!_stack.TryAdd(insertEnt, ent, insertStack, containedStack))
continue;
stackedEntity = ent;
if (insertStack.Count == 0)
break;
}
// Still stackable remaining
if (insertStack.Count > 0
&& !_containerSystem.Insert(insertEnt, storageComp.Container)
&& toInsertCount == insertStack.Count)
{
// Failed to insert anything.
return false;
}
if (playSound)
Audio.PlayPredicted(storageComp.StorageInsertSound, uid, user);
return true;
}
/// <summary>
/// Inserts an entity into storage from the player's active hand
/// </summary>
/// <param name="uid"></param>
/// <param name="player">The player to insert an entity from</param>
/// <param name="storageComp"></param>
/// <returns>true if inserted, false otherwise</returns>
public bool PlayerInsertHeldEntity(EntityUid uid, EntityUid player, StorageComponent? storageComp = null)
{
if (!Resolve(uid, ref storageComp) || !TryComp(player, out HandsComponent? hands) || hands.ActiveHandEntity == null)
return false;
var toInsert = hands.ActiveHandEntity;
if (!CanInsert(uid, toInsert.Value, out var reason, storageComp))
{
_popupSystem.PopupClient(Loc.GetString(reason ?? "comp-storage-cant-insert"), uid, player);
return false;
}
if (!_sharedHandsSystem.CanDrop(player, toInsert.Value, hands))
{
_popupSystem.PopupClient(Loc.GetString("comp-storage-cant-drop", ("entity", toInsert.Value)), uid, player);
return false;
}
return PlayerInsertEntityInWorld((uid, storageComp), player, toInsert.Value);
}
/// <summary>
/// Inserts an Entity (<paramref name="toInsert"/>) in the world into storage, informing <paramref name="player"/> if it fails.
/// <paramref name="toInsert"/> is *NOT* held, see <see cref="PlayerInsertHeldEntity(EntityUid,EntityUid,StorageComponent)"/>.
/// </summary>
/// <param name="uid"></param>
/// <param name="player">The player to insert an entity with</param>
/// <param name="toInsert"></param>
/// <returns>true if inserted, false otherwise</returns>
public bool PlayerInsertEntityInWorld(Entity<StorageComponent?> uid, EntityUid player, EntityUid toInsert)
{
if (!Resolve(uid, ref uid.Comp) || !_interactionSystem.InRangeUnobstructed(player, uid.Owner))
return false;
if (!Insert(uid, toInsert, out _, user: player, uid.Comp))
{
_popupSystem.PopupClient(Loc.GetString("comp-storage-cant-insert"), uid, player);
return false;
}
return true;
}
/// <summary>
/// Attempts to set the location of an item already inside of a storage container.
/// </summary>
public bool TrySetItemStorageLocation(Entity<ItemComponent?> itemEnt, Entity<StorageComponent?> storageEnt, ItemStorageLocation location)
{
if (!Resolve(itemEnt, ref itemEnt.Comp) || !Resolve(storageEnt, ref storageEnt.Comp))
return false;
if (!storageEnt.Comp.Container.ContainedEntities.Contains(itemEnt))
return false;
if (!ItemFitsInGridLocation(itemEnt, storageEnt, location.Position, location.Rotation))
return false;
storageEnt.Comp.StoredItems[itemEnt] = location;
Dirty(storageEnt, storageEnt.Comp);
return true;
}
/// <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;
// if the item has an available saved location, use that
if (FindSavedLocation(storageEnt, itemEnt, out storageLocation))
return true;
var storageBounding = storageEnt.Comp.Grid.GetBoundingBox();
Angle startAngle;
if (storageEnt.Comp.DefaultStorageOrientation == null)
{
startAngle = Angle.FromDegrees(-itemEnt.Comp.StoredRotation);
}
else
{
if (storageBounding.Width < storageBounding.Height)
{
startAngle = storageEnt.Comp.DefaultStorageOrientation == StorageDefaultOrientation.Horizontal
? Angle.Zero
: Angle.FromDegrees(90);
}
else
{
startAngle = storageEnt.Comp.DefaultStorageOrientation == StorageDefaultOrientation.Vertical
? Angle.Zero
: Angle.FromDegrees(90);
}
}
for (var y = storageBounding.Bottom; y <= storageBounding.Top; y++)
{
for (var x = storageBounding.Left; x <= storageBounding.Right; x++)
{
for (var angle = startAngle; angle <= Angle.FromDegrees(360 - startAngle); angle += Math.PI / 2f)
{
var location = new ItemStorageLocation(angle, (x, y));
if (ItemFitsInGridLocation(itemEnt, storageEnt, location))
{
storageLocation = location;
return true;
}
}
}
}
return false;
}
/// <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];
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);
}
/// <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))
return false;
var gridBounds = storageEnt.Comp.Grid.GetBoundingBox();
if (!gridBounds.Contains(position))
return false;
var itemShape = ItemSystem.GetAdjustedItemShape(itemEnt, rotation, position);
foreach (var box in itemShape)
{
for (var offsetY = box.Bottom; offsetY <= box.Top; offsetY++)
{
for (var offsetX = box.Left; offsetX <= box.Right; offsetX++)
{
var pos = (offsetX, offsetY);
if (!IsGridSpaceEmpty(itemEnt, storageEnt, pos))
return false;
}
}
}
return true;
}
/// <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;
foreach (var (ent, storedItem) in storageEnt.Comp.StoredItems)
{
if (ent == itemEnt.Owner)
continue;
if (!_itemQuery.TryGetComponent(ent, out var itemComp))
continue;
var adjustedShape = ItemSystem.GetAdjustedItemShape((ent, itemComp), storedItem);
foreach (var box in adjustedShape)
{
if (box.Contains(location))
return false;
}
}
return true;
}
/// <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);
}
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;
return true;
}
return false;
}
/// <summary>
/// Returns the sum of all the ItemSizes of the items inside of a storage.
/// </summary>
public int GetCumulativeItemAreas(Entity<StorageComponent?> entity)
{
if (!Resolve(entity, ref entity.Comp))
return 0;
var sum = 0;
foreach (var item in entity.Comp.Container.ContainedEntities)
{
if (!_itemQuery.TryGetComponent(item, out var itemComp))
continue;
sum += ItemSystem.GetItemShape((item, itemComp)).GetArea();
}
return sum;
}
public ItemSizePrototype GetMaxItemSize(Entity<StorageComponent?> uid)
{
if (!Resolve(uid, ref uid.Comp))
return _defaultStorageMaxItemSize;
// If we specify a max item size, use that
if (uid.Comp.MaxItemSize != null)
return _prototype.Index(uid.Comp.MaxItemSize.Value);
if (!_itemQuery.TryGetComponent(uid, out var item))
return _defaultStorageMaxItemSize;
// if there is no max item size specified, the value used
// is one below the item size of the storage entity.
return _nextSmallest[item.Size];
}
/// <summary>
/// Checks if a storage's UI is open by anyone when locked, and closes it.
/// </summary>
private void OnLockToggled(EntityUid uid, StorageComponent component, ref LockToggledEvent args)
{
if (!args.Locked)
return;
// Gets everyone looking at the UI
foreach (var actor in _ui.GetActors(uid, StorageComponent.StorageUiKey.Key).ToList())
{
if (!CanInteract(actor, (uid, component)))
_ui.CloseUi(uid, StorageComponent.StorageUiKey.Key, actor);
}
}
private void OnStackCountChanged(EntityUid uid, MetaDataComponent component, StackCountChangedEvent args)
{
if (_containerSystem.TryGetContainingContainer(uid, out var container, component) &&
container.ID == StorageComponent.ContainerId)
{
UpdateAppearance(container.Owner);
UpdateUI(container.Owner);
}
}
private void HandleOpenBackpack(ICommonSession? session)
{
HandleToggleSlotUI(session, "back");
}
private void HandleOpenBelt(ICommonSession? session)
{
HandleToggleSlotUI(session, "belt");
}
private void HandleToggleSlotUI(ICommonSession? session, string slot)
{
if (session is not { } playerSession)
return;
if (playerSession.AttachedEntity is not {Valid: true} playerEnt || !Exists(playerEnt))
return;
if (!_inventory.TryGetSlotEntity(playerEnt, slot, out var storageEnt))
return;
if (!ActionBlocker.CanInteract(playerEnt, storageEnt))
return;
if (!_ui.IsUiOpen(storageEnt.Value, StorageComponent.StorageUiKey.Key, playerEnt))
{
OpenStorageUI(storageEnt.Value, playerEnt, silent: false);
}
else
{
_ui.CloseUi(storageEnt.Value, StorageComponent.StorageUiKey.Key, playerEnt);
}
}
protected void ClearCantFillReasons()
{
#if DEBUG
CantFillReasons.Clear();
#endif
}
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;
}
/// <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);
[Serializable, NetSerializable]
protected sealed class StorageComponentState : ComponentState
{
public Dictionary<NetEntity, ItemStorageLocation> StoredItems = new();
public Dictionary<string, List<ItemStorageLocation>> SavedLocations = new();
public List<Box2i> Grid = new();
public ProtoId<ItemSizePrototype>? MaxItemSize;
public EntityWhitelist? Whitelist;
public EntityWhitelist? Blacklist;
}
}