From cc8984d0962175d7d4d6a851ef6449c9ec89c986 Mon Sep 17 00:00:00 2001 From: Nemanja <98561806+EmoGarbage404@users.noreply.github.com> Date: Mon, 4 Dec 2023 18:04:39 -0500 Subject: [PATCH] Grid Inventory (#21931) * Grid Inventory * oh boy we keep cracking on * auto insertion is kinda working? gross, too! * pieces and proper layouts * fix the sprites * mousing over grid pieces... finally * dragging deez nuts all over the screen * eek! * dragging is 90% less horrendous * auto-rotating * flatten * Rotation at last * fix rotation and change keybind for removing items. * rebinding and keybinding * wow! look at that! configurable with a button! cool! * dragging is a bit cooler, eh? * hover insert, my beloved * add some grids for storage, fix 1x1 storages, fix multiple inputs at once * el navigation * oh yeah some stuff i forgor * more fixes and QOL stuff * the griddening * the last of it (yippee) * sloth review :) --- Content.Client/Input/ContentContexts.cs | 2 + .../Options/UI/Tabs/KeyRebindTab.xaml.cs | 9 + .../Storage/StorageBoundUserInterface.cs | 144 ++---- .../Storage/Systems/StorageSystem.cs | 89 +++- .../Storage/UI/StorageUIController.cs | 71 --- Content.Client/Storage/UI/StorageWindow.cs | 182 -------- .../Systems/Hotbar/HotbarUIController.cs | 7 +- .../Systems/Hotbar/Widgets/HotbarGui.xaml | 21 +- .../Systems/Hotbar/Widgets/HotbarGui.xaml.cs | 2 +- .../Systems/Storage/Controls/ItemGridPiece.cs | 220 +++++++++ .../Storage/Controls/StorageContainer.cs | 431 ++++++++++++++++++ .../Systems/Storage/StorageUIController.cs | 327 +++++++++++++ Content.IntegrationTests/Tests/StorageTest.cs | 12 +- .../EntitySystems/ChemMasterSystem.cs | 2 +- .../EntitySystems/StorageSystem.Fill.cs | 78 +++- Content.Shared/CCVar/CCVars.cs | 6 + .../ContainerHeld/ContainerHeldSystem.cs | 2 +- Content.Shared/Input/ContentKeyFunctions.cs | 2 + Content.Shared/Item/ItemComponent.cs | 7 + Content.Shared/Item/ItemSizePrototype.cs | 6 + Content.Shared/Item/SharedItemSystem.cs | 55 +++ .../EntitySystems/SharedStorageSystem.cs | 378 ++++++++++++--- Content.Shared/Storage/ItemStorageLocation.cs | 40 ++ Content.Shared/Storage/StorageComponent.cs | 70 ++- Content.Shared/Storage/StorageHelpers.cs | 76 +++ .../en-US/escape-menu/ui/options-menu.ftl | 3 + .../Fills/Backpacks/StarterGear/backpack.yml | 3 +- .../Fills/Backpacks/StarterGear/satchel.yml | 2 +- .../Catalog/Fills/Backpacks/duffelbag.yml | 4 +- .../Catalog/Fills/Boxes/general.yml | 24 +- .../Catalog/Fills/Boxes/medical.yml | 3 +- .../Catalog/Fills/Boxes/science.yml | 11 +- .../Catalog/Fills/Boxes/security.yml | 6 +- .../Prototypes/Catalog/Fills/Items/belt.yml | 3 +- .../Catalog/Fills/Items/briefcases.yml | 2 + .../Entities/Clothing/Back/backpacks.yml | 68 ++- .../Entities/Clothing/Back/duffel.yml | 7 +- .../Entities/Clothing/Back/satchel.yml | 3 +- .../Clothing/Belt/base_clothingbelt.yml | 9 +- .../Entities/Clothing/Belt/belts.yml | 9 +- .../Entities/Clothing/Belt/quiver.yml | 3 +- .../Entities/Clothing/Belt/waist_bags.yml | 4 +- .../Entities/Clothing/Head/hats.yml | 6 +- .../OuterClothing/base_clothingouter.yml | 3 +- .../Clothing/Shoes/base_clothingshoes.yml | 3 +- .../Prototypes/Entities/Debugging/item.yml | 21 + .../Prototypes/Entities/Mobs/NPCs/silicon.yml | 3 +- .../Objects/Consumable/Drinks/drinks_cans.yml | 3 +- .../Consumable/Food/Containers/box.yml | 24 +- .../Smokeables/Cigarettes/cartons.yml | 3 +- .../Smokeables/Cigarettes/packs.yml | 7 +- .../Smokeables/Cigarettes/rolling_paper.yml | 3 +- .../Consumable/Smokeables/Cigars/case.yml | 3 +- .../Entities/Objects/Fun/candy_bucket.yml | 3 +- .../Entities/Objects/Fun/crayons.yml | 3 +- .../Entities/Objects/Fun/dice_bag.yml | 6 +- .../Prototypes/Entities/Objects/Misc/box.yml | 5 +- .../Entities/Objects/Misc/briefcases.yml | 9 +- .../Entities/Objects/Misc/medalcase.yml | 4 +- .../Entities/Objects/Misc/paper.yml | 11 +- .../Objects/Misc/subdermal_implants.yml | 3 +- .../Objects/Specific/Chapel/bibles.yml | 3 +- .../Objects/Specific/Chemistry/chem_bag.yml | 3 +- .../Objects/Specific/Hydroponics/tools.yml | 3 +- .../Objects/Specific/Janitorial/trashbag.yml | 7 +- .../Objects/Specific/Kitchen/foodcarts.yml | 3 +- .../Objects/Specific/Librarian/books_bag.yml | 3 +- .../Objects/Specific/Medical/medkits.yml | 3 +- .../Objects/Specific/Research/rped.yml | 3 +- .../Objects/Specific/Salvage/ore_bag.yml | 3 +- .../Entities/Objects/Specific/chemistry.yml | 5 +- .../Entities/Objects/Tools/matches.yml | 10 +- .../Entities/Objects/Tools/toolbox.yml | 8 +- .../Objects/Weapons/Guns/pneumatic_cannon.yml | 12 +- .../Structures/Furniture/bookshelf.yml | 3 +- .../Entities/Structures/Furniture/dresser.yml | 3 +- .../Structures/Storage/filing_cabinets.yml | 6 +- .../Entities/Structures/Storage/ore_box.yml | 3 +- .../Structures/Wallmounts/noticeboard.yml | 3 +- .../XenoArch/Effects/utility_effects.yml | 4 +- Resources/Prototypes/item_size.yml | 12 + .../Interface/Default/Storage/back.png | Bin 0 -> 204 bytes .../Interface/Default/Storage/exit.png | Bin 0 -> 208 bytes .../Default/Storage/piece_bottom.png | Bin 0 -> 114 bytes .../Default/Storage/piece_bottomLeft.png | Bin 0 -> 146 bytes .../Default/Storage/piece_bottomRight.png | Bin 0 -> 147 bytes .../Default/Storage/piece_center.png | Bin 0 -> 100 bytes .../Interface/Default/Storage/piece_left.png | Bin 0 -> 109 bytes .../Interface/Default/Storage/piece_right.png | Bin 0 -> 112 bytes .../Interface/Default/Storage/piece_top.png | Bin 0 -> 112 bytes .../Default/Storage/piece_topLeft.png | Bin 0 -> 132 bytes .../Default/Storage/piece_topRight.png | Bin 0 -> 147 bytes .../Default/Storage/sidebar_bottom.png | Bin 0 -> 136 bytes .../Interface/Default/Storage/sidebar_fat.png | Bin 0 -> 149 bytes .../Interface/Default/Storage/sidebar_mid.png | Bin 0 -> 112 bytes .../Interface/Default/Storage/sidebar_top.png | Bin 0 -> 130 bytes .../Default/Storage/tile_blocked.png | Bin 0 -> 124 bytes .../Interface/Default/Storage/tile_empty.png | Bin 0 -> 132 bytes Resources/keybinds.yml | 7 + 99 files changed, 2014 insertions(+), 619 deletions(-) delete mode 100644 Content.Client/Storage/UI/StorageUIController.cs delete mode 100644 Content.Client/Storage/UI/StorageWindow.cs create mode 100644 Content.Client/UserInterface/Systems/Storage/Controls/ItemGridPiece.cs create mode 100644 Content.Client/UserInterface/Systems/Storage/Controls/StorageContainer.cs create mode 100644 Content.Client/UserInterface/Systems/Storage/StorageUIController.cs create mode 100644 Content.Shared/Storage/ItemStorageLocation.cs create mode 100644 Content.Shared/Storage/StorageHelpers.cs create mode 100644 Resources/Prototypes/Entities/Debugging/item.yml create mode 100644 Resources/Textures/Interface/Default/Storage/back.png create mode 100644 Resources/Textures/Interface/Default/Storage/exit.png create mode 100644 Resources/Textures/Interface/Default/Storage/piece_bottom.png create mode 100644 Resources/Textures/Interface/Default/Storage/piece_bottomLeft.png create mode 100644 Resources/Textures/Interface/Default/Storage/piece_bottomRight.png create mode 100644 Resources/Textures/Interface/Default/Storage/piece_center.png create mode 100644 Resources/Textures/Interface/Default/Storage/piece_left.png create mode 100644 Resources/Textures/Interface/Default/Storage/piece_right.png create mode 100644 Resources/Textures/Interface/Default/Storage/piece_top.png create mode 100644 Resources/Textures/Interface/Default/Storage/piece_topLeft.png create mode 100644 Resources/Textures/Interface/Default/Storage/piece_topRight.png create mode 100644 Resources/Textures/Interface/Default/Storage/sidebar_bottom.png create mode 100644 Resources/Textures/Interface/Default/Storage/sidebar_fat.png create mode 100644 Resources/Textures/Interface/Default/Storage/sidebar_mid.png create mode 100644 Resources/Textures/Interface/Default/Storage/sidebar_top.png create mode 100644 Resources/Textures/Interface/Default/Storage/tile_blocked.png create mode 100644 Resources/Textures/Interface/Default/Storage/tile_empty.png diff --git a/Content.Client/Input/ContentContexts.cs b/Content.Client/Input/ContentContexts.cs index ba8b72788e..82388cb75b 100644 --- a/Content.Client/Input/ContentContexts.cs +++ b/Content.Client/Input/ContentContexts.cs @@ -30,6 +30,8 @@ namespace Content.Client.Input common.AddFunction(ContentKeyFunctions.TakeScreenshot); common.AddFunction(ContentKeyFunctions.TakeScreenshotNoUI); common.AddFunction(ContentKeyFunctions.ToggleFullscreen); + common.AddFunction(ContentKeyFunctions.MoveStoredItem); + common.AddFunction(ContentKeyFunctions.RotateStoredItem); common.AddFunction(ContentKeyFunctions.Point); common.AddFunction(ContentKeyFunctions.ZoomOut); common.AddFunction(ContentKeyFunctions.ZoomIn); diff --git a/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs b/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs index 87b1f10352..18872d16b5 100644 --- a/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs +++ b/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs @@ -97,6 +97,12 @@ namespace Content.Client.Options.UI.Tabs _deferCommands.Add(_inputManager.SaveToUserData); } + private void HandleStaticStorageUI(BaseButton.ButtonToggledEventArgs args) + { + _cfg.SetCVar(CCVars.StaticStorageUI, args.Pressed); + _cfg.SaveToFile(); + } + public KeyRebindTab() { IoCManager.InjectDependencies(this); @@ -175,6 +181,9 @@ namespace Content.Client.Options.UI.Tabs AddButton(ContentKeyFunctions.Drop); AddButton(ContentKeyFunctions.ExamineEntity); AddButton(ContentKeyFunctions.SwapHands); + AddButton(ContentKeyFunctions.MoveStoredItem); + AddButton(ContentKeyFunctions.RotateStoredItem); + AddCheckBox("ui-options-static-storage-ui", _cfg.GetCVar(CCVars.StaticStorageUI), HandleStaticStorageUI); AddHeader("ui-options-header-interaction-adv"); AddButton(ContentKeyFunctions.SmartEquipBackpack); diff --git a/Content.Client/Storage/StorageBoundUserInterface.cs b/Content.Client/Storage/StorageBoundUserInterface.cs index 9a3340e5b0..88de528ff6 100644 --- a/Content.Client/Storage/StorageBoundUserInterface.cs +++ b/Content.Client/Storage/StorageBoundUserInterface.cs @@ -1,129 +1,37 @@ -using Content.Client.Examine; -using Content.Client.Storage.UI; -using Content.Client.UserInterface.Controls; -using Content.Client.Verbs.UI; -using Content.Shared.Input; -using Content.Shared.Interaction; +using Content.Client.Storage.Systems; using Content.Shared.Storage; using JetBrains.Annotations; -using Robust.Client.GameObjects; -using Robust.Client.UserInterface; -using Robust.Client.UserInterface.Controls; -using Robust.Shared.Input; -using static Content.Shared.Storage.StorageComponent; -namespace Content.Client.Storage +namespace Content.Client.Storage; + +[UsedImplicitly] +public sealed class StorageBoundUserInterface : BoundUserInterface { - [UsedImplicitly] - public sealed class StorageBoundUserInterface : BoundUserInterface + [Dependency] private readonly IEntityManager _entManager = default!; + + private readonly StorageSystem _storage; + + public StorageBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) { - [ViewVariables] - private StorageWindow? _window; + IoCManager.InjectDependencies(this); + _storage = _entManager.System(); + } - [Dependency] private readonly IEntityManager _entManager = default!; + protected override void Open() + { + base.Open(); - public StorageBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) - { - IoCManager.InjectDependencies(this); - } + if (_entManager.TryGetComponent(Owner, out var comp)) + _storage.OpenStorageUI(Owner, comp); + } - protected override void Open() - { - base.Open(); + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + if (!disposing) + return; - if (_window == null) - { - // TODO: This is a bit of a mess but storagecomponent got moved to shared and cleaned up a bit. - var controller = IoCManager.Resolve().GetUIController(); - _window = controller.EnsureStorageWindow(Owner); - _window.Title = EntMan.GetComponent(Owner).EntityName; - - _window.EntityList.GenerateItem += _window.GenerateButton; - _window.EntityList.ItemPressed += InteractWithItem; - _window.StorageContainerButton.OnPressed += TouchedContainerButton; - - _window.OnClose += Close; - - if (EntMan.TryGetComponent(Owner, out var storageComp)) - { - BuildEntityList(Owner, storageComp); - } - - } - else - { - _window.Open(); - } - } - - public void BuildEntityList(EntityUid uid, StorageComponent component) - { - _window?.BuildEntityList(uid, component); - } - - public void InteractWithItem(BaseButton.ButtonEventArgs? args, ListData? cData) - { - if (args == null || cData is not EntityListData { Uid: var entity }) - return; - - if (args.Event.Function == EngineKeyFunctions.UIClick) - { - SendPredictedMessage(new StorageInteractWithItemEvent(_entManager.GetNetEntity(entity))); - } - else if (EntMan.EntityExists(entity)) - { - OnButtonPressed(args.Event, entity); - } - } - - private void OnButtonPressed(GUIBoundKeyEventArgs args, EntityUid entity) - { - if (args.Function == ContentKeyFunctions.ExamineEntity) - { - EntMan.System() - .DoExamine(entity); - } - else if (args.Function == EngineKeyFunctions.UseSecondary) - { - IoCManager.Resolve().GetUIController().OpenVerbMenu(entity); - } - else if (args.Function == ContentKeyFunctions.ActivateItemInWorld) - { - EntMan.EntityNetManager?.SendSystemNetworkMessage( - new InteractInventorySlotEvent(EntMan.GetNetEntity(entity), altInteract: false)); - } - else if (args.Function == ContentKeyFunctions.AltActivateItemInWorld) - { - EntMan.RaisePredictiveEvent(new InteractInventorySlotEvent(EntMan.GetNetEntity(entity), altInteract: true)); - } - else - { - return; - } - - args.Handle(); - } - - public void TouchedContainerButton(BaseButton.ButtonEventArgs args) - { - SendPredictedMessage(new StorageInsertItemMessage()); - } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - if (!disposing) - return; - - if (_window != null) - { - _window.Orphan(); - _window.EntityList.GenerateItem -= _window.GenerateButton; - _window.EntityList.ItemPressed -= InteractWithItem; - _window.StorageContainerButton.OnPressed -= TouchedContainerButton; - _window.OnClose -= Close; - _window = null; - } - } + _storage.CloseStorageUI(Owner); } } + diff --git a/Content.Client/Storage/Systems/StorageSystem.cs b/Content.Client/Storage/Systems/StorageSystem.cs index 86d6ec6378..56d0810dd5 100644 --- a/Content.Client/Storage/Systems/StorageSystem.cs +++ b/Content.Client/Storage/Systems/StorageSystem.cs @@ -1,24 +1,30 @@ -using Content.Client.Animations; +using System.Linq; +using Content.Client.Animations; using Content.Shared.Hands; using Content.Shared.Storage; using Content.Shared.Storage.EntitySystems; +using Robust.Shared.Collections; using Robust.Shared.Map; using Robust.Shared.Timing; namespace Content.Client.Storage.Systems; -// TODO kill this is all horrid. public sealed class StorageSystem : SharedStorageSystem { [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly EntityPickupAnimationSystem _entityPickupAnimation = default!; - public event Action? StorageUpdated; + private readonly List> _openStorages = new(); + public int OpenStorageAmount => _openStorages.Count; + + public event Action>? StorageUpdated; + public event Action?>? StorageOrderChanged; public override void Initialize() { base.Initialize(); + SubscribeLocalEvent(OnShutdown); SubscribeNetworkEvent(HandlePickupAnimation); SubscribeNetworkEvent(HandleAnimatingInsertingEntities); } @@ -26,7 +32,74 @@ public sealed class StorageSystem : SharedStorageSystem public override void UpdateUI(Entity entity) { if (Resolve(entity.Owner, ref entity.Comp)) - StorageUpdated?.Invoke(entity.Owner, entity.Comp); + StorageUpdated?.Invoke((entity, entity.Comp)); + } + + public void OpenStorageUI(EntityUid uid, StorageComponent component) + { + if (_openStorages.Contains((uid, component))) + return; + + ClearNonParentStorages(uid); + _openStorages.Add((uid, component)); + Entity? last = _openStorages.LastOrDefault(); + StorageOrderChanged?.Invoke(last); + } + + public void CloseStorageUI(Entity entity) + { + if (!Resolve(entity, ref entity.Comp)) + return; + + if (!_openStorages.Contains((entity, entity.Comp))) + return; + + var storages = new ValueList>(_openStorages); + var reverseStorages = storages.Reverse(); + + foreach (var storage in reverseStorages) + { + CloseStorageBoundUserInterface(storage.Owner); + _openStorages.Remove(storage); + if (storage.Owner == entity.Owner) + break; + } + + Entity? last = null; + if (_openStorages.Any()) + last = _openStorages.LastOrDefault(); + StorageOrderChanged?.Invoke(last); + } + + private void ClearNonParentStorages(EntityUid uid) + { + var storages = new ValueList>(_openStorages); + var reverseStorages = storages.Reverse(); + + foreach (var storage in reverseStorages) + { + if (storage.Comp.Container.Contains(uid)) + break; + + CloseStorageBoundUserInterface(storage.Owner); + _openStorages.Remove(storage); + } + } + + private void CloseStorageBoundUserInterface(Entity entity) + { + if (!Resolve(entity, ref entity.Comp, false)) + return; + + if (entity.Comp.OpenInterfaces.GetValueOrDefault(StorageComponent.StorageUiKey.Key) is not { } bui) + return; + + bui.Close(); + } + + private void OnShutdown(Entity ent, ref ComponentShutdown args) + { + CloseStorageUI((ent, ent.Comp)); } /// @@ -49,14 +122,14 @@ public sealed class StorageSystem : SharedStorageSystem if (!_timing.IsFirstTimePredicted) return; - if (finalCoords.InRange(EntityManager, _transform, initialCoords, 0.1f) || + if (finalCoords.InRange(EntityManager, TransformSystem, initialCoords, 0.1f) || !Exists(initialCoords.EntityId) || !Exists(finalCoords.EntityId)) { return; } - var finalMapPos = finalCoords.ToMapPos(EntityManager, _transform); - var finalPos = _transform.GetInvWorldMatrix(initialCoords.EntityId).Transform(finalMapPos); + var finalMapPos = finalCoords.ToMapPos(EntityManager, TransformSystem); + var finalPos = TransformSystem.GetInvWorldMatrix(initialCoords.EntityId).Transform(finalMapPos); _entityPickupAnimation.AnimateEntityPickup(item, initialCoords, finalPos, initialAngle); } @@ -74,7 +147,7 @@ public sealed class StorageSystem : SharedStorageSystem var entity = GetEntity(msg.StoredEntities[i]); var initialPosition = msg.EntityPositions[i]; - if (EntityManager.EntityExists(entity) && transformComp != null) + if (Exists(entity) && transformComp != null) { _entityPickupAnimation.AnimateEntityPickup(entity, GetCoordinates(initialPosition), transformComp.LocalPosition, msg.EntityAngles[i]); } diff --git a/Content.Client/Storage/UI/StorageUIController.cs b/Content.Client/Storage/UI/StorageUIController.cs deleted file mode 100644 index 352ee4f9ed..0000000000 --- a/Content.Client/Storage/UI/StorageUIController.cs +++ /dev/null @@ -1,71 +0,0 @@ -using Content.Client.Gameplay; -using Content.Client.Storage.Systems; -using Content.Shared.Storage; -using Robust.Client.UserInterface.Controllers; - -namespace Content.Client.Storage.UI; - -public sealed class StorageUIController : UIController, IOnSystemChanged, IOnStateExited -{ - // This is mainly to keep legacy functionality for now. - private readonly Dictionary _storageWindows = new(); - - public override void Initialize() - { - base.Initialize(); - EntityManager.EventBus.SubscribeLocalEvent(OnStorageShutdown); - } - public StorageWindow EnsureStorageWindow(EntityUid uid) - { - if (_storageWindows.TryGetValue(uid, out var window)) - { - UIManager.WindowRoot.AddChild(window); - return window; - } - - window = new StorageWindow(EntityManager); - _storageWindows[uid] = window; - window.OpenCenteredLeft(); - return window; - } - - private void OnStorageShutdown(EntityUid uid, StorageComponent component, ComponentShutdown args) - { - if (!_storageWindows.TryGetValue(uid, out var window)) - return; - - _storageWindows.Remove(uid); - window.Dispose(); - } - - private void OnStorageUpdate(EntityUid uid, StorageComponent component) - { - if (EntityManager.TryGetComponent(uid, out var uiComp) && - uiComp.OpenInterfaces.TryGetValue(StorageComponent.StorageUiKey.Key, out var bui)) - { - var storageBui = (StorageBoundUserInterface) bui; - - storageBui.BuildEntityList(uid, component); - } - } - - public void OnSystemLoaded(StorageSystem system) - { - system.StorageUpdated += OnStorageUpdate; - } - - public void OnSystemUnloaded(StorageSystem system) - { - system.StorageUpdated -= OnStorageUpdate; - } - - public void OnStateExited(GameplayState state) - { - foreach (var window in _storageWindows.Values) - { - window.Dispose(); - } - - _storageWindows.Clear(); - } -} diff --git a/Content.Client/Storage/UI/StorageWindow.cs b/Content.Client/Storage/UI/StorageWindow.cs deleted file mode 100644 index b096e8d472..0000000000 --- a/Content.Client/Storage/UI/StorageWindow.cs +++ /dev/null @@ -1,182 +0,0 @@ -using System.Numerics; -using Content.Client.Items.Systems; -using Content.Client.Message; -using Robust.Client.Graphics; -using Robust.Client.UserInterface.Controls; -using Content.Client.Stylesheets; -using Content.Client.UserInterface.Controls; -using Content.Shared.IdentityManagement; -using Content.Shared.Item; -using Content.Shared.Stacks; -using Content.Shared.Storage; -using Content.Shared.Storage.EntitySystems; -using Robust.Client.UserInterface; -using static Robust.Client.UserInterface.Controls.BoxContainer; -using Direction = Robust.Shared.Maths.Direction; - -namespace Content.Client.Storage.UI -{ - /// - /// GUI class for client storage component - /// - public sealed class StorageWindow : FancyWindow - { - private readonly IEntityManager _entityManager; - - private readonly SharedStorageSystem _storage; - private readonly ItemSystem _item; - - private readonly RichTextLabel _information; - public readonly ContainerButton StorageContainerButton; - public readonly ListContainer EntityList; - private readonly StyleBoxFlat _hoveredBox = new() { BackgroundColor = Color.Black.WithAlpha(0.35f) }; - private readonly StyleBoxFlat _unHoveredBox = new() { BackgroundColor = Color.Black.WithAlpha(0.0f) }; - - public StorageWindow(IEntityManager entityManager) - { - _entityManager = entityManager; - _storage = _entityManager.System(); - _item = _entityManager.System(); - SetSize = new Vector2(240, 320); - Title = Loc.GetString("comp-storage-window-title"); - RectClipContent = true; - - StorageContainerButton = new ContainerButton - { - Name = "StorageContainerButton", - MouseFilter = MouseFilterMode.Pass, - }; - - ContentsContainer.AddChild(StorageContainerButton); - - var innerContainerButton = new PanelContainer - { - PanelOverride = _unHoveredBox, - }; - - StorageContainerButton.AddChild(innerContainerButton); - - Control vBox = new BoxContainer() - { - Orientation = LayoutOrientation.Vertical, - MouseFilter = MouseFilterMode.Ignore, - Margin = new Thickness(5), - }; - - StorageContainerButton.AddChild(vBox); - - _information = new RichTextLabel - { - VerticalAlignment = VAlignment.Center - }; - _information.SetMessage(Loc.GetString("comp-storage-window-weight", - ("weight", 0), - ("maxWeight", 0), - ("size", _item.GetItemSizeLocale(SharedStorageSystem.DefaultStorageMaxItemSize)))); - - vBox.AddChild(_information); - - EntityList = new ListContainer - { - Name = "EntityListContainer", - }; - - vBox.AddChild(EntityList); - - EntityList.OnMouseEntered += _ => - { - innerContainerButton.PanelOverride = _hoveredBox; - }; - - EntityList.OnMouseExited += _ => - { - innerContainerButton.PanelOverride = _unHoveredBox; - }; - } - - /// - /// Loops through stored entities creating buttons for each, updates information labels - /// - public void BuildEntityList(EntityUid entity, StorageComponent component) - { - var storedCount = component.Container.ContainedEntities.Count; - var list = new List(storedCount); - - foreach (var uid in component.Container.ContainedEntities) - { - list.Add(new EntityListData(uid)); - } - - EntityList.PopulateList(list); - - SetStorageInformation((entity, component)); - } - - private void SetStorageInformation(Entity uid) - { - //todo: text is the straight agenda. What about anything else? - if (uid.Comp.MaxSlots == null) - { - _information.SetMarkup(Loc.GetString("comp-storage-window-weight", - ("weight", _storage.GetCumulativeItemSizes(uid, uid.Comp)), - ("maxWeight", uid.Comp.MaxTotalWeight), - ("size", _item.GetItemSizeLocale(_storage.GetMaxItemSize((uid, uid.Comp)))))); - } - else - { - _information.SetMarkup(Loc.GetString("comp-storage-window-slots", - ("itemCount", uid.Comp.Container.ContainedEntities.Count), - ("maxCount", uid.Comp.MaxSlots), - ("size", _item.GetItemSizeLocale(_storage.GetMaxItemSize((uid, uid.Comp)))))); - } - } - - /// - /// Button created for each entity that represents that item in the storage UI, with a texture, and name and size label - /// - public void GenerateButton(ListData data, ListContainerButton button) - { - if (data is not EntityListData {Uid: var entity} - || !_entityManager.EntityExists(entity)) - return; - - _entityManager.TryGetComponent(entity, out StackComponent? stack); - _entityManager.TryGetComponent(entity, out ItemComponent? item); - var count = stack?.Count ?? 1; - - var spriteView = new SpriteView - { - HorizontalAlignment = HAlignment.Left, - VerticalAlignment = VAlignment.Center, - SetSize = new Vector2(32.0f, 32.0f), - OverrideDirection = Direction.South, - }; - spriteView.SetEntity(entity); - button.AddChild(new BoxContainer - { - Orientation = LayoutOrientation.Horizontal, - SeparationOverride = 2, - Children = - { - spriteView, - new Label - { - HorizontalExpand = true, - ClipText = true, - Text = _entityManager.GetComponent(Identity.Entity(entity, _entityManager)).EntityName + - (count > 1 ? $" x {count}" : string.Empty) - }, - new Label - { - Align = Label.AlignMode.Right, - Text = item?.Size != null - ? $"{_item.GetItemSizeWeight(item.Size)}" - : Loc.GetString("comp-storage-no-item-size") - } - } - }); - button.StyleClasses.Add(StyleNano.StyleClassStorageButton); - button.EnableAllKeybinds = true; - } - } -} diff --git a/Content.Client/UserInterface/Systems/Hotbar/HotbarUIController.cs b/Content.Client/UserInterface/Systems/Hotbar/HotbarUIController.cs index 17cc03c113..397da978d8 100644 --- a/Content.Client/UserInterface/Systems/Hotbar/HotbarUIController.cs +++ b/Content.Client/UserInterface/Systems/Hotbar/HotbarUIController.cs @@ -4,6 +4,8 @@ using Content.Client.UserInterface.Systems.Hands.Controls; using Content.Client.UserInterface.Systems.Hotbar.Widgets; using Content.Client.UserInterface.Systems.Inventory; using Content.Client.UserInterface.Systems.Inventory.Controls; +using Content.Client.UserInterface.Systems.Storage; +using Content.Client.UserInterface.Systems.Storage.Controls; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controllers; @@ -13,6 +15,7 @@ public sealed class HotbarUIController : UIController { private InventoryUIController? _inventory; private HandsUIController? _hands; + private StorageUIController? _storage; public override void Initialize() { @@ -27,12 +30,14 @@ public sealed class HotbarUIController : UIController ReloadHotbar(); } - public void Setup(HandsContainer handsContainer, ItemSlotButtonContainer inventoryBar, ItemStatusPanel handStatus) + public void Setup(HandsContainer handsContainer, ItemSlotButtonContainer inventoryBar, ItemStatusPanel handStatus, StorageContainer storageContainer) { _inventory = UIManager.GetUIController(); _hands = UIManager.GetUIController(); + _storage = UIManager.GetUIController(); _hands.RegisterHandContainer(handsContainer); _inventory.RegisterInventoryBarContainer(inventoryBar); + _storage.RegisterStorageContainer(storageContainer); } public void ReloadHotbar() diff --git a/Content.Client/UserInterface/Systems/Hotbar/Widgets/HotbarGui.xaml b/Content.Client/UserInterface/Systems/Hotbar/Widgets/HotbarGui.xaml index cd34412da6..a760de7430 100644 --- a/Content.Client/UserInterface/Systems/Hotbar/Widgets/HotbarGui.xaml +++ b/Content.Client/UserInterface/Systems/Hotbar/Widgets/HotbarGui.xaml @@ -1,6 +1,7 @@  - + + + + + + (); - hotbarController.Setup(HandContainer, InventoryHotbar, StatusPanel); + hotbarController.Setup(HandContainer, InventoryHotbar, StatusPanel, StoragePanel); LayoutContainer.SetGrowVertical(this, LayoutContainer.GrowDirection.Begin); } diff --git a/Content.Client/UserInterface/Systems/Storage/Controls/ItemGridPiece.cs b/Content.Client/UserInterface/Systems/Storage/Controls/ItemGridPiece.cs new file mode 100644 index 0000000000..830d59bd90 --- /dev/null +++ b/Content.Client/UserInterface/Systems/Storage/Controls/ItemGridPiece.cs @@ -0,0 +1,220 @@ +using System.Linq; +using System.Numerics; +using Content.Client.Items.Systems; +using Content.Shared.Item; +using Content.Shared.Storage; +using Content.Shared.Storage.EntitySystems; +using Robust.Client.GameObjects; +using Robust.Client.Graphics; +using Robust.Client.UserInterface; + +namespace Content.Client.UserInterface.Systems.Storage.Controls; + +public sealed class ItemGridPiece : Control +{ + private readonly ItemSystem _itemSystem; + private readonly SpriteSystem _spriteSystem; + private readonly StorageUIController _storageController; + + private readonly List<(Texture, Vector2)> _texturesPositions = new(); + + public readonly EntityUid Entity; + public ItemStorageLocation Location; + + public event Action? OnPiecePressed; + public event Action? OnPieceUnpressed; + + #region Textures + private readonly string _centerTexturePath = "Storage/piece_center"; + private Texture? _centerTexture; + private readonly string _topTexturePath = "Storage/piece_top"; + private Texture? _topTexture; + private readonly string _bottomTexturePath = "Storage/piece_bottom"; + private Texture? _bottomTexture; + private readonly string _leftTexturePath = "Storage/piece_left"; + private Texture? _leftTexture; + private readonly string _rightTexturePath = "Storage/piece_right"; + private Texture? _rightTexture; + private readonly string _topLeftTexturePath = "Storage/piece_topLeft"; + private Texture? _topLeftTexture; + private readonly string _topRightTexturePath = "Storage/piece_topRight"; + private Texture? _topRightTexture; + private readonly string _bottomLeftTexturePath = "Storage/piece_bottomLeft"; + private Texture? _bottomLeftTexture; + private readonly string _bottomRightTexturePath = "Storage/piece_bottomRight"; + private Texture? _bottomRightTexture; + #endregion + + public ItemGridPiece(Entity entity, ItemStorageLocation location, IEntityManager entityManager) + { + IoCManager.InjectDependencies(this); + + _itemSystem = entityManager.System(); + _spriteSystem = entityManager.System(); + _storageController = UserInterfaceManager.GetUIController(); + + Entity = entity.Owner; + Location = location; + + Visible = true; + MouseFilter = MouseFilterMode.Pass; + + OnThemeUpdated(); + } + + protected override void OnThemeUpdated() + { + base.OnThemeUpdated(); + + _centerTexture = Theme.ResolveTextureOrNull(_centerTexturePath)?.Texture; + _topTexture = Theme.ResolveTextureOrNull(_topTexturePath)?.Texture; + _bottomTexture = Theme.ResolveTextureOrNull(_bottomTexturePath)?.Texture; + _leftTexture = Theme.ResolveTextureOrNull(_leftTexturePath)?.Texture; + _rightTexture = Theme.ResolveTextureOrNull(_rightTexturePath)?.Texture; + _topLeftTexture = Theme.ResolveTextureOrNull(_topLeftTexturePath)?.Texture; + _topRightTexture = Theme.ResolveTextureOrNull(_topRightTexturePath)?.Texture; + _bottomLeftTexture = Theme.ResolveTextureOrNull(_bottomLeftTexturePath)?.Texture; + _bottomRightTexture = Theme.ResolveTextureOrNull(_bottomRightTexturePath)?.Texture; + } + + protected override void Draw(DrawingHandleScreen handle) + { + base.Draw(handle); + + if (_storageController.IsDragging && _storageController.CurrentlyDragging == this) + return; + + var adjustedShape = _itemSystem.GetAdjustedItemShape((Entity, null), Location.Rotation, Vector2i.Zero); + var boundingGrid = adjustedShape.GetBoundingBox(); + var size = _centerTexture!.Size * 2 * UIScale; + + var hovering = !_storageController.IsDragging && UserInterfaceManager.CurrentlyHovered == this; + //yeah, this coloring is kinda hardcoded. deal with it. B) + Color? colorModulate = hovering ? null : Color.FromHex("#a8a8a8"); + + _texturesPositions.Clear(); + for (var y = boundingGrid.Bottom; y <= boundingGrid.Top; y++) + { + for (var x = boundingGrid.Left; x <= boundingGrid.Right; x++) + { + if (!adjustedShape.Contains(x, y)) + continue; + + var offset = size * 2 * new Vector2(x - boundingGrid.Left, y - boundingGrid.Bottom); + var topLeft = PixelPosition + offset.Floored(); + + if (GetTexture(adjustedShape, new Vector2i(x, y), Direction.NorthEast) is {} neTexture) + { + var neOffset = new Vector2(size.X, 0); + handle.DrawTextureRect(neTexture, new UIBox2(topLeft + neOffset, topLeft + neOffset + size), colorModulate); + } + if (GetTexture(adjustedShape, new Vector2i(x, y), Direction.NorthWest) is {} nwTexture) + { + _texturesPositions.Add((nwTexture, Position + offset / UIScale)); + handle.DrawTextureRect(nwTexture, new UIBox2(topLeft, topLeft + size), colorModulate); + } + if (GetTexture(adjustedShape, new Vector2i(x, y), Direction.SouthEast) is {} seTexture) + { + var seOffset = size; + handle.DrawTextureRect(seTexture, new UIBox2(topLeft + seOffset, topLeft + seOffset + size), colorModulate); + } + if (GetTexture(adjustedShape, new Vector2i(x, y), Direction.SouthWest) is {} swTexture) + { + var swOffset = new Vector2(0, size.Y); + handle.DrawTextureRect(swTexture, new UIBox2(topLeft + swOffset, topLeft + swOffset + size), colorModulate); + } + } + } + + // typically you'd divide by two, but since the textures are half a tile, this is done implicitly + var iconOffset = new Vector2((boundingGrid.Width + 1) * size.X , + (boundingGrid.Height + 1) * size.Y); + + _spriteSystem.ForceUpdate(Entity); + handle.DrawEntity(Entity, + PixelPosition + iconOffset, + Vector2.One * 2 * UIScale, + Angle.Zero, + overrideDirection: Direction.South); + } + + protected override bool HasPoint(Vector2 point) + { + foreach (var (texture, position) in _texturesPositions) + { + if (!new Box2(position, position + texture.Size * 4).Contains(point)) + continue; + + return true; + } + + return false; + } + + protected override void KeyBindDown(GUIBoundKeyEventArgs args) + { + base.KeyBindDown(args); + + OnPiecePressed?.Invoke(args, this); + } + + protected override void KeyBindUp(GUIBoundKeyEventArgs args) + { + base.KeyBindUp(args); + + OnPieceUnpressed?.Invoke(args, this); + } + + private Texture? GetTexture(IReadOnlyList boxes, Vector2i position, Direction corner) + { + var top = !boxes.Contains(position - Vector2i.Up); + var bottom = !boxes.Contains(position - Vector2i.Down); + var left = !boxes.Contains(position + Vector2i.Left); + var right = !boxes.Contains(position + Vector2i.Right); + + switch (corner) + { + case Direction.NorthEast: + if (top && right) + return _topRightTexture; + if (top) + return _topTexture; + if (right) + return _rightTexture; + return _centerTexture; + case Direction.NorthWest: + if (top && left) + return _topLeftTexture; + if (top) + return _topTexture; + if (left) + return _leftTexture; + return _centerTexture; + case Direction.SouthEast: + if (bottom && right) + return _bottomRightTexture; + if (bottom) + return _bottomTexture; + if (right) + return _rightTexture; + return _centerTexture; + case Direction.SouthWest: + if (bottom && left) + return _bottomLeftTexture; + if (bottom) + return _bottomTexture; + if (left) + return _leftTexture; + return _centerTexture; + default: + return null; + } + } + + public static Vector2 GetCenterOffset(Entity entity, ItemStorageLocation location, IEntityManager entMan) + { + var boxSize = entMan.System().GetAdjustedItemShape(entity, location).GetBoundingBox().Size; + var actualSize = new Vector2(boxSize.X + 1, boxSize.Y + 1); + return actualSize * new Vector2i(8, 8); + } +} diff --git a/Content.Client/UserInterface/Systems/Storage/Controls/StorageContainer.cs b/Content.Client/UserInterface/Systems/Storage/Controls/StorageContainer.cs new file mode 100644 index 0000000000..12bce74352 --- /dev/null +++ b/Content.Client/UserInterface/Systems/Storage/Controls/StorageContainer.cs @@ -0,0 +1,431 @@ +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Numerics; +using Content.Client.Hands.Systems; +using Content.Client.Items.Systems; +using Content.Client.Storage.Systems; +using Content.Shared.Input; +using Content.Shared.Item; +using Content.Shared.Storage; +using Robust.Client.Graphics; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.CustomControls; +using Robust.Shared.Timing; +using Robust.Shared.Utility; + +namespace Content.Client.UserInterface.Systems.Storage.Controls; + +public sealed class StorageContainer : BaseWindow +{ + [Dependency] private readonly IEntityManager _entity = default!; + private readonly StorageUIController _storageController; + + public EntityUid? StorageEntity; + + private readonly GridContainer _pieceGrid; + private readonly GridContainer _backgroundGrid; + private readonly GridContainer _sidebar; + + public event Action? OnPiecePressed; + public event Action? OnPieceUnpressed; + + private readonly string _emptyTexturePath = "Storage/tile_empty"; + private Texture? _emptyTexture; + private readonly string _blockedTexturePath = "Storage/tile_blocked"; + private Texture? _blockedTexture; + private readonly string _exitTexturePath = "Storage/exit"; + private Texture? _exitTexture; + private readonly string _backTexturePath = "Storage/back"; + private Texture? _backTexture; + private readonly string _sidebarTopTexturePath = "Storage/sidebar_top"; + private Texture? _sidebarTopTexture; + private readonly string _sidebarMiddleTexturePath = "Storage/sidebar_mid"; + private Texture? _sidebarMiddleTexture; + private readonly string _sidebarBottomTexturePath = "Storage/sidebar_bottom"; + private Texture? _sidebarBottomTexture; + private readonly string _sidebarFatTexturePath = "Storage/sidebar_fat"; + private Texture? _sidebarFatTexture; + + public StorageContainer() + { + IoCManager.InjectDependencies(this); + + _storageController = UserInterfaceManager.GetUIController(); + + OnThemeUpdated(); + + MouseFilter = MouseFilterMode.Stop; + + _sidebar = new GridContainer + { + HSeparationOverride = 0, + VSeparationOverride = 0, + Columns = 1 + }; + + _pieceGrid = new GridContainer + { + HSeparationOverride = 0, + VSeparationOverride = 0 + }; + + _backgroundGrid = new GridContainer + { + HSeparationOverride = 0, + VSeparationOverride = 0 + }; + + var container = new BoxContainer + { + Orientation = BoxContainer.LayoutOrientation.Vertical, + Children = + { + new BoxContainer + { + Orientation = BoxContainer.LayoutOrientation.Horizontal, + Children = + { + _sidebar, + new Control + { + Children = + { + _backgroundGrid, + _pieceGrid + } + } + } + } + } + }; + + AddChild(container); + } + + protected override void OnThemeUpdated() + { + base.OnThemeUpdated(); + + _emptyTexture = Theme.ResolveTextureOrNull(_emptyTexturePath)?.Texture; + _blockedTexture = Theme.ResolveTextureOrNull(_blockedTexturePath)?.Texture; + _exitTexture = Theme.ResolveTextureOrNull(_exitTexturePath)?.Texture; + _backTexture = Theme.ResolveTextureOrNull(_backTexturePath)?.Texture; + _sidebarTopTexture = Theme.ResolveTextureOrNull(_sidebarTopTexturePath)?.Texture; + _sidebarMiddleTexture = Theme.ResolveTextureOrNull(_sidebarMiddleTexturePath)?.Texture; + _sidebarBottomTexture = Theme.ResolveTextureOrNull(_sidebarBottomTexturePath)?.Texture; + _sidebarFatTexture = Theme.ResolveTextureOrNull(_sidebarFatTexturePath)?.Texture; + } + + public void UpdateContainer(Entity? entity) + { + Visible = entity != null; + StorageEntity = entity; + if (entity == null) + return; + + BuildGridRepresentation(entity.Value); + } + + private void BuildGridRepresentation(Entity entity) + { + var comp = entity.Comp; + if (!comp.Grid.Any()) + return; + + var boundingGrid = comp.Grid.GetBoundingBox(); + + _backgroundGrid.Children.Clear(); + _backgroundGrid.Rows = boundingGrid.Height + 1; + _backgroundGrid.Columns = boundingGrid.Width + 1; + for (var y = boundingGrid.Bottom; y <= boundingGrid.Top; y++) + { + for (var x = boundingGrid.Left; x <= boundingGrid.Right; x++) + { + var texture = comp.Grid.Contains(x, y) + ? _emptyTexture + : _blockedTexture; + + _backgroundGrid.AddChild(new TextureRect + { + Texture = texture, + TextureScale = new Vector2(2, 2) + }); + } + } + + #region Sidebar + _sidebar.Children.Clear(); + _sidebar.Rows = boundingGrid.Height + 1; + var exitButton = new TextureButton + { + TextureNormal = _entity.System().OpenStorageAmount == 1 + ?_exitTexture + : _backTexture, + Scale = new Vector2(2, 2), + }; + exitButton.OnPressed += _ => + { + Close(); + }; + var exitContainer = new BoxContainer + { + Children = + { + new TextureRect + { + Texture = boundingGrid.Height != 0 + ? _sidebarTopTexture + : _sidebarFatTexture, + TextureScale = new Vector2(2, 2), + Children = + { + exitButton + } + } + } + }; + _sidebar.AddChild(exitContainer); + for (var i = 0; i < boundingGrid.Height - 1; i++) + { + _sidebar.AddChild(new TextureRect + { + Texture = _sidebarMiddleTexture, + TextureScale = new Vector2(2, 2), + }); + } + + if (boundingGrid.Height > 0) + { + _sidebar.AddChild(new TextureRect + { + Texture = _sidebarBottomTexture, + TextureScale = new Vector2(2, 2), + }); + } + + #endregion + + BuildItemPieces(); + } + + public void BuildItemPieces() + { + if (!_entity.TryGetComponent(StorageEntity, out var storageComp)) + return; + + if (!storageComp.Grid.Any()) + return; + + var boundingGrid = storageComp.Grid.GetBoundingBox(); + var size = _emptyTexture!.Size * 2; + + //todo. at some point, we may want to only rebuild the pieces that have actually received new data. + + _pieceGrid.Children.Clear(); + _pieceGrid.Rows = boundingGrid.Height + 1; + _pieceGrid.Columns = boundingGrid.Width + 1; + for (var y = boundingGrid.Bottom; y <= boundingGrid.Top; y++) + { + for (var x = boundingGrid.Left; x <= boundingGrid.Right; x++) + { + var currentPosition = new Vector2i(x, y); + var item = storageComp.StoredItems + .Where(pair => pair.Value.Position == currentPosition) + .FirstOrNull(); + + var control = new Control + { + MinSize = size + }; + + if (item != null) + { + var itemEnt = _entity.GetEntity(item.Value.Key); + + if (_entity.TryGetComponent(itemEnt, out var itemEntComponent)) + { + var gridPiece = new ItemGridPiece((itemEnt, itemEntComponent), item.Value.Value, _entity) + { + MinSize = size, + }; + gridPiece.OnPiecePressed += OnPiecePressed; + gridPiece.OnPieceUnpressed += OnPieceUnpressed; + + control.AddChild(gridPiece); + } + } + + _pieceGrid.AddChild(control); + } + } + } + + protected override void FrameUpdate(FrameEventArgs args) + { + base.FrameUpdate(args); + + if (!IsOpen) + return; + + var itemSystem = _entity.System(); + var storageSystem = _entity.System(); + var handsSystem = _entity.System(); + + foreach (var child in _backgroundGrid.Children) + { + child.ModulateSelfOverride = Color.FromHex("#222222"); + } + + if (UserInterfaceManager.CurrentlyHovered is StorageContainer con && con != this) + return; + + if (!_entity.TryGetComponent(StorageEntity, out var storageComponent)) + return; + + EntityUid currentEnt; + ItemStorageLocation currentLocation; + var usingInHand = false; + if (_storageController.IsDragging && _storageController.DraggingGhost is { } dragging) + { + currentEnt = dragging.Entity; + currentLocation = dragging.Location; + } + else if (handsSystem.GetActiveHandEntity() is { } handEntity && + storageSystem.CanInsert(StorageEntity.Value, handEntity, out _, storageComp: storageComponent, ignoreLocation: true)) + { + currentEnt = handEntity; + currentLocation = new ItemStorageLocation(_storageController.DraggingRotation, Vector2i.Zero); + usingInHand = true; + } + else + { + return; + } + + if (!_entity.TryGetComponent(currentEnt, out var itemComp)) + return; + + var origin = GetMouseGridPieceLocation((currentEnt, itemComp), currentLocation); + + var itemShape = itemSystem.GetAdjustedItemShape( + (currentEnt, itemComp), + currentLocation.Rotation, + origin); + var itemBounding = itemShape.GetBoundingBox(); + + var validLocation = storageSystem.ItemFitsInGridLocation( + (currentEnt, itemComp), + (StorageEntity.Value, storageComponent), + origin, + currentLocation.Rotation); + + var validColor = usingInHand ? Color.Goldenrod : Color.Green; + + for (var y = itemBounding.Bottom; y <= itemBounding.Top; y++) + { + for (var x = itemBounding.Left; x <= itemBounding.Right; x++) + { + if (TryGetBackgroundCell(x, y, out var cell) && itemShape.Contains(x, y)) + { + cell.ModulateSelfOverride = validLocation ? validColor : Color.Red; + } + } + } + } + + protected override DragMode GetDragModeFor(Vector2 relativeMousePos) + { + if (_storageController.StaticStorageUIEnabled) + return DragMode.None; + + if (_sidebar.SizeBox.Contains(relativeMousePos - _sidebar.Position)) + { + return DragMode.Move; + } + + return DragMode.None; + } + + public Vector2i GetMouseGridPieceLocation(Entity entity, ItemStorageLocation location) + { + var origin = Vector2i.Zero; + + if (StorageEntity != null) + origin = _entity.GetComponent(StorageEntity.Value).Grid.GetBoundingBox().BottomLeft; + + var textureSize = (Vector2) _emptyTexture!.Size * 2; + var position = ((UserInterfaceManager.MousePositionScaled.Position + - _backgroundGrid.GlobalPosition + - ItemGridPiece.GetCenterOffset(entity, location, _entity) * 2 + + textureSize / 2f) + / textureSize).Floored() + origin; + return position; + } + + public bool TryGetBackgroundCell(int x, int y, [NotNullWhen(true)] out Control? cell) + { + cell = null; + + if (!_entity.TryGetComponent(StorageEntity, out var storageComponent)) + return false; + var boundingBox = storageComponent.Grid.GetBoundingBox(); + x -= boundingBox.Left; + y -= boundingBox.Bottom; + + if (x < 0 || + x >= _backgroundGrid.Columns || + y < 0 || + y >= _backgroundGrid.Rows) + { + return false; + } + + cell = _backgroundGrid.GetChild(y * _backgroundGrid.Columns + x); + return true; + } + + protected override void KeyBindDown(GUIBoundKeyEventArgs args) + { + base.KeyBindDown(args); + + if (!IsOpen) + return; + + var storageSystem = _entity.System(); + var handsSystem = _entity.System(); + + if (args.Function == ContentKeyFunctions.MoveStoredItem && StorageEntity != null) + { + if (handsSystem.GetActiveHandEntity() is { } handEntity && + storageSystem.CanInsert(StorageEntity.Value, handEntity, out _)) + { + var pos = GetMouseGridPieceLocation((handEntity, null), + new ItemStorageLocation(_storageController.DraggingRotation, Vector2i.Zero)); + + var insertLocation = new ItemStorageLocation(_storageController.DraggingRotation, pos); + if (storageSystem.ItemFitsInGridLocation( + (handEntity, null), + (StorageEntity.Value, null), + insertLocation)) + { + _entity.RaisePredictiveEvent(new StorageInsertItemIntoLocationEvent( + _entity.GetNetEntity(handEntity), + _entity.GetNetEntity(StorageEntity.Value), + insertLocation)); + args.Handle(); + } + } + } + } + + public override void Close() + { + base.Close(); + + if (StorageEntity == null) + return; + + _entity.System().CloseStorageUI(StorageEntity.Value); + } +} diff --git a/Content.Client/UserInterface/Systems/Storage/StorageUIController.cs b/Content.Client/UserInterface/Systems/Storage/StorageUIController.cs new file mode 100644 index 0000000000..d3bbd826ba --- /dev/null +++ b/Content.Client/UserInterface/Systems/Storage/StorageUIController.cs @@ -0,0 +1,327 @@ +using System.Numerics; +using Content.Client.Examine; +using Content.Client.Hands.Systems; +using Content.Client.Interaction; +using Content.Client.Storage.Systems; +using Content.Client.UserInterface.Systems.Hotbar.Widgets; +using Content.Client.UserInterface.Systems.Storage.Controls; +using Content.Client.Verbs.UI; +using Content.Shared.CCVar; +using Content.Shared.Input; +using Content.Shared.Interaction; +using Content.Shared.Item; +using Content.Shared.Storage; +using Robust.Client.Input; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controllers; +using Robust.Client.UserInterface.Controls; +using Robust.Shared.Configuration; +using Robust.Shared.Input; +using Robust.Shared.Timing; + +namespace Content.Client.UserInterface.Systems.Storage; + +public sealed class StorageUIController : UIController, IOnSystemChanged +{ + [Dependency] private readonly IConfigurationManager _configuration = default!; + [Dependency] private readonly IEntityManager _entity = default!; + [Dependency] private readonly IInputManager _input = default!; + [Dependency] private readonly IUserInterfaceManager _ui = default!; + + private readonly DragDropHelper _menuDragHelper; + private StorageContainer? _container; + + private Vector2? _lastContainerPosition; + + private HotbarGui? Hotbar => UIManager.GetActiveUIWidgetOrNull(); + + public ItemGridPiece? DraggingGhost; + public Angle DraggingRotation = Angle.Zero; + public bool StaticStorageUIEnabled; + + public bool IsDragging => _menuDragHelper.IsDragging; + public ItemGridPiece? CurrentlyDragging => _menuDragHelper.Dragged; + + public StorageUIController() + { + _menuDragHelper = new DragDropHelper(OnMenuBeginDrag, OnMenuContinueDrag, OnMenuEndDrag); + } + + public override void Initialize() + { + base.Initialize(); + + _configuration.OnValueChanged(CCVars.StaticStorageUI, OnStaticStorageChanged, true); + } + + public void OnSystemLoaded(StorageSystem system) + { + _input.FirstChanceOnKeyEvent += OnMiddleMouse; + system.StorageUpdated += OnStorageUpdated; + system.StorageOrderChanged += OnStorageOrderChanged; + } + + public void OnSystemUnloaded(StorageSystem system) + { + _input.FirstChanceOnKeyEvent -= OnMiddleMouse; + system.StorageUpdated -= OnStorageUpdated; + system.StorageOrderChanged -= OnStorageOrderChanged; + } + + private void OnStorageOrderChanged(Entity? nullEnt) + { + if (_container == null) + return; + + _container.UpdateContainer(nullEnt); + + if (IsDragging) + _menuDragHelper.EndDrag(); + + if (nullEnt is not null) + { + if (_lastContainerPosition == null) + { + _container.OpenCenteredAt(new Vector2(0.5f, 0.75f)); + } + else + { + _container.Open(); + + if (!StaticStorageUIEnabled) + LayoutContainer.SetPosition(_container, _lastContainerPosition.Value); + } + + if (StaticStorageUIEnabled) + { + // we have to orphan it here because Open() sets the parent. + _container.Orphan(); + Hotbar?.StorageContainer.AddChild(_container); + } + } + else + { + _lastContainerPosition = _container.GlobalPosition; + _container.Close(); + } + } + + private void OnStaticStorageChanged(bool obj) + { + if (StaticStorageUIEnabled == obj) + return; + + StaticStorageUIEnabled = obj; + + if (_container == null) + return; + + if (!_container.IsOpen) + return; + + _container.Orphan(); + if (StaticStorageUIEnabled) + { + Hotbar?.StorageContainer.AddChild(_container); + _lastContainerPosition = null; + } + else + { + _ui.WindowRoot.AddChild(_container); + } + } + + /// One might ask, Hey Emo, why are you parsing raw keyboard input just to rotate a rectangle? + /// The answer is, that input bindings regarding mouse inputs are always intercepted by the UI, + /// thus, if i want to be able to rotate my damn piece anywhere on the screen, + /// I have to side-step all of the input handling. Cheers. + private void OnMiddleMouse(KeyEventArgs keyEvent, KeyEventType type) + { + if (keyEvent.Handled) + return; + + if (type != KeyEventType.Down) + return; + + //todo there's gotta be a method for this in InputManager just expose it to content I BEG. + if (!_input.TryGetKeyBinding(ContentKeyFunctions.RotateStoredItem, out var binding)) + return; + if (binding.BaseKey != keyEvent.Key) + return; + + if (keyEvent.Shift && + !(binding.Mod1 == Keyboard.Key.Shift || + binding.Mod2 == Keyboard.Key.Shift || + binding.Mod3 == Keyboard.Key.Shift)) + return; + + if (keyEvent.Alt && + !(binding.Mod1 == Keyboard.Key.Alt || + binding.Mod2 == Keyboard.Key.Alt || + binding.Mod3 == Keyboard.Key.Alt)) + return; + + if (keyEvent.Control && + !(binding.Mod1 == Keyboard.Key.Control || + binding.Mod2 == Keyboard.Key.Control || + binding.Mod3 == Keyboard.Key.Control)) + return; + + if (!IsDragging && _entity.System().GetActiveHandEntity() == null) + return; + + //clamp it to a cardinal. + DraggingRotation = (DraggingRotation + Math.PI / 2f).GetCardinalDir().ToAngle(); + if (DraggingGhost != null) + DraggingGhost.Location.Rotation = DraggingRotation; + + if (IsDragging || (_container != null && UIManager.CurrentlyHovered == _container)) + keyEvent.Handle(); + } + + private void OnStorageUpdated(Entity uid) + { + if (_container?.StorageEntity != uid) + return; + + _container.BuildItemPieces(); + } + + public void RegisterStorageContainer(StorageContainer container) + { + if (_container != null) + { + container.OnPiecePressed -= OnPiecePressed; + container.OnPieceUnpressed -= OnPieceUnpressed; + } + + _container = container; + container.OnPiecePressed += OnPiecePressed; + container.OnPieceUnpressed += OnPieceUnpressed; + + if (!StaticStorageUIEnabled) + _container.Orphan(); + } + + private void OnPiecePressed(GUIBoundKeyEventArgs args, ItemGridPiece control) + { + if (IsDragging || !_container?.IsOpen == true) + return; + + if (args.Function == ContentKeyFunctions.MoveStoredItem) + { + _menuDragHelper.MouseDown(control); + _menuDragHelper.Update(0f); + + args.Handle(); + } + else if (args.Function == ContentKeyFunctions.ExamineEntity) + { + _entity.System().DoExamine(control.Entity); + args.Handle(); + } + else if (args.Function == EngineKeyFunctions.UseSecondary) + { + UIManager.GetUIController().OpenVerbMenu(control.Entity); + args.Handle(); + } + else if (args.Function == ContentKeyFunctions.ActivateItemInWorld) + { + _entity.EntityNetManager?.SendSystemNetworkMessage( + new InteractInventorySlotEvent(_entity.GetNetEntity(control.Entity), altInteract: false)); + args.Handle(); + } + else if (args.Function == ContentKeyFunctions.AltActivateItemInWorld) + { + _entity.RaisePredictiveEvent(new InteractInventorySlotEvent(_entity.GetNetEntity(control.Entity), altInteract: true)); + args.Handle(); + } + } + + private void OnPieceUnpressed(GUIBoundKeyEventArgs args, ItemGridPiece control) + { + if (_container?.StorageEntity is not { } storageEnt) + return; + + if (args.Function == ContentKeyFunctions.MoveStoredItem) + { + if (DraggingGhost is { } draggingGhost) + { + var position = _container.GetMouseGridPieceLocation(draggingGhost.Entity, draggingGhost.Location); + _entity.RaisePredictiveEvent(new StorageSetItemLocationEvent( + _entity.GetNetEntity(draggingGhost.Entity), + _entity.GetNetEntity(storageEnt), + new ItemStorageLocation(DraggingRotation, position))); + _container?.BuildItemPieces(); + } + else //if we just clicked, then take it out of the bag. + { + _entity.RaisePredictiveEvent(new StorageInteractWithItemEvent( + _entity.GetNetEntity(control.Entity), + _entity.GetNetEntity(storageEnt))); + } + _menuDragHelper.EndDrag(); + args.Handle(); + } + } + + private bool OnMenuBeginDrag() + { + if (_menuDragHelper.Dragged is not { } dragged) + return false; + + DraggingRotation = dragged.Location.Rotation; + DraggingGhost = new ItemGridPiece( + (dragged.Entity, _entity.GetComponent(dragged.Entity)), + dragged.Location, + _entity); + DraggingGhost.MouseFilter = Control.MouseFilterMode.Ignore; + DraggingGhost.Visible = true; + DraggingGhost.Orphan(); + + UIManager.PopupRoot.AddChild(DraggingGhost); + SetDraggingRotation(); + return true; + } + + private bool OnMenuContinueDrag(float frameTime) + { + if (DraggingGhost == null) + return false; + SetDraggingRotation(); + return true; + } + + private void SetDraggingRotation() + { + if (DraggingGhost == null) + return; + + var offset = ItemGridPiece.GetCenterOffset( + (DraggingGhost.Entity, null), + new ItemStorageLocation(DraggingRotation, Vector2i.Zero), + _entity); + + // I don't know why it divides the position by 2. Hope this helps! -emo + LayoutContainer.SetPosition(DraggingGhost, UIManager.MousePositionScaled.Position / 2 - offset ); + } + + private void OnMenuEndDrag() + { + if (DraggingGhost == null) + return; + DraggingGhost.Visible = false; + DraggingGhost = null; + DraggingRotation = Angle.Zero; + } + + public override void FrameUpdate(FrameEventArgs args) + { + base.FrameUpdate(args); + + _menuDragHelper.Update(args.DeltaSeconds); + + if (!StaticStorageUIEnabled && _container?.Parent != null) + _lastContainerPosition = _container.GlobalPosition; + } +} diff --git a/Content.IntegrationTests/Tests/StorageTest.cs b/Content.IntegrationTests/Tests/StorageTest.cs index c618616cd2..659b310661 100644 --- a/Content.IntegrationTests/Tests/StorageTest.cs +++ b/Content.IntegrationTests/Tests/StorageTest.cs @@ -128,15 +128,7 @@ namespace Content.IntegrationTests.Tests if (maxSize == null) continue; - if (storage.MaxSlots != null) - { - Assert.That(GetFillSize(fill, true, protoMan, itemSys), Is.LessThanOrEqualTo(storage.MaxSlots), - $"{proto.ID} storage fill has too many items."); - } - else - { - Assert.That(size, Is.LessThanOrEqualTo(storage.MaxTotalWeight), $"{proto.ID} storage fill is too large."); - } + Assert.That(size, Is.LessThanOrEqualTo(storage.Grid.GetArea()), $"{proto.ID} storage fill is too large."); foreach (var entry in fill.Contents) { @@ -210,7 +202,7 @@ namespace Content.IntegrationTests.Tests if (proto.TryGetComponent("Item", out var item)) - return itemSystem.GetItemSizeWeight(item.Size) * entry.Amount; + return itemSystem.GetItemShape(item).GetArea() * entry.Amount; Assert.Fail($"Prototype is missing item comp: {entry.PrototypeId}"); return 0; diff --git a/Content.Server/Chemistry/EntitySystems/ChemMasterSystem.cs b/Content.Server/Chemistry/EntitySystems/ChemMasterSystem.cs index 5b91d9456b..cbb84be83c 100644 --- a/Content.Server/Chemistry/EntitySystems/ChemMasterSystem.cs +++ b/Content.Server/Chemistry/EntitySystems/ChemMasterSystem.cs @@ -355,7 +355,7 @@ namespace Content.Server.Chemistry.EntitySystems return (Name(pill), quantity); })).ToList(); - return new ContainerInfo(name, _storageSystem.GetCumulativeItemSizes(container.Value, storage), storage.MaxTotalWeight) + return new ContainerInfo(name, _storageSystem.GetCumulativeItemAreas((container.Value, storage)), storage.Grid.GetArea()) { Entities = pills }; diff --git a/Content.Server/Storage/EntitySystems/StorageSystem.Fill.cs b/Content.Server/Storage/EntitySystems/StorageSystem.Fill.cs index 500a3fb100..10278cc805 100644 --- a/Content.Server/Storage/EntitySystems/StorageSystem.Fill.cs +++ b/Content.Server/Storage/EntitySystems/StorageSystem.Fill.cs @@ -1,5 +1,7 @@ +using System.Linq; using Content.Server.Spawners.Components; using Content.Server.Storage.Components; +using Content.Shared.Item; using Content.Shared.Prototypes; using Content.Shared.Storage; using Content.Shared.Storage.Components; @@ -15,14 +17,70 @@ public sealed partial class StorageSystem if (component.Contents.Count == 0) return; - TryComp(uid, out var storageComp); - TryComp(uid, out var entityStorageComp); - - if (entityStorageComp == null && storageComp == null) + if (TryComp(uid, out var storageComp)) + { + FillStorage((uid, component, storageComp)); + } + else if (TryComp(uid, out var entityStorageComp)) + { + FillEntityStorage((uid, component, entityStorageComp)); + } + else { Log.Error($"StorageFillComponent couldn't find any StorageComponent ({uid})"); - return; } + } + + private void FillStorage(Entity entity) + { + var (uid, component, storage) = entity; + + if (!Resolve(uid, ref component, ref storage)) + return; + + var coordinates = Transform(uid).Coordinates; + + var spawnItems = EntitySpawnCollection.GetSpawns(component.Contents, Random); + + var items = new List>(); + foreach (var spawnPrototype in spawnItems) + { + var ent = Spawn(spawnPrototype, coordinates); + + // No, you are not allowed to fill a container with entity spawners. + DebugTools.Assert(!_prototype.Index(spawnPrototype) + .HasComponent(typeof(RandomSpawnerComponent))); + + if (!TryComp(ent, out var itemComp)) + { + Log.Error($"Tried to fill {ToPrettyString(entity)} with non-item {spawnPrototype}."); + Del(ent); + continue; + } + + items.Add((ent, itemComp)); + } + + // we order the items from biggest to smallest to try and reduce poor placement in the grid. + var sortedItems = items + .OrderByDescending(x => ItemSystem.GetItemShape(x.Comp).GetArea()); + + foreach (var ent in sortedItems) + { + if (Insert(uid, ent, out _, out var reason, storageComp: storage, playSound: false)) + continue; + + Log.Error($"Tried to StorageFill {ToPrettyString(ent)} inside {ToPrettyString(uid)} but can't. reason: {reason}"); + Del(ent); + } + } + + private void FillEntityStorage(Entity entity) + { + var (uid, component, entityStorageComp) = entity; + + if (!Resolve(uid, ref component, ref entityStorageComp)) + return; var coordinates = Transform(uid).Coordinates; @@ -32,18 +90,14 @@ public sealed partial class StorageSystem // No, you are not allowed to fill a container with entity spawners. DebugTools.Assert(!_prototype.Index(item) .HasComponent(typeof(RandomSpawnerComponent))); - var ent = EntityManager.SpawnEntity(item, coordinates); + var ent = Spawn(item, coordinates); // handle depending on storage component, again this should be unified after ECS if (entityStorageComp != null && EntityStorage.Insert(ent, uid, entityStorageComp)) continue; - var reason = string.Empty; - if (storageComp != null && Insert(uid, ent, out _, out reason, storageComp: storageComp, playSound: false)) - continue; - - Log.Error($"Tried to StorageFill {item} inside {ToPrettyString(uid)} but can't. Reason: {Loc.GetString(reason ?? "no reason.")}"); - EntityManager.DeleteEntity(ent); + Log.Error($"Tried to StorageFill {item} inside {ToPrettyString(uid)} but can't."); + Del(ent); } } } diff --git a/Content.Shared/CCVar/CCVars.cs b/Content.Shared/CCVar/CCVars.cs index a8adbe4873..999ef21e25 100644 --- a/Content.Shared/CCVar/CCVars.cs +++ b/Content.Shared/CCVar/CCVars.cs @@ -1655,6 +1655,12 @@ namespace Content.Shared.CCVar public static readonly CVarDef ToggleWalk = CVarDef.Create("control.toggle_walk", false, CVar.CLIENTONLY | CVar.ARCHIVE); + /// + /// Whether or not the storage UI is static and bound to the hotbar, or unbound and allowed to be dragged anywhere. + /// + public static readonly CVarDef StaticStorageUI = + CVarDef.Create("control.static_storage_ui", true, CVar.CLIENTONLY | CVar.ARCHIVE); + /* * UPDATE */ diff --git a/Content.Shared/ContainerHeld/ContainerHeldSystem.cs b/Content.Shared/ContainerHeld/ContainerHeldSystem.cs index e0ecfe1992..db8ad5e5e1 100644 --- a/Content.Shared/ContainerHeld/ContainerHeldSystem.cs +++ b/Content.Shared/ContainerHeld/ContainerHeldSystem.cs @@ -29,7 +29,7 @@ public sealed class ContainerHeldSystem : EntitySystem { return; } - if (_storage.GetCumulativeItemSizes(uid, storage) >= comp.Threshold) + if (_storage.GetCumulativeItemAreas(uid) >= comp.Threshold) { _item.SetHeldPrefix(uid, "full", item); _appearance.SetData(uid, ToggleVisuals.Toggled, true, appearance); diff --git a/Content.Shared/Input/ContentKeyFunctions.cs b/Content.Shared/Input/ContentKeyFunctions.cs index f4307cd058..7a5a6e82d8 100644 --- a/Content.Shared/Input/ContentKeyFunctions.cs +++ b/Content.Shared/Input/ContentKeyFunctions.cs @@ -32,6 +32,8 @@ namespace Content.Shared.Input public static readonly BoundKeyFunction SmartEquipBelt = "SmartEquipBelt"; public static readonly BoundKeyFunction OpenAHelp = "OpenAHelp"; public static readonly BoundKeyFunction SwapHands = "SwapHands"; + public static readonly BoundKeyFunction MoveStoredItem = "MoveStoredItem"; + public static readonly BoundKeyFunction RotateStoredItem = "RotateStoredItem"; public static readonly BoundKeyFunction ThrowItemInHand = "ThrowItemInHand"; public static readonly BoundKeyFunction TryPullObject = "TryPullObject"; public static readonly BoundKeyFunction MovePulledObject = "MovePulledObject"; diff --git a/Content.Shared/Item/ItemComponent.cs b/Content.Shared/Item/ItemComponent.cs index ef4d451320..21926882a0 100644 --- a/Content.Shared/Item/ItemComponent.cs +++ b/Content.Shared/Item/ItemComponent.cs @@ -34,6 +34,13 @@ public sealed partial class ItemComponent : Component [ViewVariables(VVAccess.ReadWrite)] [DataField("sprite")] public string? RsiPath; + + /// + /// An optional override for the shape of the item within the grid storage. + /// If null, a default shape will be used based on . + /// + [DataField, AutoNetworkedField] + public List? Shape; } [Serializable, NetSerializable] diff --git a/Content.Shared/Item/ItemSizePrototype.cs b/Content.Shared/Item/ItemSizePrototype.cs index e498809593..5b3d4d8f91 100644 --- a/Content.Shared/Item/ItemSizePrototype.cs +++ b/Content.Shared/Item/ItemSizePrototype.cs @@ -24,6 +24,12 @@ public sealed partial class ItemSizePrototype : IPrototype, IComparable + /// The default inventory shape associated with this item size. + /// + [DataField(required: true)] + public IReadOnlyList DefaultShape = new List(); + public int CompareTo(ItemSizePrototype? other) { if (other is not { } otherItemSize) diff --git a/Content.Shared/Item/SharedItemSystem.cs b/Content.Shared/Item/SharedItemSystem.cs index 21679bbd72..c718b21e0f 100644 --- a/Content.Shared/Item/SharedItemSystem.cs +++ b/Content.Shared/Item/SharedItemSystem.cs @@ -2,6 +2,7 @@ using Content.Shared.Hands.EntitySystems; using Content.Shared.Interaction; using Content.Shared.Verbs; using Content.Shared.Examine; +using Content.Shared.Storage; using JetBrains.Annotations; using Robust.Shared.Containers; using Robust.Shared.GameStates; @@ -150,4 +151,58 @@ public abstract class SharedItemSystem : EntitySystem { return GetSizePrototype(size).Weight; } + + /// + /// Gets the default shape of an item. + /// + public IReadOnlyList GetItemShape(Entity uid) + { + if (!Resolve(uid, ref uid.Comp)) + return new Box2i[] { }; + + return uid.Comp.Shape ?? GetSizePrototype(uid.Comp.Size).DefaultShape; + } + + /// + /// Gets the default shape of an item. + /// + public IReadOnlyList GetItemShape(ItemComponent component) + { + return component.Shape ?? GetSizePrototype(component.Size).DefaultShape; + } + + /// + /// Gets the shape of an item, adjusting for rotation and offset. + /// + public IReadOnlyList GetAdjustedItemShape(Entity entity, ItemStorageLocation location) + { + return GetAdjustedItemShape(entity, location.Rotation, location.Position); + } + + /// + /// Gets the shape of an item, adjusting for rotation and offset. + /// + public IReadOnlyList GetAdjustedItemShape(Entity entity, Angle rotation, Vector2i position) + { + if (!Resolve(entity, ref entity.Comp)) + return new Box2i[] { }; + + var shapes = GetItemShape(entity); + var boundingShape = shapes.GetBoundingBox(); + var boundingCenter = ((Box2) boundingShape).Center; + var matty = Matrix3.CreateTransform(boundingCenter, rotation); + var drift = boundingShape.BottomLeft - matty.TransformBox(boundingShape).BottomLeft; + + var adjustedShapes = new List(); + foreach (var shape in shapes) + { + var transformed = matty.TransformBox(shape).Translated(drift); + var floored = new Box2i(transformed.BottomLeft.Floored(), transformed.TopRight.Floored()); + var translated = floored.Translated(position); + + adjustedShapes.Add(translated); + } + + return adjustedShapes; + } } diff --git a/Content.Shared/Storage/EntitySystems/SharedStorageSystem.cs b/Content.Shared/Storage/EntitySystems/SharedStorageSystem.cs index 31f6ae5c7b..2f6550bf2e 100644 --- a/Content.Shared/Storage/EntitySystems/SharedStorageSystem.cs +++ b/Content.Shared/Storage/EntitySystems/SharedStorageSystem.cs @@ -1,10 +1,10 @@ +using System.Diagnostics.CodeAnalysis; using System.Linq; using Content.Shared.ActionBlocker; using Content.Shared.CombatMode; using Content.Shared.Containers.ItemSlots; using Content.Shared.Destructible; using Content.Shared.DoAfter; -using Content.Shared.FixedPoint; using Content.Shared.Hands.Components; using Content.Shared.Hands.EntitySystems; using Content.Shared.Implants.Components; @@ -17,7 +17,6 @@ using Content.Shared.Stacks; using Content.Shared.Storage.Components; using Content.Shared.Timing; using Content.Shared.Verbs; -using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; using Robust.Shared.Containers; using Robust.Shared.Map; @@ -35,15 +34,16 @@ public abstract class SharedStorageSystem : EntitySystem [Dependency] private readonly EntityLookupSystem _entityLookupSystem = default!; [Dependency] protected readonly SharedEntityStorageSystem EntityStorage = default!; [Dependency] private readonly SharedInteractionSystem _interactionSystem = default!; - [Dependency] private readonly SharedItemSystem _item = default!; + [Dependency] protected readonly SharedItemSystem ItemSystem = default!; [Dependency] private readonly SharedPopupSystem _popupSystem = default!; [Dependency] private readonly SharedHandsSystem _sharedHandsSystem = default!; [Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!; [Dependency] private readonly SharedAppearanceSystem _appearance = default!; [Dependency] protected readonly SharedAudioSystem Audio = default!; [Dependency] private readonly SharedCombatModeSystem _combatMode = default!; - [Dependency] protected readonly SharedTransformSystem _transform = default!; + [Dependency] protected readonly SharedTransformSystem TransformSystem = default!; [Dependency] private readonly SharedStackSystem _stack = default!; + [Dependency] private readonly SharedUserInterfaceSystem _ui = default!; [Dependency] protected readonly UseDelaySystem UseDelay = default!; private EntityQuery _itemQuery; @@ -69,17 +69,18 @@ public abstract class SharedStorageSystem : EntitySystem SubscribeLocalEvent(OnImplantActivate); SubscribeLocalEvent(AfterInteract); SubscribeLocalEvent(OnDestroy); - SubscribeLocalEvent(OnInsertItemMessage); SubscribeLocalEvent(OnBoundUIOpen); SubscribeLocalEvent(OnStackCountChanged); - SubscribeLocalEvent(OnContainerModified); - SubscribeLocalEvent(OnContainerModified); + SubscribeLocalEvent(OnEntInserted); + SubscribeLocalEvent(OnEntRemoved); SubscribeLocalEvent(OnInsertAttempt); SubscribeLocalEvent(OnDoAfter); - SubscribeLocalEvent(OnInteractWithItem); + SubscribeAllEvent(OnInteractWithItem); + SubscribeAllEvent(OnSetItemLocation); + SubscribeAllEvent(OnInsertItemIntoLocation); } private void OnComponentInit(EntityUid uid, StorageComponent storageComp, ComponentInit args) @@ -132,7 +133,7 @@ public abstract class SharedStorageSystem : EntitySystem PlayerInsertHeldEntity(uid, args.User, storageComp); // Always handle it, even if insertion fails. // We don't want to trigger any AfterInteract logic here. - // Example bug: placing wires if item doesn't fit in backpack. + // Example issue would be placing wires if item doesn't fit in backpack. args.Handled = true; } @@ -145,6 +146,7 @@ public abstract class SharedStorageSystem : EntitySystem return; OpenStorageUI(uid, args.User, storageComp); + args.Handled = true; } /// @@ -152,11 +154,11 @@ public abstract class SharedStorageSystem : EntitySystem /// private void OnImplantActivate(EntityUid uid, StorageComponent storageComp, OpenStorageImplantEvent args) { - // TODO: Make this an action or something. - if (args.Handled || !_xformQuery.TryGetComponent(uid, out var xform)) + if (args.Handled) return; - OpenStorageUI(uid, xform.ParentUid, storageComp); + OpenStorageUI(uid, args.Performer, storageComp); + args.Handled = true; } /// @@ -224,8 +226,8 @@ public abstract class SharedStorageSystem : EntitySystem var position = EntityCoordinates.FromMap( parent.IsValid() ? parent : uid, - transformEnt.MapPosition, - _transform + TransformSystem.GetMapCoordinates(transformEnt), + TransformSystem ); args.Handled = true; @@ -270,8 +272,8 @@ public abstract class SharedStorageSystem : EntitySystem var position = EntityCoordinates.FromMap( xform.ParentUid.IsValid() ? xform.ParentUid : uid, - new MapCoordinates(_transform.GetWorldPosition(targetXform), targetXform.MapID), - _transform + new MapCoordinates(TransformSystem.GetWorldPosition(targetXform), targetXform.MapID), + TransformSystem ); var angle = targetXform.LocalRotation; @@ -284,7 +286,7 @@ public abstract class SharedStorageSystem : EntitySystem } } - // If we picked up atleast one thing, play a sound and do a cool animation! + // If we picked up at least one thing, play a sound and do a cool animation! if (successfullyInserted.Count > 0) { Audio.PlayPvs(component.StorageInsertSound, uid); @@ -300,7 +302,7 @@ public abstract class SharedStorageSystem : EntitySystem private void OnDestroy(EntityUid uid, StorageComponent storageComp, DestructionEventArgs args) { - var coordinates = _transform.GetMoverCoordinates(uid); + var coordinates = TransformSystem.GetMoverCoordinates(uid); // Being destroyed so need to recalculate. _containerSystem.EmptyContainer(storageComp.Container, destination: coordinates); @@ -311,16 +313,24 @@ public abstract class SharedStorageSystem : EntitySystem /// item in the user's hand if it is currently empty, or interact with the item using the user's currently /// held item. /// - private void OnInteractWithItem(EntityUid uid, StorageComponent storageComp, StorageInteractWithItemEvent args) + private void OnInteractWithItem(StorageInteractWithItemEvent msg, EntitySessionEventArgs args) { - if (args.Session.AttachedEntity is not { } player) + if (args.SenderSession.AttachedEntity is not { } player) return; - var entity = GetEntity(args.InteractedItemUID); + var uid = GetEntity(msg.StorageUid); + var entity = GetEntity(msg.InteractedItemUid); + + if (!TryComp(uid, out var storageComp)) + return; + + if (!_ui.TryGetUi(uid, StorageComponent.StorageUiKey.Key, out var bui) || + !bui.SubscribedSessions.Contains(args.SenderSession)) + return; if (!Exists(entity)) { - Log.Error($"Player {args.Session} interacted with non-existent item {args.InteractedItemUID} stored in {ToPrettyString(uid)}"); + Log.Error($"Player {args.SenderSession} interacted with non-existent item {msg.InteractedItemUid} stored in {ToPrettyString(uid)}"); return; } @@ -346,12 +356,58 @@ public abstract class SharedStorageSystem : EntitySystem _interactionSystem.InteractUsing(player, hands.ActiveHandEntity.Value, entity, Transform(entity).Coordinates, checkCanInteract: false); } - private void OnInsertItemMessage(EntityUid uid, StorageComponent storageComp, StorageComponent.StorageInsertItemMessage args) + private void OnSetItemLocation(StorageSetItemLocationEvent msg, EntitySessionEventArgs args) { - if (args.Session.AttachedEntity == null) + if (args.SenderSession.AttachedEntity is not { } player) return; - PlayerInsertHeldEntity(uid, args.Session.AttachedEntity.Value, storageComp); + var storageEnt = GetEntity(msg.StorageEnt); + var itemEnt = GetEntity(msg.ItemEnt); + + if (!TryComp(storageEnt, out var storageComp)) + return; + + if (!_ui.TryGetUi(storageEnt, StorageComponent.StorageUiKey.Key, out var bui) || + !bui.SubscribedSessions.Contains(args.SenderSession)) + return; + + if (!Exists(itemEnt)) + { + Log.Error($"Player {args.SenderSession} set location of non-existent item {msg.ItemEnt} stored in {ToPrettyString(storageEnt)}"); + return; + } + + if (!_actionBlockerSystem.CanInteract(player, itemEnt)) + return; + + TrySetItemStorageLocation((itemEnt, null), (storageEnt, storageComp), msg.Location); + } + + 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(storageEnt, out var storageComp)) + return; + + if (!_ui.TryGetUi(storageEnt, StorageComponent.StorageUiKey.Key, out var bui) || + !bui.SubscribedSessions.Contains(args.SenderSession)) + return; + + if (!Exists(itemEnt)) + { + Log.Error($"Player {args.SenderSession} set location of non-existent item {msg.ItemEnt} stored in {ToPrettyString(storageEnt)}"); + return; + } + + if (!_actionBlockerSystem.CanInteract(player, itemEnt) || !_sharedHandsSystem.IsHolding(player, itemEnt, out _)) + return; + + InsertAt((storageEnt, storageComp), (itemEnt, null), msg.Location, out _, player); } private void OnBoundUIOpen(EntityUid uid, StorageComponent storageComp, BoundUIOpenedEvent args) @@ -363,17 +419,45 @@ public abstract class SharedStorageSystem : EntitySystem } } - private void OnContainerModified(EntityUid uid, StorageComponent component, ContainerModifiedMessage args) + private void OnEntInserted(Entity entity, ref EntInsertedIntoContainerMessage args) { // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract - if (component.Container == null) + if (entity.Comp.Container == null) return; if (args.Container.ID != StorageComponent.ContainerId) return; - UpdateAppearance((uid, component, null)); - UpdateUI((uid, component)); + if (!entity.Comp.StoredItems.ContainsKey(GetNetEntity(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[GetNetEntity(args.Entity)] = location.Value; + Dirty(entity, entity.Comp); + } + + UpdateAppearance((entity, entity.Comp, null)); + UpdateUI((entity, entity.Comp)); + } + + private void OnEntRemoved(Entity 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(GetNetEntity(args.Entity)); + Dirty(entity, entity.Comp); + + UpdateAppearance((entity, entity.Comp, null)); + UpdateUI((entity, entity.Comp)); } private void OnInsertAttempt(EntityUid uid, StorageComponent component, ContainerIsInsertingAttemptEvent args) @@ -396,18 +480,8 @@ public abstract class SharedStorageSystem : EntitySystem if (storage.Container == null) return; // component hasn't yet been initialized. - int capacity; - int used; - if (storage.MaxSlots == null) - { - used = GetCumulativeItemSizes(uid, storage); - capacity = storage.MaxTotalWeight; - } - else - { - capacity = storage.MaxSlots.Value; - used = storage.Container.ContainedEntities.Count; - } + var capacity = storage.Grid.GetArea(); + var used = GetCumulativeItemAreas((uid, storage)); _appearance.SetData(uid, StorageVisuals.StorageUsed, used, appearance); _appearance.SetData(uid, StorageVisuals.Capacity, capacity, appearance); @@ -450,8 +524,17 @@ public abstract class SharedStorageSystem : EntitySystem /// If returning false, the reason displayed to the player /// /// + /// + /// /// true if it can be inserted, false otherwise - public bool CanInsert(EntityUid uid, EntityUid insertEnt, out string? reason, StorageComponent? storageComp = null, ItemComponent? item = null, bool ignoreStacks = false) + 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)) { @@ -485,38 +568,58 @@ public abstract class SharedStorageSystem : EntitySystem return true; } - var maxSize = _item.GetSizePrototype(GetMaxItemSize((uid, storageComp))); - if (_item.GetSizePrototype(item.Size) > maxSize) + var maxSize = ItemSystem.GetSizePrototype(GetMaxItemSize((uid, storageComp))); + if (ItemSystem.GetSizePrototype(item.Size) > maxSize) { reason = "comp-storage-too-big"; return false; } if (TryComp(insertEnt, out var insertStorage) - && _item.GetSizePrototype(GetMaxItemSize((insertEnt, insertStorage))) >= maxSize) + && ItemSystem.GetSizePrototype(GetMaxItemSize((insertEnt, insertStorage))) >= maxSize) { reason = "comp-storage-too-big"; return false; } - if (storageComp.MaxSlots != null) + if (!ignoreLocation && !storageComp.StoredItems.ContainsKey(GetNetEntity(insertEnt))) { - if (storageComp.Container.ContainedEntities.Count >= storageComp.MaxSlots) + if (!TryGetAvailableGridSpace((uid, storageComp), (insertEnt, item), out _)) { reason = "comp-storage-insufficient-capacity"; return false; } } - else if (_item.GetItemSizeWeight(item.Size) + GetCumulativeItemSizes(uid, storageComp) > storageComp.MaxTotalWeight) - { - reason = "comp-storage-insufficient-capacity"; - return false; - } reason = null; return true; } + /// + /// Inserts into the storage container at a given location + /// + /// true if the entity was inserted, false otherwise. This will also return true if a stack was partially + /// inserted. + public bool InsertAt( + Entity uid, + Entity insertEnt, + ItemStorageLocation location, + out EntityUid? stackedEntity, + EntityUid? user = null, + bool playSound = true) + { + stackedEntity = null; + if (!Resolve(uid, ref uid.Comp)) + return false; + + if (!ItemFitsInGridLocation(insertEnt, uid, location)) + return false; + + uid.Comp.StoredItems[GetNetEntity(insertEnt)] = location; + Dirty(uid, uid.Comp); + return Insert(uid, insertEnt, out stackedEntity, out _, user: user, storageComp: uid.Comp, playSound: playSound); + } + /// /// Inserts into the storage container /// @@ -653,6 +756,148 @@ public abstract class SharedStorageSystem : EntitySystem return true; } + /// + /// Attempts to set the location of an item already inside of a storage container. + /// + public bool TrySetItemStorageLocation(Entity itemEnt, Entity 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[GetNetEntity(itemEnt)] = location; + Dirty(storageEnt, storageEnt.Comp); + return true; + } + + /// + /// Tries to find the first available spot on a storage grid. + /// starts at the top-left and goes right and down. + /// + public bool TryGetAvailableGridSpace( + Entity storageEnt, + Entity itemEnt, + [NotNullWhen(true)] out ItemStorageLocation? storageLocation) + { + storageLocation = null; + + if (!Resolve(storageEnt, ref storageEnt.Comp) || !Resolve(itemEnt, ref itemEnt.Comp)) + return false; + + var storageBounding = storageEnt.Comp.Grid.GetBoundingBox(); + + for (var y = storageBounding.Bottom; y <= storageBounding.Top; y++) + { + for (var x = storageBounding.Left; x <= storageBounding.Right; x++) + { + for (var angle = Angle.Zero; angle <= Angle.FromDegrees(360); angle += Math.PI / 2f) + { + var location = new ItemStorageLocation(angle, (x, y)); + if (ItemFitsInGridLocation(itemEnt, storageEnt, location)) + { + storageLocation = location; + return true; + } + } + } + } + + return false; + } + + /// + /// Checks if an item fits into a specific spot on a storage grid. + /// + public bool ItemFitsInGridLocation( + Entity itemEnt, + Entity storageEnt, + ItemStorageLocation location) + { + return ItemFitsInGridLocation(itemEnt, storageEnt, location.Position, location.Rotation); + } + + /// + /// Checks if an item fits into a specific spot on a storage grid. + /// + public bool ItemFitsInGridLocation( + Entity itemEnt, + Entity 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; + } + + /// + /// Checks if a space on a grid is valid and not occupied by any other pieces. + /// + public bool IsGridSpaceEmpty(Entity itemEnt, Entity 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 (netEnt, storedItem) in storageEnt.Comp.StoredItems) + { + var ent = GetEntity(netEnt); + + 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; + } + /// /// Returns true if there is enough space to theoretically fit another item. /// @@ -661,13 +906,7 @@ public abstract class SharedStorageSystem : EntitySystem if (!Resolve(uid, ref uid.Comp)) return false; - //todo maybe this shouldn't be authoritative over weight? idk. - if (uid.Comp.MaxSlots != null) - { - return uid.Comp.Container.ContainedEntities.Count < uid.Comp.MaxSlots || HasSpaceInStacks(uid); - } - - return GetCumulativeItemSizes(uid, uid.Comp) < uid.Comp.MaxTotalWeight || HasSpaceInStacks(uid); + return GetCumulativeItemAreas(uid) < uid.Comp.Grid.GetArea() || HasSpaceInStacks(uid); } private bool HasSpaceInStacks(Entity uid, string? stackType = null) @@ -695,17 +934,17 @@ public abstract class SharedStorageSystem : EntitySystem /// /// Returns the sum of all the ItemSizes of the items inside of a storage. /// - public int GetCumulativeItemSizes(EntityUid uid, StorageComponent? component = null) + public int GetCumulativeItemAreas(Entity entity) { - if (!Resolve(uid, ref component)) + if (!Resolve(entity, ref entity.Comp)) return 0; var sum = 0; - foreach (var item in component.Container.ContainedEntities) + foreach (var item in entity.Comp.Container.ContainedEntities) { if (!_itemQuery.TryGetComponent(item, out var itemComp)) continue; - sum += _item.GetItemSizeWeight(itemComp.Size); + sum += ItemSystem.GetItemShape((item, itemComp)).GetArea(); } return sum; @@ -722,7 +961,7 @@ public abstract class SharedStorageSystem : EntitySystem if (!_itemQuery.TryGetComponent(uid, out var item)) return DefaultStorageMaxItemSize; - var size = _item.GetSizePrototype(item.Size); + var size = ItemSystem.GetSizePrototype(item.Size); // if there is no max item size specified, the value used // is one below the item size of the storage entity, clamped at ItemSize.Tiny @@ -742,17 +981,6 @@ public abstract class SharedStorageSystem : EntitySystem } } - public FixedPoint2 GetStorageFillPercentage(Entity uid) - { - if (!Resolve(uid, ref uid.Comp)) - return 0; - - var slotPercent = FixedPoint2.New(uid.Comp.Container.ContainedEntities.Count) / uid.Comp.MaxSlots ?? FixedPoint2.Zero; - var weightPercent = FixedPoint2.New(GetCumulativeItemSizes(uid)) / uid.Comp.MaxTotalWeight; - - return FixedPoint2.Max(slotPercent, weightPercent); - } - /// /// Plays a clientside pickup animation for the specified uid. /// diff --git a/Content.Shared/Storage/ItemStorageLocation.cs b/Content.Shared/Storage/ItemStorageLocation.cs new file mode 100644 index 0000000000..a43be5a44f --- /dev/null +++ b/Content.Shared/Storage/ItemStorageLocation.cs @@ -0,0 +1,40 @@ +using Robust.Shared.Serialization; + +namespace Content.Shared.Storage; + +[DataDefinition, Serializable, NetSerializable] +public partial record struct ItemStorageLocation +{ + /// + /// The rotation, stored a cardinal direction in order to reduce rounding errors. + /// + [DataField] + private Direction _rotation; + + /// + /// The rotation of the piece in storage. + /// + public Angle Rotation + { + get => _rotation.ToAngle(); + set => _rotation = value.GetCardinalDir(); + } + + /// + /// Where the item is located in storage. + /// + [DataField] + public Vector2i Position; + + public ItemStorageLocation(Angle rotation, Vector2i position) + { + Rotation = rotation; + Position = position; + } + + public bool Equals(ItemStorageLocation? other) + { + return Rotation == other?.Rotation && + Position == other.Value.Position; + } +}; diff --git a/Content.Shared/Storage/StorageComponent.cs b/Content.Shared/Storage/StorageComponent.cs index 422b7f9989..99677cc6c1 100644 --- a/Content.Shared/Storage/StorageComponent.cs +++ b/Content.Shared/Storage/StorageComponent.cs @@ -5,6 +5,7 @@ using Robust.Shared.Audio; using Robust.Shared.Containers; using Robust.Shared.GameStates; using Robust.Shared.Map; +using Robust.Shared.Player; using Robust.Shared.Prototypes; using Robust.Shared.Serialization; @@ -26,11 +27,16 @@ namespace Content.Shared.Storage public Container Container = default!; /// - /// A limit for the cumulative ItemSize weights that can be inserted in this storage. - /// If MaxSlots is not null, then this is ignored. + /// A dictionary storing each entity to its position within the storage grid. /// [DataField, ViewVariables(VVAccess.ReadWrite), AutoNetworkedField] - public int MaxTotalWeight; + public Dictionary StoredItems = new(); + + /// + /// A list of boxes that comprise a combined grid that determines the location that items can be stored. + /// + [DataField, ViewVariables(VVAccess.ReadWrite), AutoNetworkedField] + public List Grid = new(); /// /// The maximum size item that can be inserted into this storage, @@ -39,12 +45,6 @@ namespace Content.Shared.Storage [Access(typeof(SharedStorageSystem))] public ProtoId? MaxItemSize; - /// - /// The max number of entities that can be inserted into this storage. - /// - [DataField, ViewVariables(VVAccess.ReadWrite), AutoNetworkedField] - public int? MaxSlots; - // TODO: Make area insert its own component. [DataField("quickInsert")] public bool QuickInsert; // Can insert storables by "attacking" them with the storage entity @@ -94,11 +94,6 @@ namespace Content.Shared.Storage [DataField("storageCloseSound")] public SoundSpecifier? StorageCloseSound; - [Serializable, NetSerializable] - public sealed class StorageInsertItemMessage : BoundUserInterfaceMessage - { - } - [Serializable, NetSerializable] public enum StorageUiKey { @@ -107,15 +102,54 @@ namespace Content.Shared.Storage } [Serializable, NetSerializable] - public sealed class StorageInteractWithItemEvent : BoundUserInterfaceMessage + public sealed class StorageInteractWithItemEvent : EntityEventArgs { - public readonly NetEntity InteractedItemUID; - public StorageInteractWithItemEvent(NetEntity interactedItemUID) + public readonly NetEntity InteractedItemUid; + + public readonly NetEntity StorageUid; + + public StorageInteractWithItemEvent(NetEntity interactedItemUid, NetEntity storageUid) { - InteractedItemUID = interactedItemUID; + InteractedItemUid = interactedItemUid; + StorageUid = storageUid; } } + [Serializable, NetSerializable] + public sealed class StorageSetItemLocationEvent : EntityEventArgs + { + public readonly NetEntity ItemEnt; + + public readonly NetEntity StorageEnt; + + public readonly ItemStorageLocation Location; + + public StorageSetItemLocationEvent(NetEntity itemEnt, NetEntity storageEnt, ItemStorageLocation location) + { + ItemEnt = itemEnt; + StorageEnt = storageEnt; + Location = location; + } + } + + [Serializable, NetSerializable] + public sealed class StorageInsertItemIntoLocationEvent : EntityEventArgs + { + public readonly NetEntity ItemEnt; + + public readonly NetEntity StorageEnt; + + public readonly ItemStorageLocation Location; + + public StorageInsertItemIntoLocationEvent(NetEntity itemEnt, NetEntity storageEnt, ItemStorageLocation location) + { + ItemEnt = itemEnt; + StorageEnt = storageEnt; + Location = location; + } + } + + /// /// Network event for displaying an animation of entities flying into a storage entity /// diff --git a/Content.Shared/Storage/StorageHelpers.cs b/Content.Shared/Storage/StorageHelpers.cs new file mode 100644 index 0000000000..bf0d32a0e5 --- /dev/null +++ b/Content.Shared/Storage/StorageHelpers.cs @@ -0,0 +1,76 @@ +namespace Content.Shared.Storage; + +public static class StorageHelper +{ + public static Box2i GetBoundingBox(this IReadOnlyList boxes) + { + if (boxes.Count == 0) + return new Box2i(); + + var firstBox = boxes[0]; + + if (boxes.Count == 1) + return firstBox; + + var bottom = firstBox.Bottom; + var left = firstBox.Left; + var top = firstBox.Top; + var right = firstBox.Right; + + for (var i = 1; i < boxes.Count; i++) + { + var box = boxes[i]; + + if (bottom > box.Bottom) + bottom = box.Bottom; + + if (left > box.Left) + left = box.Left; + + if (top < box.Top) + top = box.Top; + + if (right < box.Right) + right = box.Right; + } + return new Box2i(left, bottom, right, top); + } + + public static int GetArea(this IReadOnlyList boxes) + { + var area = 0; + var bounding = boxes.GetBoundingBox(); + for (var y = bounding.Bottom; y <= bounding.Top; y++) + { + for (var x = bounding.Left; x <= bounding.Right; x++) + { + if (boxes.Contains(x, y)) + area++; + } + } + + return area; + } + + public static bool Contains(this IReadOnlyList boxes, int x, int y) + { + foreach (var box in boxes) + { + if (box.Contains(x, y)) + return true; + } + + return false; + } + + public static bool Contains(this IReadOnlyList boxes, Vector2i point) + { + foreach (var box in boxes) + { + if (box.Contains(point)) + return true; + } + + return false; + } +} diff --git a/Resources/Locale/en-US/escape-menu/ui/options-menu.ftl b/Resources/Locale/en-US/escape-menu/ui/options-menu.ftl index ddf76ebb1a..7c541e0257 100644 --- a/Resources/Locale/en-US/escape-menu/ui/options-menu.ftl +++ b/Resources/Locale/en-US/escape-menu/ui/options-menu.ftl @@ -109,6 +109,9 @@ ui-options-function-alt-activate-item-in-world = Alternative activate item in wo ui-options-function-drop = Drop item ui-options-function-examine-entity = Examine ui-options-function-swap-hands = Swap hands +ui-options-function-move-stored-item = Move stored item +ui-options-function-rotate-stored-item = Rotate stored item +ui-options-static-storage-ui = Static storage UI ui-options-function-smart-equip-backpack = Smart-equip to backpack ui-options-function-smart-equip-belt = Smart-equip to belt diff --git a/Resources/Prototypes/Catalog/Fills/Backpacks/StarterGear/backpack.yml b/Resources/Prototypes/Catalog/Fills/Backpacks/StarterGear/backpack.yml index 07e6cb05d8..25a9ad8963 100644 --- a/Resources/Prototypes/Catalog/Fills/Backpacks/StarterGear/backpack.yml +++ b/Resources/Prototypes/Catalog/Fills/Backpacks/StarterGear/backpack.yml @@ -318,7 +318,8 @@ description: Holds the kit of CentComm's most feared agents. components: - type: Storage - maxTotalWeight: 56 + grid: + - 0,0,7,6 - type: StorageFill contents: - id: BoxSurvivalEngineering diff --git a/Resources/Prototypes/Catalog/Fills/Backpacks/StarterGear/satchel.yml b/Resources/Prototypes/Catalog/Fills/Backpacks/StarterGear/satchel.yml index b70ef28e76..87688dbe5b 100644 --- a/Resources/Prototypes/Catalog/Fills/Backpacks/StarterGear/satchel.yml +++ b/Resources/Prototypes/Catalog/Fills/Backpacks/StarterGear/satchel.yml @@ -234,9 +234,9 @@ - type: StorageFill contents: - id: BoxSurvival + - id: BoxForensicPad - id: Lighter - id: CigPackBlack - - id: BoxForensicPad - id: HandLabeler - type: entity diff --git a/Resources/Prototypes/Catalog/Fills/Backpacks/duffelbag.yml b/Resources/Prototypes/Catalog/Fills/Backpacks/duffelbag.yml index 06c7691dbf..75adf2d5d8 100644 --- a/Resources/Prototypes/Catalog/Fills/Backpacks/duffelbag.yml +++ b/Resources/Prototypes/Catalog/Fills/Backpacks/duffelbag.yml @@ -153,13 +153,13 @@ tags: [] # ignore "WhitelistChameleon" tag - type: StorageFill contents: + - id: ClothingOuterArmorBasic - id: ClothingHeadHatCentcom - id: ClothingEyesGlassesSunglasses - id: ClothingUniformJumpsuitCentcomOfficial - id: ClothingShoesBootsJack - id: ClothingHandsGlovesColorBlack - id: ClothingHeadsetAltCentComFake - - id: ClothingOuterArmorBasic - id: Paper - id: Pen - id: CentcomPDAFake @@ -198,8 +198,6 @@ name: syndicate pyjama duffel bag description: Contains 3 pairs of syndicate pyjamas and 3 plushies for the ultimate sleepover. components: - - type: Storage - maxTotalWeight: 44 - type: StorageFill contents: - id: ClothingUniformJumpsuitPyjamaSyndicateRed diff --git a/Resources/Prototypes/Catalog/Fills/Boxes/general.yml b/Resources/Prototypes/Catalog/Fills/Boxes/general.yml index ae5f01301d..7970083596 100644 --- a/Resources/Prototypes/Catalog/Fills/Boxes/general.yml +++ b/Resources/Prototypes/Catalog/Fills/Boxes/general.yml @@ -37,7 +37,8 @@ - state: box - state: light - type: Storage - maxTotalWeight: 24 + grid: + - 0,0,5,3 whitelist: components: - LightBulb @@ -60,7 +61,8 @@ - state: box - state: lighttube - type: Storage - maxTotalWeight: 24 + grid: + - 0,0,5,3 whitelist: components: - LightBulb @@ -85,7 +87,8 @@ - state: box - state: lightmixed - type: Storage - maxTotalWeight: 24 + grid: + - 0,0,5,3 whitelist: components: - LightBulb @@ -250,7 +253,8 @@ - id: TrashBag amount: 6 - type: Storage - maxTotalWeight: 24 + grid: + - 0,0,5,3 maxItemSize: Normal whitelist: tags: @@ -391,7 +395,8 @@ - id: HandheldGPSBasic amount: 6 - type: Storage - maxTotalWeight: 24 + grid: + - 0,0,5,3 - type: Sprite layers: - state: box @@ -419,7 +424,8 @@ - state: box - state: candle - type: Storage - maxTotalWeight: 30 + grid: + - 0,0,9,2 - type: StorageFill contents: - id: Candle @@ -443,7 +449,8 @@ - state: box - state: candle - type: Storage - maxTotalWeight: 30 + grid: + - 0,0,9,2 - type: StorageFill contents: - id: CandleSmall @@ -476,7 +483,8 @@ - id: DartYellow amount: 3 - type: Storage - maxTotalWeight: 24 + grid: + - 0,0,5,3 - type: Sprite layers: - state: box diff --git a/Resources/Prototypes/Catalog/Fills/Boxes/medical.yml b/Resources/Prototypes/Catalog/Fills/Boxes/medical.yml index e35e9086ff..c41753bdaf 100644 --- a/Resources/Prototypes/Catalog/Fills/Boxes/medical.yml +++ b/Resources/Prototypes/Catalog/Fills/Boxes/medical.yml @@ -94,7 +94,8 @@ id: BoxMouthSwab components: - type: Storage - maxTotalWeight: 20 + grid: + - 0,0,4,3 - type: StorageFill contents: - id: DiseaseSwab diff --git a/Resources/Prototypes/Catalog/Fills/Boxes/science.yml b/Resources/Prototypes/Catalog/Fills/Boxes/science.yml index b4bfb578c1..cd0320e088 100644 --- a/Resources/Prototypes/Catalog/Fills/Boxes/science.yml +++ b/Resources/Prototypes/Catalog/Fills/Boxes/science.yml @@ -5,14 +5,15 @@ description: A box full of beakers. components: - type: Storage - maxTotalWeight: 12 + grid: + - 0,0,3,2 maxItemSize: Normal - type: StorageFill contents: - - id: Beaker - amount: 2 - - id: LargeBeaker - amount: 2 + - id: LargeBeaker + amount: 2 + - id: Beaker + amount: 2 - type: Sprite layers: - state: box diff --git a/Resources/Prototypes/Catalog/Fills/Boxes/security.yml b/Resources/Prototypes/Catalog/Fills/Boxes/security.yml index 2fa5c43046..9975f4f3f0 100644 --- a/Resources/Prototypes/Catalog/Fills/Boxes/security.yml +++ b/Resources/Prototypes/Catalog/Fills/Boxes/security.yml @@ -50,7 +50,8 @@ description: A box full of zipties. components: - type: Storage - maxTotalWeight: 20 + grid: + - 0,0,4,3 whitelist: components: - Handcuff @@ -70,7 +71,8 @@ description: A box of forensic pads. components: - type: Storage - maxTotalWeight: 20 + grid: + - 0,0,4,3 - type: StorageFill contents: - id: ForensicPad diff --git a/Resources/Prototypes/Catalog/Fills/Items/belt.yml b/Resources/Prototypes/Catalog/Fills/Items/belt.yml index 0e4fa72eed..6417d88b2b 100644 --- a/Resources/Prototypes/Catalog/Fills/Items/belt.yml +++ b/Resources/Prototypes/Catalog/Fills/Items/belt.yml @@ -126,8 +126,9 @@ - type: Item size: Ginormous - type: Storage - maxSlots: 8 maxItemSize: Normal + grid: + - 0,0,7,1 - type: StorageFill contents: - id: ExGrenade diff --git a/Resources/Prototypes/Catalog/Fills/Items/briefcases.yml b/Resources/Prototypes/Catalog/Fills/Items/briefcases.yml index c36a4e6b79..ce2cf1e2b3 100644 --- a/Resources/Prototypes/Catalog/Fills/Items/briefcases.yml +++ b/Resources/Prototypes/Catalog/Fills/Items/briefcases.yml @@ -19,6 +19,8 @@ size: Ginormous - type: Storage maxItemSize: Huge + grid: + - 0,0,6,3 - type: StorageFill contents: - id: WeaponSniperHristov diff --git a/Resources/Prototypes/Entities/Clothing/Back/backpacks.yml b/Resources/Prototypes/Entities/Clothing/Back/backpacks.yml index 51b9b2e828..b8792211a2 100644 --- a/Resources/Prototypes/Entities/Clothing/Back/backpacks.yml +++ b/Resources/Prototypes/Entities/Clothing/Back/backpacks.yml @@ -14,8 +14,9 @@ slots: - back - type: Storage + grid: + - 0,0,6,3 maxItemSize: Huge - maxTotalWeight: 28 - type: ContainerContainer containers: storagebase: !type:Container @@ -197,7 +198,8 @@ - type: Sprite sprite: Clothing/Back/Backpacks/ertleader.rsi - type: Storage - maxTotalWeight: 44 + grid: + - 0,0,10,3 - type: entity parent: ClothingBackpackERTLeader @@ -264,7 +266,8 @@ size: Ginormous - type: Storage maxItemSize: Huge - maxTotalWeight: 200 #5 duffels worth of gear + grid: + - 0,0,19,9 - type: entity parent: ClothingBackpackClown @@ -278,3 +281,62 @@ - type: Unremoveable deleteOnDrop: false +# Debug +- type: entity + parent: ClothingBackpack + id: ClothingBackpackDebug + name: wackpack + description: What the fuck is this? + suffix: Debug + components: + - type: Storage + grid: + - 0,0,3,3 + - 5,0,7,2 + - 0,5,7,5 + - 6,4,7,5 + - 9,2,10,3 + - 9,5,9,5 + +- type: entity + parent: ClothingBackpack + id: ClothingBackpackDebug2 + name: big wackpack + description: What the fuck is this? + suffix: Debug + components: + - type: Storage + grid: + - 0,0,39,24 + +- type: entity + parent: ClothingBackpack + id: ClothingBackpackDebug3 + name: gay wackpack + description: What the fuck is this? + suffix: Debug + components: + - type: Storage + grid: + - 0,0,0,3 + - 0,0,2,0 + - 0,3,2,3 + - 2,1,2,1 + - 4,0,4,2 + - 6,0,6,2 + - 5,1,5,1 + - 5,3,5,3 + - 9,0,9,1 + - 8,2,8,3 + - 10,2,10,3 + +- type: entity + parent: ClothingBackpack + id: ClothingBackpackDebug4 + name: offset wackpack + description: What the fuck is this? + suffix: Debug + components: + - type: Storage + grid: + - 5,5,11,8 diff --git a/Resources/Prototypes/Entities/Clothing/Back/duffel.yml b/Resources/Prototypes/Entities/Clothing/Back/duffel.yml index 1562eda95e..190d05b777 100644 --- a/Resources/Prototypes/Entities/Clothing/Back/duffel.yml +++ b/Resources/Prototypes/Entities/Clothing/Back/duffel.yml @@ -8,7 +8,8 @@ sprite: Clothing/Back/Duffels/duffel.rsi - type: Storage maxItemSize: Huge - maxTotalWeight: 40 + grid: + - 0,0,9,4 - type: ClothingSpeedModifier walkModifier: 1 sprintModifier: 0.9 @@ -231,7 +232,8 @@ size: Ginormous - type: Storage maxItemSize: Huge - maxTotalWeight: 200 #5 duffels worth of gear + grid: + - 0,0,19,9 - type: ClothingSpeedModifier sprintModifier: 1 # makes its stats identical to other variants of bag of holding @@ -243,7 +245,6 @@ components: - type: Storage maxItemSize: Huge - maxTotalWeight: 40 - type: ClothingSpeedModifier walkModifier: 1 sprintModifier: 1 diff --git a/Resources/Prototypes/Entities/Clothing/Back/satchel.yml b/Resources/Prototypes/Entities/Clothing/Back/satchel.yml index dcb723393b..8b8cf2c622 100644 --- a/Resources/Prototypes/Entities/Clothing/Back/satchel.yml +++ b/Resources/Prototypes/Entities/Clothing/Back/satchel.yml @@ -171,4 +171,5 @@ size: Ginormous - type: Storage maxItemSize: Huge - maxTotalWeight: 200 #5 duffels worth of gear + grid: + - 0,0,19,9 diff --git a/Resources/Prototypes/Entities/Clothing/Belt/base_clothingbelt.yml b/Resources/Prototypes/Entities/Clothing/Belt/base_clothingbelt.yml index 6d94ab454b..614e739e05 100644 --- a/Resources/Prototypes/Entities/Clothing/Belt/base_clothingbelt.yml +++ b/Resources/Prototypes/Entities/Clothing/Belt/base_clothingbelt.yml @@ -22,8 +22,9 @@ id: ClothingBeltStorageBase components: - type: Storage - maxSlots: 7 maxItemSize: Normal + grid: + - 0,0,7,1 - type: Item size: Ginormous - type: ContainerContainer @@ -39,12 +40,12 @@ abstract: true parent: ClothingBeltBase id: ClothingBeltAmmoProviderBase - components: + components: - type: BallisticAmmoProvider mayTransfer: true - type: Item size: Ginormous - type: ContainerContainer - containers: + containers: ballistic-ammo: !type:Container - + diff --git a/Resources/Prototypes/Entities/Clothing/Belt/belts.yml b/Resources/Prototypes/Entities/Clothing/Belt/belts.yml index ebaa8cc123..5655a2639e 100644 --- a/Resources/Prototypes/Entities/Clothing/Belt/belts.yml +++ b/Resources/Prototypes/Entities/Clothing/Belt/belts.yml @@ -391,7 +391,8 @@ - type: Clothing sprite: Clothing/Belt/sheath.rsi - type: Storage - maxSlots: 1 + grid: + - 0,0,1,1 whitelist: tags: - CaptainSabre @@ -449,7 +450,8 @@ - type: Clothing sprite: Clothing/Belt/holster.rsi - type: Storage - maxSlots: 3 + grid: + - 0,0,3,1 - type: entity parent: ClothingBeltStorageBase @@ -554,7 +556,8 @@ - type: Clothing sprite: Clothing/Belt/wand.rsi - type: Storage - maxSlots: 8 + grid: + - 0,0,15,1 whitelist: tags: - WizardWand diff --git a/Resources/Prototypes/Entities/Clothing/Belt/quiver.yml b/Resources/Prototypes/Entities/Clothing/Belt/quiver.yml index e6685eb47a..6cbf5a69ca 100644 --- a/Resources/Prototypes/Entities/Clothing/Belt/quiver.yml +++ b/Resources/Prototypes/Entities/Clothing/Belt/quiver.yml @@ -12,7 +12,8 @@ visible: false - type: Clothing - type: Storage - maxSlots: 15 + grid: + - 0,0,7,3 maxItemSize: Small whitelist: tags: diff --git a/Resources/Prototypes/Entities/Clothing/Belt/waist_bags.yml b/Resources/Prototypes/Entities/Clothing/Belt/waist_bags.yml index be8f02d803..81fddf379b 100644 --- a/Resources/Prototypes/Entities/Clothing/Belt/waist_bags.yml +++ b/Resources/Prototypes/Entities/Clothing/Belt/waist_bags.yml @@ -9,8 +9,8 @@ - type: Clothing sprite: Clothing/Belt/waistbag_leather.rsi - type: Storage - maxSlots: 4 - maxItemSize: Small + grid: + - 0,0,3,1 #Colorization on worn items doesn't work. If this ever gets fixed, you can duplicate this entry and change the color on the sprite to add color variants. #- type: entity diff --git a/Resources/Prototypes/Entities/Clothing/Head/hats.yml b/Resources/Prototypes/Entities/Clothing/Head/hats.yml index 5274a69bdb..12deec76e9 100644 --- a/Resources/Prototypes/Entities/Clothing/Head/hats.yml +++ b/Resources/Prototypes/Entities/Clothing/Head/hats.yml @@ -208,7 +208,8 @@ - type: Clothing sprite: Clothing/Head/Hats/chefhat.rsi - type: Storage - maxSlots: 1 + grid: + - 0,0,0,0 - type: UserInterface interfaces: - key: enum.StorageUiKey.Key @@ -765,7 +766,8 @@ size: Small sprite: Clothing/Head/Hats/magician.rsi - type: Storage - maxSlots: 1 + grid: + - 0,0,0,0 - type: UserInterface interfaces: - key: enum.StorageUiKey.Key diff --git a/Resources/Prototypes/Entities/Clothing/OuterClothing/base_clothingouter.yml b/Resources/Prototypes/Entities/Clothing/OuterClothing/base_clothingouter.yml index 8b86d8e253..4fbdc88526 100644 --- a/Resources/Prototypes/Entities/Clothing/OuterClothing/base_clothingouter.yml +++ b/Resources/Prototypes/Entities/Clothing/OuterClothing/base_clothingouter.yml @@ -29,7 +29,8 @@ id: ClothingOuterStorageBase components: - type: Storage - maxTotalWeight: 6 + grid: + - 0,0,2,1 - type: ContainerContainer containers: storagebase: !type:Container diff --git a/Resources/Prototypes/Entities/Clothing/Shoes/base_clothingshoes.yml b/Resources/Prototypes/Entities/Clothing/Shoes/base_clothingshoes.yml index c7152b23d9..fcd14d2b9a 100644 --- a/Resources/Prototypes/Entities/Clothing/Shoes/base_clothingshoes.yml +++ b/Resources/Prototypes/Entities/Clothing/Shoes/base_clothingshoes.yml @@ -41,7 +41,8 @@ id: ClothingShoesStorageBase components: - type: Storage - maxSlots: 1 + grid: + - 0,0,1,1 maxItemSize: Normal - type: ContainerContainer containers: diff --git a/Resources/Prototypes/Entities/Debugging/item.yml b/Resources/Prototypes/Entities/Debugging/item.yml new file mode 100644 index 0000000000..e3c8ffddd2 --- /dev/null +++ b/Resources/Prototypes/Entities/Debugging/item.yml @@ -0,0 +1,21 @@ +- type: entity + parent: BaseItem + id: DebugItemShapeWeird + name: weirdly shaped item + description: What is it...? + suffix: DEBUG + components: + - type: Tag + tags: + - Debug + - type: Sprite + sprite: Objects/Misc/skub.rsi + state: icon + - type: Item + size: Tiny + shape: + - 0, 0, 2, 2 + - 1, 1, 1, 4 + - 1, 4, 6, 4 + - 6, 2, 6, 2 + - 5, 3, 5, 5 diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml b/Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml index 8d4919a977..bdb908af36 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml @@ -165,7 +165,8 @@ node: bot - type: Storage maxItemSize: Huge - maxTotalWeight: 40 + grid: + - 0,0,9,3 - type: Access groups: - Cargo diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_cans.yml b/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_cans.yml index fec821419b..a5a8393764 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_cans.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_cans.yml @@ -403,7 +403,8 @@ - type: Item size: Normal - type: Storage - maxSlots: 6 + grid: + - 0,0,5,1 whitelist: tags: - Cola diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/Containers/box.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/Containers/box.yml index 41aadf05fd..3e73714a63 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/Containers/box.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/Containers/box.yml @@ -38,7 +38,8 @@ map: ["pink-box6"] visible: false - type: Storage - maxSlots: 6 + grid: + - 0,0,2,1 whitelist: tags: - Donut @@ -118,7 +119,8 @@ map: ["box12"] visible: false - type: Storage - maxSlots: 12 + grid: + - 0,0,5,1 whitelist: tags: - Egg @@ -206,11 +208,9 @@ map: ["enum.StorageVisualLayers.Door"] # TODO make these entitystorage again + placeablesurface after entity storage ECS gets merged. - type: Storage - maxSlots: 1 + grid: + - 0,0,1,1 maxItemSize: Normal - whitelist: - tags: - - Pizza - type: Item sprite: Objects/Consumable/Food/Baked/pizza.rsi heldPrefix: box @@ -297,7 +297,8 @@ map: ["box6"] visible: false - type: Storage - maxSlots: 6 + grid: + - 0,0,2,1 - type: Item sprite: Objects/Consumable/Food/Baked/nuggets.rsi size: Small @@ -331,7 +332,8 @@ sprite: Objects/Consumable/Food/Baked/donkpocket.rsi state: box - type: Storage - maxTotalWeight: 12 + grid: + - 0,0,3,2 whitelist: tags: - DonkPocket @@ -459,6 +461,9 @@ - type: Item sprite: Objects/Storage/Happyhonk/clown.rsi heldPrefix: box + - type: Storage + grid: + - 0,0,3,3 - type: Tag tags: - Trash @@ -583,7 +588,8 @@ name: syndicate snack box components: - type: Storage - maxSlots: 9 + grid: + - 0,0,5,2 - type: StorageFill contents: # toy diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Smokeables/Cigarettes/cartons.yml b/Resources/Prototypes/Entities/Objects/Consumable/Smokeables/Cigarettes/cartons.yml index bea963b4b6..54bc12d4fa 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Smokeables/Cigarettes/cartons.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Smokeables/Cigarettes/cartons.yml @@ -14,7 +14,8 @@ sprite: Objects/Consumable/Smokeables/Cigarettes/Cartons/green.rsi size: Normal - type: Storage - maxSlots: 6 + grid: + - 0,0,5,1 - type: StorageFill contents: - id: CigPackGreen diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Smokeables/Cigarettes/packs.yml b/Resources/Prototypes/Entities/Objects/Consumable/Smokeables/Cigarettes/packs.yml index 6bc14cfb77..77c121e4c8 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Smokeables/Cigarettes/packs.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Smokeables/Cigarettes/packs.yml @@ -43,7 +43,8 @@ Steel: 50 - type: SpaceGarbage - type: Storage - maxSlots: 5 + grid: + - 0,0,4,1 - type: Item size: Small - type: StorageFill @@ -108,8 +109,8 @@ Steel: 50 - type: SpaceGarbage - type: Storage - maxSlots: 10 - maxTotalWeight: 20 + grid: + - 0,0,4,1 - type: Item size: Small - type: StorageFill diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Smokeables/Cigarettes/rolling_paper.yml b/Resources/Prototypes/Entities/Objects/Consumable/Smokeables/Cigarettes/rolling_paper.yml index 324a28caca..44edce5e07 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Smokeables/Cigarettes/rolling_paper.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Smokeables/Cigarettes/rolling_paper.yml @@ -5,11 +5,12 @@ description: A pack of thin pieces of paper used to make fine smokeables. components: - type: Storage + grid: + - 0,0,3,1 whitelist: tags: - RollingPaper - CigFilter - maxSlots: 20 - type: StorageFill contents: - id: PaperRolling diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Smokeables/Cigars/case.yml b/Resources/Prototypes/Entities/Objects/Consumable/Smokeables/Cigars/case.yml index eede84cd4b..813c1352b4 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Smokeables/Cigars/case.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Smokeables/Cigars/case.yml @@ -35,7 +35,8 @@ map: ["cigar8"] visible: false - type: Storage - maxSlots: 8 + grid: + - 0,0,3,1 - type: Item sprite: Objects/Consumable/Smokeables/Cigars/case.rsi size: Small diff --git a/Resources/Prototypes/Entities/Objects/Fun/candy_bucket.yml b/Resources/Prototypes/Entities/Objects/Fun/candy_bucket.yml index d78def0c94..d8f11cdea2 100644 --- a/Resources/Prototypes/Entities/Objects/Fun/candy_bucket.yml +++ b/Resources/Prototypes/Entities/Objects/Fun/candy_bucket.yml @@ -23,7 +23,8 @@ False: {state: empty_icon} - type: Storage maxItemSize: Small - maxTotalWeight: 10 + grid: + - 0,0,3,2 whitelist: components: - Pill diff --git a/Resources/Prototypes/Entities/Objects/Fun/crayons.yml b/Resources/Prototypes/Entities/Objects/Fun/crayons.yml index 46cc2dbd4f..e693f9b7c4 100644 --- a/Resources/Prototypes/Entities/Objects/Fun/crayons.yml +++ b/Resources/Prototypes/Entities/Objects/Fun/crayons.yml @@ -235,7 +235,8 @@ sprite: Objects/Fun/crayons.rsi state: box - type: Storage - maxSlots: 7 + grid: + - 0,0,6,0 maxItemSize: Tiny - type: Item sprite: Objects/Fun/crayons.rsi diff --git a/Resources/Prototypes/Entities/Objects/Fun/dice_bag.yml b/Resources/Prototypes/Entities/Objects/Fun/dice_bag.yml index f2d547dd8c..1a579572d9 100644 --- a/Resources/Prototypes/Entities/Objects/Fun/dice_bag.yml +++ b/Resources/Prototypes/Entities/Objects/Fun/dice_bag.yml @@ -19,7 +19,8 @@ - type: Item size: Small - type: Storage - maxTotalWeight: 8 + grid: + - 0,0,3,1 whitelist: tags: - Dice @@ -33,4 +34,5 @@ sprite: Objects/Fun/dice.rsi state: magicdicebag - type: Storage - maxTotalWeight: 20 + grid: + - 0,0,4,3 diff --git a/Resources/Prototypes/Entities/Objects/Misc/box.yml b/Resources/Prototypes/Entities/Objects/Misc/box.yml index 442e712851..edb1a81239 100644 --- a/Resources/Prototypes/Entities/Objects/Misc/box.yml +++ b/Resources/Prototypes/Entities/Objects/Misc/box.yml @@ -8,9 +8,12 @@ - type: Item sprite: Objects/Storage/boxes.rsi size: Large + shape: + - 0,0,2,2 - type: Storage - maxTotalWeight: 8 maxItemSize: Small + grid: + - 0,0,2,2 - type: ContainerContainer containers: storagebase: !type:Container diff --git a/Resources/Prototypes/Entities/Objects/Misc/briefcases.yml b/Resources/Prototypes/Entities/Objects/Misc/briefcases.yml index c04801f0c7..aa8823d70d 100644 --- a/Resources/Prototypes/Entities/Objects/Misc/briefcases.yml +++ b/Resources/Prototypes/Entities/Objects/Misc/briefcases.yml @@ -7,7 +7,8 @@ - type: Item size: Ginormous - type: Storage - maxTotalWeight: 24 + grid: + - 0,0,5,3 - type: Tag tags: - Briefcase @@ -33,12 +34,6 @@ components: - type: Item size: Huge - - type: Storage - maxSlots: 6 - maxTotalWeight: 24 - - type: Tag - tags: - - Briefcase - type: entity name: brown briefcase diff --git a/Resources/Prototypes/Entities/Objects/Misc/medalcase.yml b/Resources/Prototypes/Entities/Objects/Misc/medalcase.yml index d1ceae2c60..f178fffae0 100644 --- a/Resources/Prototypes/Entities/Objects/Misc/medalcase.yml +++ b/Resources/Prototypes/Entities/Objects/Misc/medalcase.yml @@ -11,8 +11,8 @@ sprite: Objects/Storage/medalcase.rsi size: Normal - type: Storage - maxSlots: 8 - maxTotalWeight: 16 + grid: + - 0,0,7,1 - type: StorageFill contents: - id: ClothingNeckGoldmedal diff --git a/Resources/Prototypes/Entities/Objects/Misc/paper.yml b/Resources/Prototypes/Entities/Objects/Misc/paper.yml index 34de8d7e30..a191666fb4 100644 --- a/Resources/Prototypes/Entities/Objects/Misc/paper.yml +++ b/Resources/Prototypes/Entities/Objects/Misc/paper.yml @@ -338,9 +338,9 @@ sprite: Objects/Misc/bureaucracy.rsi size: Small - type: Storage - maxSlots: 10 maxItemSize: Small - maxTotalWeight: 20 + grid: + - 0,0,4,3 whitelist: tags: - Document @@ -476,8 +476,8 @@ quickEquip: false sprite: Objects/Misc/clipboard.rsi - type: Storage - maxSlots: 15 - maxTotalWeight: 30 + grid: + - 0,0,5,3 whitelist: tags: - Document @@ -535,7 +535,8 @@ quickEquip: false sprite: Objects/Misc/qm_clipboard.rsi - type: Storage - maxSlots: 20 + grid: + - 0,0,4,3 quickInsert: true whitelist: tags: diff --git a/Resources/Prototypes/Entities/Objects/Misc/subdermal_implants.yml b/Resources/Prototypes/Entities/Objects/Misc/subdermal_implants.yml index 08ec019bf7..18edca6c71 100644 --- a/Resources/Prototypes/Entities/Objects/Misc/subdermal_implants.yml +++ b/Resources/Prototypes/Entities/Objects/Misc/subdermal_implants.yml @@ -118,7 +118,8 @@ components: - Hands # no use giving a mouse a storage implant, but a monkey is another story... - type: Storage - maxSlots: 4 + grid: + - 0,0,2,2 maxItemSize: Small - type: ContainerContainer containers: diff --git a/Resources/Prototypes/Entities/Objects/Specific/Chapel/bibles.yml b/Resources/Prototypes/Entities/Objects/Specific/Chapel/bibles.yml index 3e72257d5c..1a23a8791c 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Chapel/bibles.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Chapel/bibles.yml @@ -37,7 +37,8 @@ slots: - Belt - type: Storage - maxSlots: 1 + grid: + - 0,0,0,1 - type: UserInterface interfaces: - key: enum.StorageUiKey.Key diff --git a/Resources/Prototypes/Entities/Objects/Specific/Chemistry/chem_bag.yml b/Resources/Prototypes/Entities/Objects/Specific/Chemistry/chem_bag.yml index a221b2ca1f..4d35b51ffe 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Chemistry/chem_bag.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Chemistry/chem_bag.yml @@ -16,7 +16,8 @@ size: Ginormous - type: Storage maxItemSize: Normal # allow up to 5 large beakers / 10 beakers / 10 pill canisters - maxTotalWeight: 20 + grid: + - 0,0,4,3 quickInsert: true areaInsert: true whitelist: diff --git a/Resources/Prototypes/Entities/Objects/Specific/Hydroponics/tools.yml b/Resources/Prototypes/Entities/Objects/Specific/Hydroponics/tools.yml index 026c47f176..7e02106e6a 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Hydroponics/tools.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Hydroponics/tools.yml @@ -128,7 +128,8 @@ slots: - belt - type: Storage - maxSlots: 40 + grid: + - 0,0,7,4 maxItemSize: Small quickInsert: true areaInsert: true diff --git a/Resources/Prototypes/Entities/Objects/Specific/Janitorial/trashbag.yml b/Resources/Prototypes/Entities/Objects/Specific/Janitorial/trashbag.yml index 7f61cf69d4..567f91b15f 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Janitorial/trashbag.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Janitorial/trashbag.yml @@ -9,8 +9,9 @@ - state: icon-0 map: ["enum.StorageFillLayers.Fill"] - type: Storage - maxTotalWeight: 48 maxItemSize: Small + grid: + - 0,0,7,5 quickInsert: true areaInsert: true storageOpenSound: @@ -56,9 +57,9 @@ parent: TrashBagBlue components: - type: Storage - maxSlots: 100 maxItemSize: Huge - maxTotalWeight: 200 + grid: + - 0,0,19,9 quickInsert: true areaInsert: true areaInsertRadius: 1000 diff --git a/Resources/Prototypes/Entities/Objects/Specific/Kitchen/foodcarts.yml b/Resources/Prototypes/Entities/Objects/Specific/Kitchen/foodcarts.yml index 81f40c9d7c..ecd4cfd5d5 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Kitchen/foodcarts.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Kitchen/foodcarts.yml @@ -48,7 +48,8 @@ type: StorageBoundUserInterface - type: Storage maxItemSize: Normal - maxTotalWeight: 40 + grid: + - 0,0,7,4 - type: TileFrictionModifier modifier: 0.4 # makes it slide diff --git a/Resources/Prototypes/Entities/Objects/Specific/Librarian/books_bag.yml b/Resources/Prototypes/Entities/Objects/Specific/Librarian/books_bag.yml index 939af4a304..e4befcbb67 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Librarian/books_bag.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Librarian/books_bag.yml @@ -15,7 +15,8 @@ - type: Item size: Ginormous - type: Storage - maxSlots: 15 + grid: + - 0,0,7,3 quickInsert: true areaInsert: true whitelist: diff --git a/Resources/Prototypes/Entities/Objects/Specific/Medical/medkits.yml b/Resources/Prototypes/Entities/Objects/Specific/Medical/medkits.yml index 1e73c47f9a..d7f2231ec9 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Medical/medkits.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Medical/medkits.yml @@ -8,8 +8,9 @@ sprite: Objects/Specific/Medical/firstaidkits.rsi state: firstaid - type: Storage - maxTotalWeight: 8 maxItemSize: Small + grid: + - 0,0,3,1 - type: Item size: Large sprite: Objects/Specific/Medical/firstaidkits.rsi diff --git a/Resources/Prototypes/Entities/Objects/Specific/Research/rped.yml b/Resources/Prototypes/Entities/Objects/Specific/Research/rped.yml index dd9d771ebe..bbad13c831 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Research/rped.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Research/rped.yml @@ -15,7 +15,8 @@ - MachineUpgrading - type: PartExchanger - type: Storage - maxSlots: 30 + grid: + - 0,0,9,2 quickInsert: true areaInsert: true whitelist: diff --git a/Resources/Prototypes/Entities/Objects/Specific/Salvage/ore_bag.yml b/Resources/Prototypes/Entities/Objects/Specific/Salvage/ore_bag.yml index 231d82fb76..a36bfaf676 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Salvage/ore_bag.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Salvage/ore_bag.yml @@ -16,8 +16,9 @@ - type: Item size: Ginormous - type: Storage - maxSlots: 10 maxItemSize: Normal + grid: + - 0,0,9,3 quickInsert: true areaInsert: true whitelist: diff --git a/Resources/Prototypes/Entities/Objects/Specific/chemistry.yml b/Resources/Prototypes/Entities/Objects/Specific/chemistry.yml index e940b6cb9f..7ef0e6f5d2 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/chemistry.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/chemistry.yml @@ -387,7 +387,7 @@ id: SyringeCryostasis parent: BaseSyringe name: cryostasis syringe - description: A syringe used to contain chemicals or solutions without reactions. + description: A syringe used to contain chemicals or solutions without reactions. components: - type: Sprite layers: @@ -480,7 +480,8 @@ tags: - PillCanister - type: Storage - maxTotalWeight: 10 + grid: + - 0,0,4,1 quickInsert: true areaInsert: true areaInsertRadius: 1 diff --git a/Resources/Prototypes/Entities/Objects/Tools/matches.yml b/Resources/Prototypes/Entities/Objects/Tools/matches.yml index 98affd271c..e8601fcf35 100644 --- a/Resources/Prototypes/Entities/Objects/Tools/matches.yml +++ b/Resources/Prototypes/Entities/Objects/Tools/matches.yml @@ -4,8 +4,8 @@ abstract: true components: - type: Storage - maxSlots: 7 - maxTotalWeight: 14 + grid: + - 0,0,6,1 - type: Item size: Small @@ -83,12 +83,12 @@ heldPrefix: matchbox size: Small - type: Storage - maxSlots: 5 - maxTotalWeight: 5 + grid: + - 0,0,2,1 - type: StorageFill contents: - id: Matchstick - amount: 5 + amount: 6 - type: ItemCounter count: tags: [Matchstick] diff --git a/Resources/Prototypes/Entities/Objects/Tools/toolbox.yml b/Resources/Prototypes/Entities/Objects/Tools/toolbox.yml index 0acc760a57..30bcdf3739 100644 --- a/Resources/Prototypes/Entities/Objects/Tools/toolbox.yml +++ b/Resources/Prototypes/Entities/Objects/Tools/toolbox.yml @@ -8,9 +8,9 @@ sound: path: /Audio/Items/toolbox_drop.ogg - type: Storage - maxSlots: 7 maxItemSize: Normal - maxTotalWeight: 14 + grid: + - 0,0,6,3 - type: Item size: Ginormous - type: ItemCooldown @@ -117,8 +117,8 @@ sprite: Objects/Tools/Toolboxes/toolbox_syn.rsi - type: Storage maxItemSize: Huge - maxSlots: 8 - maxTotalWeight: 32 + grid: + - 0,0,7,3 - type: MeleeWeapon damage: types: diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/pneumatic_cannon.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/pneumatic_cannon.yml index c978d2da05..ac9d390065 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/pneumatic_cannon.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/pneumatic_cannon.yml @@ -32,9 +32,9 @@ container: storagebase - type: PneumaticCannon - type: Storage - maxSlots: 8 maxItemSize: Normal - maxTotalWeight: 32 + grid: + - 0,0,7,3 blacklist: tags: - CannonRestrict @@ -77,9 +77,9 @@ layers: - state: piecannon - type: Storage - maxSlots: 8 maxItemSize: Normal - maxTotalWeight: 32 + grid: + - 0,0,7,3 whitelist: components: - CreamPie @@ -126,9 +126,9 @@ - type: Item size: Ginormous - type: Storage - maxSlots: 100 maxItemSize: Ginormous - maxTotalWeight: 1600 + grid: + - 0,0,19,10 whitelist: tags: [] #dodging a test fail like the IRS - type: PneumaticCannon diff --git a/Resources/Prototypes/Entities/Structures/Furniture/bookshelf.yml b/Resources/Prototypes/Entities/Structures/Furniture/bookshelf.yml index 6d5e0e3dcd..c638fac207 100644 --- a/Resources/Prototypes/Entities/Structures/Furniture/bookshelf.yml +++ b/Resources/Prototypes/Entities/Structures/Furniture/bookshelf.yml @@ -47,7 +47,8 @@ - type: Pullable - type: Occluder - type: Storage - maxSlots: 32 + grid: + - 0,0,15,3 maxItemSize: Normal whitelist: tags: diff --git a/Resources/Prototypes/Entities/Structures/Furniture/dresser.yml b/Resources/Prototypes/Entities/Structures/Furniture/dresser.yml index 29061a06b1..e3629fcf07 100644 --- a/Resources/Prototypes/Entities/Structures/Furniture/dresser.yml +++ b/Resources/Prototypes/Entities/Structures/Furniture/dresser.yml @@ -25,7 +25,8 @@ - !type:DoActsBehavior acts: [ "Destruction" ] - type: Storage - maxSlots: 6 + grid: + - 0,0,5,3 maxItemSize: Normal - type: ContainerContainer containers: diff --git a/Resources/Prototypes/Entities/Structures/Storage/filing_cabinets.yml b/Resources/Prototypes/Entities/Structures/Storage/filing_cabinets.yml index f0ce9c46d3..df25ed5238 100644 --- a/Resources/Prototypes/Entities/Structures/Storage/filing_cabinets.yml +++ b/Resources/Prototypes/Entities/Structures/Storage/filing_cabinets.yml @@ -6,7 +6,8 @@ description: A cabinet for all your filing needs. components: - type: Storage - maxSlots: 12 + grid: + - 0,0,9,3 maxItemSize: Normal whitelist: tags: @@ -66,7 +67,8 @@ description: A small drawer for all your filing needs, Now with wheels! components: - type: Storage - maxSlots: 8 + grid: + - 0,0,7,2 maxItemSize: Normal whitelist: tags: diff --git a/Resources/Prototypes/Entities/Structures/Storage/ore_box.yml b/Resources/Prototypes/Entities/Structures/Storage/ore_box.yml index 6bad8c319f..961d7854c0 100644 --- a/Resources/Prototypes/Entities/Structures/Storage/ore_box.yml +++ b/Resources/Prototypes/Entities/Structures/Storage/ore_box.yml @@ -50,7 +50,8 @@ True: { visible: false } False: { visible: true } - type: Storage - maxSlots: 60 + grid: + - 0,0,9,5 maxItemSize: Normal storageOpenSound: /Audio/Effects/closetopen.ogg storageCloseSound: /Audio/Effects/closetclose.ogg diff --git a/Resources/Prototypes/Entities/Structures/Wallmounts/noticeboard.yml b/Resources/Prototypes/Entities/Structures/Wallmounts/noticeboard.yml index d5c330f47e..bad84227c8 100644 --- a/Resources/Prototypes/Entities/Structures/Wallmounts/noticeboard.yml +++ b/Resources/Prototypes/Entities/Structures/Wallmounts/noticeboard.yml @@ -38,7 +38,8 @@ - !type:DoActsBehavior acts: ["Destruction"] - type: Storage - maxSlots: 10 + grid: + - 0,0,4,3 maxItemSize: Small whitelist: tags: diff --git a/Resources/Prototypes/XenoArch/Effects/utility_effects.yml b/Resources/Prototypes/XenoArch/Effects/utility_effects.yml index c9297d901b..fdf28fe9a0 100644 --- a/Resources/Prototypes/XenoArch/Effects/utility_effects.yml +++ b/Resources/Prototypes/XenoArch/Effects/utility_effects.yml @@ -52,9 +52,9 @@ storagebase: !type:Container ents: [ ] - type: Storage - maxSlots: 5 maxItemSize: Huge - maxTotalWeight: 40 + grid: + - 0,0,10,5 - type: artifactEffect id: EffectPhasing diff --git a/Resources/Prototypes/item_size.yml b/Resources/Prototypes/item_size.yml index 47d1fe3db4..f02a7e9fd6 100644 --- a/Resources/Prototypes/item_size.yml +++ b/Resources/Prototypes/item_size.yml @@ -3,33 +3,45 @@ id: Tiny weight: 1 name: item-component-size-Tiny + defaultShape: + - 0,0,0,0 # Items that can fit inside of a standard pocket. - type: itemSize id: Small weight: 2 name: item-component-size-Small + defaultShape: + - 0,0,0,1 # Items that can fit inside of a standard bag. - type: itemSize id: Normal weight: 4 name: item-component-size-Normal + defaultShape: + - 0,0,1,1 # Items that are too large to fit inside of standard bags, but can worn in exterior slots or placed in custom containers. - type: itemSize id: Large weight: 8 name: item-component-size-Large + defaultShape: + - 0,0,3,1 # Items that are too large to place inside of any kind of container. - type: itemSize id: Huge weight: 16 name: item-component-size-Huge + defaultShape: + - 0,0,3,3 # Picture furry gf - type: itemSize id: Ginormous weight: 32 name: item-component-size-Ginormous + defaultShape: + - 0,0,5,5 diff --git a/Resources/Textures/Interface/Default/Storage/back.png b/Resources/Textures/Interface/Default/Storage/back.png new file mode 100644 index 0000000000000000000000000000000000000000..3fe8540952ecea793792e9636c6dd3775c50fb80 GIT binary patch literal 204 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|+B{txLo9mF zPCU)qpdjGF{&7+6jh4BA%yTcY>Sl|O9Q^E6saeiTXT%_-M>R8VmTMB;#T_l_SR~KJ!TBI zw04T92wFEzRas@0#&A<%)h?B!W6K-VpBN{I6ftbN?!xouhQ?H&ix@mz{an^LB{Ts5 D?mkcy literal 0 HcmV?d00001 diff --git a/Resources/Textures/Interface/Default/Storage/exit.png b/Resources/Textures/Interface/Default/Storage/exit.png new file mode 100644 index 0000000000000000000000000000000000000000..186c447d4b616ebc6eba8c13806613a55bb10cd7 GIT binary patch literal 208 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|x;$MRLo9le z6C|cAm^1UI{l+4n8zK<}g^8vtvt@+MSWnHF!}Efx@T6f{`z!_k8h=!K|s%P z2jdT(#Ds(ct9E{VAMYl@Tp-!RGr@FmMWclC1yf^#gih8V#?rkGehQleF7b4`r8p;) z{M?@Zzi=1B-IYI{7#vEl)qh_f<0&~^rp+noDZ|<=O%ZAi=Vkz1#o+1c=d#Wzp$Pyj CT}X=n literal 0 HcmV?d00001 diff --git a/Resources/Textures/Interface/Default/Storage/piece_bottom.png b/Resources/Textures/Interface/Default/Storage/piece_bottom.png new file mode 100644 index 0000000000000000000000000000000000000000..a860ddd1813e706ca009aa45539562c128e59f63 GIT binary patch literal 114 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1|*O0@9PFqjKx9jP7LeL$-D$|bUa-gLnNjq z_w@DsJ>S5-;lsy|e~zwxaCy1@8RpzWPfuU}aMIX-i<7hV&~gTbU7dX2Boe}!fch9b MUHx3vIVCg!07u6rXOW=RU!b`Rp00i_>zopr0E*@@=l}o! literal 0 HcmV?d00001 diff --git a/Resources/Textures/Interface/Default/Storage/piece_bottomRight.png b/Resources/Textures/Interface/Default/Storage/piece_bottomRight.png new file mode 100644 index 0000000000000000000000000000000000000000..59a49cb3cc829f91fd3a9d026604602f988d1940 GIT binary patch literal 147 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1|*O0@9PFqjKx9jP7LeL$-D$|d_7$pLnNjq z_w@DsJ>S4;Q}g4+`>om2_iz00@#CMP%NW=q>}r1<>T!Oj&kO{;J{7;dsKyANetP

BD)z4*}Q$iB}zScJb literal 0 HcmV?d00001 diff --git a/Resources/Textures/Interface/Default/Storage/piece_center.png b/Resources/Textures/Interface/Default/Storage/piece_center.png new file mode 100644 index 0000000000000000000000000000000000000000..27916d44c99b9525f11d7dbfc4b72a91f1808b99 GIT binary patch literal 100 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1|*O0@9PFqjKx9jP7LeL$-D$|S5-;lsy|e~zwxaCy1@8RvT}442IKLwOVTp8%?4@O1TaS?83{1ON_cAMOAE literal 0 HcmV?d00001 diff --git a/Resources/Textures/Interface/Default/Storage/piece_left.png b/Resources/Textures/Interface/Default/Storage/piece_left.png new file mode 100644 index 0000000000000000000000000000000000000000..a99eeada369514c0adfe576af0a88502d907aecf GIT binary patch literal 109 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1|*O0@9PFqjKx9jP7LeL$-D$|)ID7sLnNjq ziwFt*TC#NMX;ppw<7XOJHyn_tsj;d5sP*H^%hMN385xpa@rO>-5jgS4;Q}d(a;jOKw|407#QL*9VVaC(T{o_AqOK_<%FqF>{tlGSDo(WJFgQu&X J%Q~loCIH3#B>VsX literal 0 HcmV?d00001 diff --git a/Resources/Textures/Interface/Default/Storage/piece_top.png b/Resources/Textures/Interface/Default/Storage/piece_top.png new file mode 100644 index 0000000000000000000000000000000000000000..219ec30e24bd3bb701847cca649d85a6c84fe49d GIT binary patch literal 112 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1|*O0@9PFqjKx9jP7LeL$-D$|v^-rLLnNjq ziwFt*I?}+tAtUo;(Zf&$ef{G-;f(U~=i~Qq+136sx}e6uP<)#^)ZSPi4ycR4)78&q Iol`;+0Ls-KivR!s literal 0 HcmV?d00001 diff --git a/Resources/Textures/Interface/Default/Storage/piece_topLeft.png b/Resources/Textures/Interface/Default/Storage/piece_topLeft.png new file mode 100644 index 0000000000000000000000000000000000000000..bfe9134c0f48ebcc6762ca5519d808f58dc02be7 GIT binary patch literal 132 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1|*O0@9PFqjKx9jP7LeL$-D$|>^xl@LnNjq zrz9jK2nY%NI?}*wz{SZ~d+E}pr-zm{upWEx^l7S?kkFB}4vvoB^y7r>YJVN-31^g- d=kN7lU~nnqS>MI*co)zl22WQ%mvv4FO#t~qC_?}M literal 0 HcmV?d00001 diff --git a/Resources/Textures/Interface/Default/Storage/piece_topRight.png b/Resources/Textures/Interface/Default/Storage/piece_topRight.png new file mode 100644 index 0000000000000000000000000000000000000000..c8a48b186724ae9206a807dc520c2e71a04dbd73 GIT binary patch literal 147 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1|*O0@9PFqjKx9jP7LeL$-D$|d_7$pLnNjq ziwFt*I^Mv%r}}$er>nR3biwIO+1K^{Cr_L{{qUmCwuhgepD!r^1Q9=eR2YbS`1sKv sW?v2Oq6e3k>+i@pkjvD?)aA>-@KQwVWo%8#L7>45p00i_>zopr0LQj7@c;k- literal 0 HcmV?d00001 diff --git a/Resources/Textures/Interface/Default/Storage/sidebar_bottom.png b/Resources/Textures/Interface/Default/Storage/sidebar_bottom.png new file mode 100644 index 0000000000000000000000000000000000000000..59339253689574608cb5c9b63f9bccc7ebc06c91 GIT binary patch literal 136 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|oIG6|Lo9mV zo?R$-z<|g3;{2GE=^y?ndmT~LF{|V&7RVciE^FvCq1oH;t;!}@T9la|ks5?U?Og47M wrhoB$4>+WlCMh4h&gwbgir5Y7=H83!mI|;lK*U#cVxN1`#|A3>~2?eO4bPc>;AYc)I$z JtaD0e0svyrA?pAD literal 0 HcmV?d00001 diff --git a/Resources/Textures/Interface/Default/Storage/sidebar_top.png b/Resources/Textures/Interface/Default/Storage/sidebar_top.png new file mode 100644 index 0000000000000000000000000000000000000000..798c03da7d969bf9f37ebd6f94f572ee9e832501 GIT binary patch literal 130 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|Y&=~YLo9le zQyLon^Q17jv9Y!N|Mcn8lUL~pjfOXF+>p3&^QPyeKcAobv#Ck6hnzJy;1Y3i1><72 c9u8)PH_5Dv*X@#O1e(L(>FVdQ&MBb@0M_X(9RL6T literal 0 HcmV?d00001 diff --git a/Resources/Textures/Interface/Default/Storage/tile_blocked.png b/Resources/Textures/Interface/Default/Storage/tile_blocked.png new file mode 100644 index 0000000000000000000000000000000000000000..663f9b5ec5413e8d03b6f6ebeb08a66e5ea32128 GIT binary patch literal 124 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|%sgEjLo9le zpFDh6dDww5q484viL3m^4Xo>pnDi8`m+fFoxo!8VDNvk8Py7JGO1;FygoFeJhF4QK VgVYski-0CDc)I$ztaD0e0s#IoB-;Q0 literal 0 HcmV?d00001 diff --git a/Resources/Textures/Interface/Default/Storage/tile_empty.png b/Resources/Textures/Interface/Default/Storage/tile_empty.png new file mode 100644 index 0000000000000000000000000000000000000000..551ac8fe86fe177a34d8097395eae10991f1693a GIT binary patch literal 132 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|>^xl@Lo9le zd%C-;4>&L;G#V-?DZQJtsPJf)=qVl`@OhHqcI~jsDz_4YtOF|;n@=!A98+LD@MXgS c?g?BBoUQ_vCxwKVfF?0`y85}Sb4q9e00;ReE&u=k literal 0 HcmV?d00001 diff --git a/Resources/keybinds.yml b/Resources/keybinds.yml index b3ba05def2..28bbb9f3d0 100644 --- a/Resources/keybinds.yml +++ b/Resources/keybinds.yml @@ -163,6 +163,13 @@ binds: - function: SwapHands type: State key: X +- function: MoveStoredItem + type: State + key: MouseLeft + canFocus: true +- function: RotateStoredItem + type: State + key: MouseRight - function: Drop type: State key: Q