]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Grid Inventory (#21931)
authorNemanja <98561806+EmoGarbage404@users.noreply.github.com>
Mon, 4 Dec 2023 23:04:39 +0000 (18:04 -0500)
committerGitHub <noreply@github.com>
Mon, 4 Dec 2023 23:04:39 +0000 (16:04 -0700)
* 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 :)

99 files changed:
Content.Client/Input/ContentContexts.cs
Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs
Content.Client/Storage/StorageBoundUserInterface.cs
Content.Client/Storage/Systems/StorageSystem.cs
Content.Client/Storage/UI/StorageUIController.cs [deleted file]
Content.Client/Storage/UI/StorageWindow.cs [deleted file]
Content.Client/UserInterface/Systems/Hotbar/HotbarUIController.cs
Content.Client/UserInterface/Systems/Hotbar/Widgets/HotbarGui.xaml
Content.Client/UserInterface/Systems/Hotbar/Widgets/HotbarGui.xaml.cs
Content.Client/UserInterface/Systems/Storage/Controls/ItemGridPiece.cs [new file with mode: 0644]
Content.Client/UserInterface/Systems/Storage/Controls/StorageContainer.cs [new file with mode: 0644]
Content.Client/UserInterface/Systems/Storage/StorageUIController.cs [new file with mode: 0644]
Content.IntegrationTests/Tests/StorageTest.cs
Content.Server/Chemistry/EntitySystems/ChemMasterSystem.cs
Content.Server/Storage/EntitySystems/StorageSystem.Fill.cs
Content.Shared/CCVar/CCVars.cs
Content.Shared/ContainerHeld/ContainerHeldSystem.cs
Content.Shared/Input/ContentKeyFunctions.cs
Content.Shared/Item/ItemComponent.cs
Content.Shared/Item/ItemSizePrototype.cs
Content.Shared/Item/SharedItemSystem.cs
Content.Shared/Storage/EntitySystems/SharedStorageSystem.cs
Content.Shared/Storage/ItemStorageLocation.cs [new file with mode: 0644]
Content.Shared/Storage/StorageComponent.cs
Content.Shared/Storage/StorageHelpers.cs [new file with mode: 0644]
Resources/Locale/en-US/escape-menu/ui/options-menu.ftl
Resources/Prototypes/Catalog/Fills/Backpacks/StarterGear/backpack.yml
Resources/Prototypes/Catalog/Fills/Backpacks/StarterGear/satchel.yml
Resources/Prototypes/Catalog/Fills/Backpacks/duffelbag.yml
Resources/Prototypes/Catalog/Fills/Boxes/general.yml
Resources/Prototypes/Catalog/Fills/Boxes/medical.yml
Resources/Prototypes/Catalog/Fills/Boxes/science.yml
Resources/Prototypes/Catalog/Fills/Boxes/security.yml
Resources/Prototypes/Catalog/Fills/Items/belt.yml
Resources/Prototypes/Catalog/Fills/Items/briefcases.yml
Resources/Prototypes/Entities/Clothing/Back/backpacks.yml
Resources/Prototypes/Entities/Clothing/Back/duffel.yml
Resources/Prototypes/Entities/Clothing/Back/satchel.yml
Resources/Prototypes/Entities/Clothing/Belt/base_clothingbelt.yml
Resources/Prototypes/Entities/Clothing/Belt/belts.yml
Resources/Prototypes/Entities/Clothing/Belt/quiver.yml
Resources/Prototypes/Entities/Clothing/Belt/waist_bags.yml
Resources/Prototypes/Entities/Clothing/Head/hats.yml
Resources/Prototypes/Entities/Clothing/OuterClothing/base_clothingouter.yml
Resources/Prototypes/Entities/Clothing/Shoes/base_clothingshoes.yml
Resources/Prototypes/Entities/Debugging/item.yml [new file with mode: 0644]
Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml
Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_cans.yml
Resources/Prototypes/Entities/Objects/Consumable/Food/Containers/box.yml
Resources/Prototypes/Entities/Objects/Consumable/Smokeables/Cigarettes/cartons.yml
Resources/Prototypes/Entities/Objects/Consumable/Smokeables/Cigarettes/packs.yml
Resources/Prototypes/Entities/Objects/Consumable/Smokeables/Cigarettes/rolling_paper.yml
Resources/Prototypes/Entities/Objects/Consumable/Smokeables/Cigars/case.yml
Resources/Prototypes/Entities/Objects/Fun/candy_bucket.yml
Resources/Prototypes/Entities/Objects/Fun/crayons.yml
Resources/Prototypes/Entities/Objects/Fun/dice_bag.yml
Resources/Prototypes/Entities/Objects/Misc/box.yml
Resources/Prototypes/Entities/Objects/Misc/briefcases.yml
Resources/Prototypes/Entities/Objects/Misc/medalcase.yml
Resources/Prototypes/Entities/Objects/Misc/paper.yml
Resources/Prototypes/Entities/Objects/Misc/subdermal_implants.yml
Resources/Prototypes/Entities/Objects/Specific/Chapel/bibles.yml
Resources/Prototypes/Entities/Objects/Specific/Chemistry/chem_bag.yml
Resources/Prototypes/Entities/Objects/Specific/Hydroponics/tools.yml
Resources/Prototypes/Entities/Objects/Specific/Janitorial/trashbag.yml
Resources/Prototypes/Entities/Objects/Specific/Kitchen/foodcarts.yml
Resources/Prototypes/Entities/Objects/Specific/Librarian/books_bag.yml
Resources/Prototypes/Entities/Objects/Specific/Medical/medkits.yml
Resources/Prototypes/Entities/Objects/Specific/Research/rped.yml
Resources/Prototypes/Entities/Objects/Specific/Salvage/ore_bag.yml
Resources/Prototypes/Entities/Objects/Specific/chemistry.yml
Resources/Prototypes/Entities/Objects/Tools/matches.yml
Resources/Prototypes/Entities/Objects/Tools/toolbox.yml
Resources/Prototypes/Entities/Objects/Weapons/Guns/pneumatic_cannon.yml
Resources/Prototypes/Entities/Structures/Furniture/bookshelf.yml
Resources/Prototypes/Entities/Structures/Furniture/dresser.yml
Resources/Prototypes/Entities/Structures/Storage/filing_cabinets.yml
Resources/Prototypes/Entities/Structures/Storage/ore_box.yml
Resources/Prototypes/Entities/Structures/Wallmounts/noticeboard.yml
Resources/Prototypes/XenoArch/Effects/utility_effects.yml
Resources/Prototypes/item_size.yml
Resources/Textures/Interface/Default/Storage/back.png [new file with mode: 0644]
Resources/Textures/Interface/Default/Storage/exit.png [new file with mode: 0644]
Resources/Textures/Interface/Default/Storage/piece_bottom.png [new file with mode: 0644]
Resources/Textures/Interface/Default/Storage/piece_bottomLeft.png [new file with mode: 0644]
Resources/Textures/Interface/Default/Storage/piece_bottomRight.png [new file with mode: 0644]
Resources/Textures/Interface/Default/Storage/piece_center.png [new file with mode: 0644]
Resources/Textures/Interface/Default/Storage/piece_left.png [new file with mode: 0644]
Resources/Textures/Interface/Default/Storage/piece_right.png [new file with mode: 0644]
Resources/Textures/Interface/Default/Storage/piece_top.png [new file with mode: 0644]
Resources/Textures/Interface/Default/Storage/piece_topLeft.png [new file with mode: 0644]
Resources/Textures/Interface/Default/Storage/piece_topRight.png [new file with mode: 0644]
Resources/Textures/Interface/Default/Storage/sidebar_bottom.png [new file with mode: 0644]
Resources/Textures/Interface/Default/Storage/sidebar_fat.png [new file with mode: 0644]
Resources/Textures/Interface/Default/Storage/sidebar_mid.png [new file with mode: 0644]
Resources/Textures/Interface/Default/Storage/sidebar_top.png [new file with mode: 0644]
Resources/Textures/Interface/Default/Storage/tile_blocked.png [new file with mode: 0644]
Resources/Textures/Interface/Default/Storage/tile_empty.png [new file with mode: 0644]
Resources/keybinds.yml

index ba8b72788e65a2b0e59d4aa6ad81dc4ff33e8ad8..82388cb75b3cc0b48d7fb4d9666fe862fafd25b4 100644 (file)
@@ -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);
index 87b1f10352f58529d36736ac41297b882a99ea9e..18872d16b5e8a9489243d025f53d0de956c6cd53 100644 (file)
@@ -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);
index 9a3340e5b0ed5242fbc0604b7b541a759cabf038..88de528ff6d38e4028ae15b89bb6faa8391b6725 100644 (file)
-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
-{
-    [UsedImplicitly]
-    public sealed class StorageBoundUserInterface : BoundUserInterface
-    {
-        [ViewVariables]
-        private StorageWindow? _window;
-
-        [Dependency] private readonly IEntityManager _entManager = default!;
-
-        public StorageBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
-        {
-            IoCManager.InjectDependencies(this);
-        }
-
-        protected override void Open()
-        {
-            base.Open();
-
-            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<IUserInterfaceManager>().GetUIController<StorageUIController>();
-                _window = controller.EnsureStorageWindow(Owner);
-                _window.Title = EntMan.GetComponent<MetaDataComponent>(Owner).EntityName;
-
-                _window.EntityList.GenerateItem += _window.GenerateButton;
-                _window.EntityList.ItemPressed += InteractWithItem;
-                _window.StorageContainerButton.OnPressed += TouchedContainerButton;
-
-                _window.OnClose += Close;
-
-                if (EntMan.TryGetComponent<StorageComponent>(Owner, out var storageComp))
-                {
-                    BuildEntityList(Owner, storageComp);
-                }
+namespace Content.Client.Storage;
 
-            }
-            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;
+[UsedImplicitly]
+public sealed class StorageBoundUserInterface : BoundUserInterface
+{
+    [Dependency] private readonly IEntityManager _entManager = default!;
 
-            if (args.Event.Function == EngineKeyFunctions.UIClick)
-            {
-                SendPredictedMessage(new StorageInteractWithItemEvent(_entManager.GetNetEntity(entity)));
-            }
-            else if (EntMan.EntityExists(entity))
-            {
-                OnButtonPressed(args.Event, entity);
-            }
-        }
+    private readonly StorageSystem _storage;
 
-        private void OnButtonPressed(GUIBoundKeyEventArgs args, EntityUid entity)
-        {
-            if (args.Function == ContentKeyFunctions.ExamineEntity)
-            {
-                EntMan.System<ExamineSystem>()
-                    .DoExamine(entity);
-            }
-            else if (args.Function == EngineKeyFunctions.UseSecondary)
-            {
-                IoCManager.Resolve<IUserInterfaceManager>().GetUIController<VerbMenuUIController>().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;
-            }
+    public StorageBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
+    {
+        IoCManager.InjectDependencies(this);
+        _storage = _entManager.System<StorageSystem>();
+    }
 
-            args.Handle();
-        }
+    protected override void Open()
+    {
+        base.Open();
 
-        public void TouchedContainerButton(BaseButton.ButtonEventArgs args)
-        {
-            SendPredictedMessage(new StorageInsertItemMessage());
-        }
+        if (_entManager.TryGetComponent<StorageComponent>(Owner, out var comp))
+            _storage.OpenStorageUI(Owner, comp);
+    }
 
-        protected override void Dispose(bool disposing)
-        {
-            base.Dispose(disposing);
-            if (!disposing)
-                return;
+    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);
     }
 }
+
index 86d6ec637898bf09fb2f842f69376fccb2d03bbb..56d0810dd5b5f009433445ea51893e1b29b9fe09 100644 (file)
@@ -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<EntityUid, StorageComponent>? StorageUpdated;
+    private readonly List<Entity<StorageComponent>> _openStorages = new();
+    public int OpenStorageAmount => _openStorages.Count;
+
+    public event Action<Entity<StorageComponent>>? StorageUpdated;
+    public event Action<Entity<StorageComponent>?>? StorageOrderChanged;
 
     public override void Initialize()
     {
         base.Initialize();
 
+        SubscribeLocalEvent<StorageComponent, ComponentShutdown>(OnShutdown);
         SubscribeNetworkEvent<PickupAnimationEvent>(HandlePickupAnimation);
         SubscribeNetworkEvent<AnimateInsertingEntitiesEvent>(HandleAnimatingInsertingEntities);
     }
@@ -26,7 +32,74 @@ public sealed class StorageSystem : SharedStorageSystem
     public override void UpdateUI(Entity<StorageComponent?> 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<StorageComponent>? last = _openStorages.LastOrDefault();
+        StorageOrderChanged?.Invoke(last);
+    }
+
+    public void CloseStorageUI(Entity<StorageComponent?> entity)
+    {
+        if (!Resolve(entity, ref entity.Comp))
+            return;
+
+        if (!_openStorages.Contains((entity, entity.Comp)))
+            return;
+
+        var storages = new ValueList<Entity<StorageComponent>>(_openStorages);
+        var reverseStorages = storages.Reverse();
+
+        foreach (var storage in reverseStorages)
+        {
+            CloseStorageBoundUserInterface(storage.Owner);
+            _openStorages.Remove(storage);
+            if (storage.Owner == entity.Owner)
+                break;
+        }
+
+        Entity<StorageComponent>? last = null;
+        if (_openStorages.Any())
+            last = _openStorages.LastOrDefault();
+        StorageOrderChanged?.Invoke(last);
+    }
+
+    private void ClearNonParentStorages(EntityUid uid)
+    {
+        var storages = new ValueList<Entity<StorageComponent>>(_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<UserInterfaceComponent?> 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<StorageComponent> ent, ref ComponentShutdown args)
+    {
+        CloseStorageUI((ent, ent.Comp));
     }
 
     /// <inheritdoc />
@@ -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 (file)
index 352ee4f..0000000
+++ /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<StorageSystem>, IOnStateExited<GameplayState>
-{
-    // This is mainly to keep legacy functionality for now.
-    private readonly Dictionary<EntityUid, StorageWindow> _storageWindows = new();
-
-    public override void Initialize()
-    {
-        base.Initialize();
-        EntityManager.EventBus.SubscribeLocalEvent<StorageComponent, ComponentShutdown>(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<UserInterfaceComponent>(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 (file)
index b096e8d..0000000
+++ /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
-{
-    /// <summary>
-    /// GUI class for client storage component
-    /// </summary>
-    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<SharedStorageSystem>();
-            _item = _entityManager.System<ItemSystem>();
-            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;
-            };
-        }
-
-        /// <summary>
-        /// Loops through stored entities creating buttons for each, updates information labels
-        /// </summary>
-        public void BuildEntityList(EntityUid entity, StorageComponent component)
-        {
-            var storedCount = component.Container.ContainedEntities.Count;
-            var list = new List<EntityListData>(storedCount);
-
-            foreach (var uid in component.Container.ContainedEntities)
-            {
-                list.Add(new EntityListData(uid));
-            }
-
-            EntityList.PopulateList(list);
-
-            SetStorageInformation((entity, component));
-        }
-
-        private void SetStorageInformation(Entity<StorageComponent> 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))))));
-            }
-        }
-
-        /// <summary>
-        /// Button created for each entity that represents that item in the storage UI, with a texture, and name and size label
-        /// </summary>
-        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<MetaDataComponent>(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;
-        }
-    }
-}
index 17cc03c113105a9fbc8643d1fd5bf2544afe8a18..397da978d8acd246e6890e3161d7b428b9a4d9aa 100644 (file)
@@ -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<InventoryUIController>();
         _hands = UIManager.GetUIController<HandsUIController>();
+        _storage = UIManager.GetUIController<StorageUIController>();
         _hands.RegisterHandContainer(handsContainer);
         _inventory.RegisterInventoryBarContainer(inventoryBar);
+        _storage.RegisterStorageContainer(storageContainer);
     }
 
     public void ReloadHotbar()
index cd34412da6197371b7ca09b0155dc9e489a96b48..a760de7430ebcd15f148c12a7f278b6593da053e 100644 (file)
@@ -1,6 +1,7 @@
 <widgets:HotbarGui
     xmlns="https://spacestation14.io"
     xmlns:inventory="clr-namespace:Content.Client.UserInterface.Systems.Inventory.Controls"
+    xmlns:storage="clr-namespace:Content.Client.UserInterface.Systems.Storage.Controls"
     xmlns:hands="clr-namespace:Content.Client.UserInterface.Systems.Hands.Controls"
     xmlns:widgets="clr-namespace:Content.Client.UserInterface.Systems.Hotbar.Widgets"
     Name="HotbarInterface"
@@ -8,11 +9,21 @@
     VerticalAlignment="Bottom"
     Orientation="Vertical"
     HorizontalAlignment="Center">
-    <inventory:ItemStatusPanel
-        Name="StatusPanel"
-        Visible="False"
-        HorizontalAlignment="Center"
-    />
+    <Control HorizontalAlignment="Center">
+        <inventory:ItemStatusPanel
+            Name="StatusPanel"
+            Visible="False"
+            HorizontalAlignment="Center"
+        />
+        <BoxContainer Name="StorageContainer"
+                      Access="Public"
+                      HorizontalAlignment="Center"
+                      Margin="10">
+            <storage:StorageContainer
+                Name="StoragePanel"
+                Visible="False"/>
+        </BoxContainer>
+    </Control>
     <inventory:ItemSlotButtonContainer
         Name="InventoryHotbar"
         Access="Public"
index 2fd3537206aec6092c72ddc7194e690d05ca11a8..0e670d978f8e0eb06008bdadfd5a8a7ce1c6a48b 100644 (file)
@@ -13,7 +13,7 @@ public sealed partial class HotbarGui : UIWidget
         StatusPanel.Update(null);
         var hotbarController = UserInterfaceManager.GetUIController<HotbarUIController>();
 
-        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 (file)
index 0000000..830d59b
--- /dev/null
@@ -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<GUIBoundKeyEventArgs, ItemGridPiece>? OnPiecePressed;
+    public event Action<GUIBoundKeyEventArgs, ItemGridPiece>? 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<ItemComponent> entity, ItemStorageLocation location,  IEntityManager entityManager)
+    {
+        IoCManager.InjectDependencies(this);
+
+        _itemSystem = entityManager.System<ItemSystem>();
+        _spriteSystem = entityManager.System<SpriteSystem>();
+        _storageController = UserInterfaceManager.GetUIController<StorageUIController>();
+
+        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<Box2i> 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<ItemComponent?> entity, ItemStorageLocation location, IEntityManager entMan)
+    {
+        var boxSize = entMan.System<ItemSystem>().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 (file)
index 0000000..12bce74
--- /dev/null
@@ -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<GUIBoundKeyEventArgs, ItemGridPiece>? OnPiecePressed;
+    public event Action<GUIBoundKeyEventArgs, ItemGridPiece>? 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<StorageUIController>();
+
+        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<StorageComponent>? entity)
+    {
+        Visible = entity != null;
+        StorageEntity = entity;
+        if (entity == null)
+            return;
+
+        BuildGridRepresentation(entity.Value);
+    }
+
+    private void BuildGridRepresentation(Entity<StorageComponent> 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<StorageSystem>().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<StorageComponent>(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<ItemComponent>(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<ItemSystem>();
+        var storageSystem = _entity.System<StorageSystem>();
+        var handsSystem = _entity.System<HandsSystem>();
+
+        foreach (var child in _backgroundGrid.Children)
+        {
+            child.ModulateSelfOverride = Color.FromHex("#222222");
+        }
+
+        if (UserInterfaceManager.CurrentlyHovered is StorageContainer con && con != this)
+            return;
+
+        if (!_entity.TryGetComponent<StorageComponent>(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<ItemComponent>(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<ItemComponent?> entity, ItemStorageLocation location)
+    {
+        var origin = Vector2i.Zero;
+
+        if (StorageEntity != null)
+            origin = _entity.GetComponent<StorageComponent>(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<StorageComponent>(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<StorageSystem>();
+        var handsSystem = _entity.System<HandsSystem>();
+
+        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<StorageSystem>().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 (file)
index 0000000..d3bbd82
--- /dev/null
@@ -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<StorageSystem>
+{
+    [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<ItemGridPiece> _menuDragHelper;
+    private StorageContainer? _container;
+
+    private Vector2? _lastContainerPosition;
+
+    private HotbarGui? Hotbar => UIManager.GetActiveUIWidgetOrNull<HotbarGui>();
+
+    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<ItemGridPiece>(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<StorageComponent>? 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<HandsSystem>().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<StorageComponent> 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<ExamineSystem>().DoExamine(control.Entity);
+            args.Handle();
+        }
+        else if (args.Function == EngineKeyFunctions.UseSecondary)
+        {
+            UIManager.GetUIController<VerbMenuUIController>().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<ItemComponent>(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;
+    }
+}
index c618616cd2776b9c0915c3d0982e87c1651e5d50..659b310661b0b2a5d064be082ea9393ff0622f4a 100644 (file)
@@ -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<ItemComponent>("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;
index 5b91d9456bd1a767696f968df815de0a4e2aeb54..cbb84be83c33b6392ff4b7c05ad9bc67f117cec7 100644 (file)
@@ -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
             };
index 500a3fb100c291f2706d659e324761287ac8b86e..10278cc8051d53f650fa093c9405ee4ab461900e 100644 (file)
@@ -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,15 +17,71 @@ public sealed partial class StorageSystem
         if (component.Contents.Count == 0)
             return;
 
-        TryComp<StorageComponent>(uid, out var storageComp);
-        TryComp<EntityStorageComponent>(uid, out var entityStorageComp);
-
-        if (entityStorageComp == null && storageComp == null)
+        if (TryComp<StorageComponent>(uid, out var storageComp))
+        {
+            FillStorage((uid, component, storageComp));
+        }
+        else if (TryComp<EntityStorageComponent>(uid, out var entityStorageComp))
+        {
+            FillEntityStorage((uid, component, entityStorageComp));
+        }
+        else
         {
             Log.Error($"StorageFillComponent couldn't find any StorageComponent ({uid})");
+        }
+    }
+
+    private void FillStorage(Entity<StorageFillComponent?, StorageComponent?> 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<Entity<ItemComponent>>();
+        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<EntityPrototype>(spawnPrototype)
+                .HasComponent(typeof(RandomSpawnerComponent)));
+
+            if (!TryComp<ItemComponent>(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<StorageFillComponent?, EntityStorageComponent?> entity)
+    {
+        var (uid, component, entityStorageComp) = entity;
+
+        if (!Resolve(uid, ref component, ref entityStorageComp))
+            return;
+
         var coordinates = Transform(uid).Coordinates;
 
         var spawnItems = EntitySpawnCollection.GetSpawns(component.Contents, Random);
@@ -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<EntityPrototype>(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);
         }
     }
 }
index a8adbe4873f27a09a2c85b43049c91996a121369..999ef21e256fd174da360aff8d05ecea8e890c21 100644 (file)
@@ -1655,6 +1655,12 @@ namespace Content.Shared.CCVar
         public static readonly CVarDef<bool> ToggleWalk =
             CVarDef.Create("control.toggle_walk", false, CVar.CLIENTONLY | CVar.ARCHIVE);
 
+        /// <summary>
+        /// Whether or not the storage UI is static and bound to the hotbar, or unbound and allowed to be dragged anywhere.
+        /// </summary>
+        public static readonly CVarDef<bool> StaticStorageUI =
+            CVarDef.Create("control.static_storage_ui", true, CVar.CLIENTONLY | CVar.ARCHIVE);
+
         /*
          * UPDATE
          */
index e0ecfe199205ff8760f58a81f04c9075d6ccd358..db8ad5e5e1cdaa5391e6335105135ffe6c10f445 100644 (file)
@@ -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);
index f4307cd058c5d5bac4892168dca8f9a48ec37aca..7a5a6e82d82ca1dd311ade5ea484eb9207276d21 100644 (file)
@@ -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";
index ef4d4513207b4ea4e987a7f71d7826c996ee99a9..21926882a0b8d3cf724b44a8077b0710a6957a2d 100644 (file)
@@ -34,6 +34,13 @@ public sealed partial class ItemComponent : Component
     [ViewVariables(VVAccess.ReadWrite)]
     [DataField("sprite")]
     public string? RsiPath;
+
+    /// <summary>
+    /// An optional override for the shape of the item within the grid storage.
+    /// If null, a default shape will be used based on <see cref="Size"/>.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public List<Box2i>? Shape;
 }
 
 [Serializable, NetSerializable]
index e498809593491dba9a98e7f051697ffa363ff73b..5b3d4d8f919c41ac95ab2e020d617d9359dfd292 100644 (file)
@@ -24,6 +24,12 @@ public sealed partial class ItemSizePrototype : IPrototype, IComparable<ItemSize
     [DataField]
     public readonly LocId Name;
 
+    /// <summary>
+    /// The default inventory shape associated with this item size.
+    /// </summary>
+    [DataField(required: true)]
+    public IReadOnlyList<Box2i> DefaultShape = new List<Box2i>();
+
     public int CompareTo(ItemSizePrototype? other)
     {
         if (other is not { } otherItemSize)
index 21679bbd72186c02160a08c3dd0e084d8d15868a..c718b21e0ffeef36f61c43f8a0f83e77c5b149e7 100644 (file)
@@ -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;
     }
+
+    /// <summary>
+    /// Gets the default shape of an item.
+    /// </summary>
+    public IReadOnlyList<Box2i> GetItemShape(Entity<ItemComponent?> uid)
+    {
+        if (!Resolve(uid, ref uid.Comp))
+            return new Box2i[] { };
+
+        return uid.Comp.Shape ?? GetSizePrototype(uid.Comp.Size).DefaultShape;
+    }
+
+    /// <summary>
+    /// Gets the default shape of an item.
+    /// </summary>
+    public IReadOnlyList<Box2i> GetItemShape(ItemComponent component)
+    {
+        return component.Shape ?? GetSizePrototype(component.Size).DefaultShape;
+    }
+
+    /// <summary>
+    /// Gets the shape of an item, adjusting for rotation and offset.
+    /// </summary>
+    public IReadOnlyList<Box2i> GetAdjustedItemShape(Entity<ItemComponent?> entity, ItemStorageLocation location)
+    {
+        return GetAdjustedItemShape(entity, location.Rotation, location.Position);
+    }
+
+    /// <summary>
+    /// Gets the shape of an item, adjusting for rotation and offset.
+    /// </summary>
+    public IReadOnlyList<Box2i> GetAdjustedItemShape(Entity<ItemComponent?> 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<Box2i>();
+        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;
+    }
 }
index 31f6ae5c7bedb4a5636c71ccb76f87951a3c7fe5..2f6550bf2e0e176c2c2a4895d35e904a3ce1e866 100644 (file)
@@ -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<ItemComponent> _itemQuery;
@@ -69,17 +69,18 @@ public abstract class SharedStorageSystem : EntitySystem
         SubscribeLocalEvent<StorageComponent, OpenStorageImplantEvent>(OnImplantActivate);
         SubscribeLocalEvent<StorageComponent, AfterInteractEvent>(AfterInteract);
         SubscribeLocalEvent<StorageComponent, DestructionEventArgs>(OnDestroy);
-        SubscribeLocalEvent<StorageComponent, StorageComponent.StorageInsertItemMessage>(OnInsertItemMessage);
         SubscribeLocalEvent<StorageComponent, BoundUIOpenedEvent>(OnBoundUIOpen);
         SubscribeLocalEvent<MetaDataComponent, StackCountChangedEvent>(OnStackCountChanged);
 
-        SubscribeLocalEvent<StorageComponent, EntInsertedIntoContainerMessage>(OnContainerModified);
-        SubscribeLocalEvent<StorageComponent, EntRemovedFromContainerMessage>(OnContainerModified);
+        SubscribeLocalEvent<StorageComponent, EntInsertedIntoContainerMessage>(OnEntInserted);
+        SubscribeLocalEvent<StorageComponent, EntRemovedFromContainerMessage>(OnEntRemoved);
         SubscribeLocalEvent<StorageComponent, ContainerIsInsertingAttemptEvent>(OnInsertAttempt);
 
         SubscribeLocalEvent<StorageComponent, AreaPickupDoAfterEvent>(OnDoAfter);
 
-        SubscribeLocalEvent<StorageComponent, StorageInteractWithItemEvent>(OnInteractWithItem);
+        SubscribeAllEvent<StorageInteractWithItemEvent>(OnInteractWithItem);
+        SubscribeAllEvent<StorageSetItemLocationEvent>(OnSetItemLocation);
+        SubscribeAllEvent<StorageInsertItemIntoLocationEvent>(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;
     }
 
     /// <summary>
@@ -152,11 +154,11 @@ public abstract class SharedStorageSystem : EntitySystem
     /// </summary>
     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;
     }
 
     /// <summary>
@@ -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.
     /// </summary>
-    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 uid = GetEntity(msg.StorageUid);
+        var entity = GetEntity(msg.InteractedItemUid);
+
+        if (!TryComp<StorageComponent>(uid, out var storageComp))
             return;
 
-        var entity = GetEntity(args.InteractedItemUID);
+        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.SenderSession.AttachedEntity is not { } player)
+            return;
+
+        var storageEnt = GetEntity(msg.StorageEnt);
+        var itemEnt = GetEntity(msg.ItemEnt);
+
+        if (!TryComp<StorageComponent>(storageEnt, out var storageComp))
+            return;
+
+        if (!_ui.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.Session.AttachedEntity == null)
+        if (args.SenderSession.AttachedEntity is not { } player)
+            return;
+
+        var storageEnt = GetEntity(msg.StorageEnt);
+        var itemEnt = GetEntity(msg.ItemEnt);
+
+        if (!TryComp<StorageComponent>(storageEnt, out var storageComp))
+            return;
+
+        if (!_ui.TryGetUi(storageEnt, StorageComponent.StorageUiKey.Key, out var bui) ||
+            !bui.SubscribedSessions.Contains(args.SenderSession))
             return;
 
-        PlayerInsertHeldEntity(uid, args.Session.AttachedEntity.Value, storageComp);
+        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<StorageComponent> 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<StorageComponent> entity, ref EntRemovedFromContainerMessage args)
+    {
+        // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
+        if (entity.Comp.Container == null)
+            return;
+
+        if (args.Container.ID != StorageComponent.ContainerId)
+            return;
+
+        entity.Comp.StoredItems.Remove(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
     /// <param name="reason">If returning false, the reason displayed to the player</param>
     /// <param name="storageComp"></param>
     /// <param name="item"></param>
+    /// <param name="ignoreStacks"></param>
+    /// <param name="ignoreLocation"></param>
     /// <returns>true if it can be inserted, false otherwise</returns>
-    public bool CanInsert(EntityUid uid, EntityUid insertEnt, out string? reason, StorageComponent? storageComp = null, ItemComponent? item = null, bool ignoreStacks = false)
+    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<StorageComponent>(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;
     }
 
+    /// <summary>
+    ///     Inserts into the storage container at a given location
+    /// </summary>
+    /// <returns>true if the entity was inserted, false otherwise. This will also return true if a stack was partially
+    /// inserted.</returns>
+    public bool InsertAt(
+        Entity<StorageComponent?> uid,
+        Entity<ItemComponent?> insertEnt,
+        ItemStorageLocation location,
+        out EntityUid? stackedEntity,
+        EntityUid? user = null,
+        bool playSound = true)
+    {
+        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);
+    }
+
     /// <summary>
     ///     Inserts into the storage container
     /// </summary>
@@ -654,20 +757,156 @@ public abstract class SharedStorageSystem : EntitySystem
     }
 
     /// <summary>
-    /// Returns true if there is enough space to theoretically fit another item.
+    /// Attempts to set the location of an item already inside of a storage container.
     /// </summary>
-    public bool HasSpace(Entity<StorageComponent?> uid)
+    public bool TrySetItemStorageLocation(Entity<ItemComponent?> itemEnt, Entity<StorageComponent?> storageEnt, ItemStorageLocation location)
     {
-        if (!Resolve(uid, ref uid.Comp))
+        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;
+    }
+
+    /// <summary>
+    /// Tries to find the first available spot on a storage grid.
+    /// starts at the top-left and goes right and down.
+    /// </summary>
+    public bool TryGetAvailableGridSpace(
+        Entity<StorageComponent?> storageEnt,
+        Entity<ItemComponent?> itemEnt,
+        [NotNullWhen(true)] out ItemStorageLocation? storageLocation)
+    {
+        storageLocation = null;
+
+        if (!Resolve(storageEnt, ref storageEnt.Comp) || !Resolve(itemEnt, ref itemEnt.Comp))
+            return false;
+
+        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;
+    }
+
+    /// <summary>
+    /// Checks if an item fits into a specific spot on a storage grid.
+    /// </summary>
+    public bool ItemFitsInGridLocation(
+        Entity<ItemComponent?> itemEnt,
+        Entity<StorageComponent?> storageEnt,
+        ItemStorageLocation location)
+    {
+        return ItemFitsInGridLocation(itemEnt, storageEnt, location.Position, location.Rotation);
+    }
+
+    /// <summary>
+    /// Checks if an item fits into a specific spot on a storage grid.
+    /// </summary>
+    public bool ItemFitsInGridLocation(
+        Entity<ItemComponent?> itemEnt,
+        Entity<StorageComponent?> storageEnt,
+        Vector2i position,
+        Angle rotation)
+    {
+        if (!Resolve(itemEnt, ref itemEnt.Comp) || !Resolve(storageEnt, ref storageEnt.Comp))
             return false;
 
-        //todo maybe this shouldn't be authoritative over weight? idk.
-        if (uid.Comp.MaxSlots != null)
+        var gridBounds = storageEnt.Comp.Grid.GetBoundingBox();
+        if (!gridBounds.Contains(position))
+            return false;
+
+        var itemShape = ItemSystem.GetAdjustedItemShape(itemEnt, rotation, position);
+
+        foreach (var box in itemShape)
         {
-            return uid.Comp.Container.ContainedEntities.Count < uid.Comp.MaxSlots || HasSpaceInStacks(uid);
+            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 GetCumulativeItemSizes(uid, uid.Comp) < uid.Comp.MaxTotalWeight || HasSpaceInStacks(uid);
+        return true;
+    }
+
+    /// <summary>
+    /// Checks if a space on a grid is valid and not occupied by any other pieces.
+    /// </summary>
+    public bool IsGridSpaceEmpty(Entity<ItemComponent?> itemEnt, Entity<StorageComponent?> storageEnt, Vector2i location)
+    {
+        if (!Resolve(storageEnt, ref storageEnt.Comp))
+            return false;
+
+        var validGrid = false;
+        foreach (var grid in storageEnt.Comp.Grid)
+        {
+            if (grid.Contains(location))
+            {
+                validGrid = true;
+                break;
+            }
+        }
+
+        if (!validGrid)
+            return false;
+
+        foreach (var (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;
+    }
+
+    /// <summary>
+    /// Returns true if there is enough space to theoretically fit another item.
+    /// </summary>
+    public bool HasSpace(Entity<StorageComponent?> uid)
+    {
+        if (!Resolve(uid, ref uid.Comp))
+            return false;
+
+        return GetCumulativeItemAreas(uid) < uid.Comp.Grid.GetArea() || HasSpaceInStacks(uid);
     }
 
     private bool HasSpaceInStacks(Entity<StorageComponent?> uid, string? stackType = null)
@@ -695,17 +934,17 @@ public abstract class SharedStorageSystem : EntitySystem
     /// <summary>
     /// Returns the sum of all the ItemSizes of the items inside of a storage.
     /// </summary>
-    public int GetCumulativeItemSizes(EntityUid uid, StorageComponent? component = null)
+    public int GetCumulativeItemAreas(Entity<StorageComponent?> 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<StorageComponent?> 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);
-    }
-
     /// <summary>
     /// Plays a clientside pickup animation for the specified uid.
     /// </summary>
diff --git a/Content.Shared/Storage/ItemStorageLocation.cs b/Content.Shared/Storage/ItemStorageLocation.cs
new file mode 100644 (file)
index 0000000..a43be5a
--- /dev/null
@@ -0,0 +1,40 @@
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Storage;
+
+[DataDefinition, Serializable, NetSerializable]
+public partial record struct ItemStorageLocation
+{
+    /// <summary>
+    /// The rotation, stored a cardinal direction in order to reduce rounding errors.
+    /// </summary>
+    [DataField]
+    private Direction _rotation;
+
+    /// <summary>
+    /// The rotation of the piece in storage.
+    /// </summary>
+    public Angle Rotation
+    {
+        get => _rotation.ToAngle();
+        set => _rotation = value.GetCardinalDir();
+    }
+
+    /// <summary>
+    /// Where the item is located in storage.
+    /// </summary>
+    [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;
+    }
+};
index 422b7f9989b69ee70a9b96b399dca1c3b00449ee..99677cc6c176e7e55340ed926b07b4d63f217ef8 100644 (file)
@@ -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,24 +27,23 @@ namespace Content.Shared.Storage
         public Container Container = default!;
 
         /// <summary>
-        /// 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.
         /// </summary>
         [DataField, ViewVariables(VVAccess.ReadWrite), AutoNetworkedField]
-        public int MaxTotalWeight;
+        public Dictionary<NetEntity, ItemStorageLocation> StoredItems = new();
 
         /// <summary>
-        /// The maximum size item that can be inserted into this storage,
+        /// A list of boxes that comprise a combined grid that determines the location that items can be stored.
         /// </summary>
         [DataField, ViewVariables(VVAccess.ReadWrite), AutoNetworkedField]
-        [Access(typeof(SharedStorageSystem))]
-        public ProtoId<ItemSizePrototype>? MaxItemSize;
+        public List<Box2i> Grid = new();
 
         /// <summary>
-        /// The max number of entities that can be inserted into this storage.
+        /// The maximum size item that can be inserted into this storage,
         /// </summary>
         [DataField, ViewVariables(VVAccess.ReadWrite), AutoNetworkedField]
-        public int? MaxSlots;
+        [Access(typeof(SharedStorageSystem))]
+        public ProtoId<ItemSizePrototype>? MaxItemSize;
 
         // TODO: Make area insert its own component.
         [DataField("quickInsert")]
@@ -95,27 +95,61 @@ namespace Content.Shared.Storage
         public SoundSpecifier? StorageCloseSound;
 
         [Serializable, NetSerializable]
-        public sealed class StorageInsertItemMessage : BoundUserInterfaceMessage
+        public enum StorageUiKey
         {
+            Key,
         }
+    }
 
-        [Serializable, NetSerializable]
-        public enum StorageUiKey
+    [Serializable, NetSerializable]
+    public sealed class StorageInteractWithItemEvent : EntityEventArgs
+    {
+        public readonly NetEntity InteractedItemUid;
+
+        public readonly NetEntity StorageUid;
+
+        public StorageInteractWithItemEvent(NetEntity interactedItemUid, NetEntity storageUid)
         {
-            Key,
+            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 StorageInteractWithItemEvent : BoundUserInterfaceMessage
+    public sealed class StorageInsertItemIntoLocationEvent : EntityEventArgs
     {
-        public readonly NetEntity InteractedItemUID;
-        public StorageInteractWithItemEvent(NetEntity interactedItemUID)
+        public readonly NetEntity ItemEnt;
+
+        public readonly NetEntity StorageEnt;
+
+        public readonly ItemStorageLocation Location;
+
+        public StorageInsertItemIntoLocationEvent(NetEntity itemEnt, NetEntity storageEnt, ItemStorageLocation location)
         {
-            InteractedItemUID = interactedItemUID;
+            ItemEnt = itemEnt;
+            StorageEnt = storageEnt;
+            Location = location;
         }
     }
 
+
     /// <summary>
     /// Network event for displaying an animation of entities flying into a storage entity
     /// </summary>
diff --git a/Content.Shared/Storage/StorageHelpers.cs b/Content.Shared/Storage/StorageHelpers.cs
new file mode 100644 (file)
index 0000000..bf0d32a
--- /dev/null
@@ -0,0 +1,76 @@
+namespace Content.Shared.Storage;
+
+public static class StorageHelper
+{
+    public static Box2i GetBoundingBox(this IReadOnlyList<Box2i> 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<Box2i> 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<Box2i> 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<Box2i> boxes, Vector2i point)
+    {
+        foreach (var box in boxes)
+        {
+            if (box.Contains(point))
+                return true;
+        }
+
+        return false;
+    }
+}
index ddf76ebb1a6b2c873f099cfe333787451ecd15b8..7c541e0257e793b067dca76415e50fe0fe7bcb2f 100644 (file)
@@ -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
index 07e6cb05d816d3def69b88ba6b2e809f4af53bb9..25a9ad896345fbe429ccdbf3c8f09479c7f26a57 100644 (file)
   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
index b70ef28e76a79f10fd6fb2d01c57fdf1abc36f63..87688dbe5b21bb70e3661b7cde722f009fcd9484 100644 (file)
     - type: StorageFill
       contents:
       - id: BoxSurvival
+      - id: BoxForensicPad
       - id: Lighter
       - id: CigPackBlack
-      - id: BoxForensicPad
       - id: HandLabeler
 
 - type: entity
index 06c7691dbfe273426e4219d711bc5b26cf5b7342..75adf2d5d8ae75941bc61e228744c24003246649 100644 (file)
     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
   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
index ae5f01301d5236e8244f1a11c1509c3721f7b2a0..7970083596640e51b342bcc674b04d76a1dd8bfe 100644 (file)
@@ -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
       - id: TrashBag
         amount: 6
   - type: Storage
-    maxTotalWeight: 24
+    grid:
+    - 0,0,5,3
     maxItemSize: Normal
     whitelist:
       tags:
       - id: HandheldGPSBasic
         amount: 6
   - type: Storage
-    maxTotalWeight: 24
+    grid:
+    - 0,0,5,3
   - type: Sprite
     layers:
       - state: box
       - state: box
       - state: candle
   - type: Storage
-    maxTotalWeight: 30
+    grid:
+    - 0,0,9,2
   - type: StorageFill
     contents:
       - id: Candle
       - state: box
       - state: candle
   - type: Storage
-    maxTotalWeight: 30
+    grid:
+    - 0,0,9,2
   - type: StorageFill
     contents:
       - id: CandleSmall
       - id: DartYellow
         amount: 3
   - type: Storage
-    maxTotalWeight: 24
+    grid:
+    - 0,0,5,3
   - type: Sprite
     layers:
       - state: box
index e35e9086ffeb405c174374e382dac8b9ad52f2d4..c41753bdaf17049cbc9c7908acdd507a4fcfb850 100644 (file)
@@ -94,7 +94,8 @@
   id: BoxMouthSwab
   components:
   - type: Storage
-    maxTotalWeight: 20
+    grid:
+    - 0,0,4,3
   - type: StorageFill
     contents:
       - id: DiseaseSwab
index b4bfb578c19b7f7e57b36ddb7e47d1e41fb9b438..cd0320e088d744c71a7942299df9960e9770408d 100644 (file)
@@ -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
index 2fa5c430465bf7c77b0d1dc3fd531d0cf6dcf3eb..9975f4f3f0c8336d595c7fe325ebf5f0ee7bec05 100644 (file)
@@ -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
index 0e4fa72eedeb36ee8a84efd2d63a0d2b35e3e2fe..6417d88b2b602b1b74e448d0ff829027ec64d61b 100644 (file)
   - type: Item
     size: Ginormous
   - type: Storage
-    maxSlots: 8
     maxItemSize: Normal
+    grid:
+    - 0,0,7,1
   - type: StorageFill
     contents:
       - id: ExGrenade
index c36a4e6b79c1347378584a4ad123141fe1a64e0b..ce2cf1e2b34a8fb15ceb71933d1c6240263a3cd5 100644 (file)
@@ -19,6 +19,8 @@
     size: Ginormous
   - type: Storage
     maxItemSize: Huge
+    grid:
+    - 0,0,6,3
   - type: StorageFill
     contents:
       - id: WeaponSniperHristov
index 51b9b2e828b0e82a2c60ed158434a44ea30cb071..b8792211a279c6e02ff73a850d9b2e71e9fc0d90 100644 (file)
@@ -14,8 +14,9 @@
     slots:
     - back
   - type: Storage
+    grid:
+    - 0,0,6,3
     maxItemSize: Huge
-    maxTotalWeight: 28
   - type: ContainerContainer
     containers:
       storagebase: !type:Container
   - type: Sprite
     sprite: Clothing/Back/Backpacks/ertleader.rsi
   - type: Storage
-    maxTotalWeight: 44
+    grid:
+    - 0,0,10,3
 
 - type: entity
   parent: ClothingBackpackERTLeader
     size: Ginormous
   - type: Storage
     maxItemSize: Huge
-    maxTotalWeight: 200 #5 duffels worth of gear
+    grid:
+    - 0,0,19,9
 
 - type: entity
   parent: ClothingBackpackClown
   - 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
index 1562eda95e264a934ba228c1cf24d347a2437b75..190d05b7773592ffe3c4ea0660c280be243a8878 100644 (file)
@@ -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
     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
 
   components:
   - type: Storage
     maxItemSize: Huge
-    maxTotalWeight: 40
   - type: ClothingSpeedModifier
     walkModifier: 1
     sprintModifier: 1
index dcb723393bc592e234f042b54f625fd4ca17cef8..8b8cf2c6224f961e401ff32d8372f8da164ec239 100644 (file)
     size: Ginormous
   - type: Storage
     maxItemSize: Huge
-    maxTotalWeight: 200 #5 duffels worth of gear
+    grid:
+    - 0,0,19,9
index 6d94ab454b621f94c8b1173862fbb5996318fa72..614e739e058dc90ca6083729b1f97d6d07cd5841 100644 (file)
@@ -22,8 +22,9 @@
   id: ClothingBeltStorageBase
   components:
   - type: Storage
-    maxSlots: 7
     maxItemSize: Normal
+    grid:
+    - 0,0,7,1
   - type: Item
     size: Ginormous
   - type: ContainerContainer
   abstract: true
   parent: ClothingBeltBase
   id: ClothingBeltAmmoProviderBase
-  components: 
+  components:
   - type: BallisticAmmoProvider
     mayTransfer: true
   - type: Item
     size: Ginormous
   - type: ContainerContainer
-    containers: 
+    containers:
       ballistic-ammo: !type:Container
-      
+
index ebaa8cc12388d0424ac1233fecdee21b5d654f8a..5655a2639ee199fb7585a99911dde2832b54d50a 100644 (file)
   - type: Clothing
     sprite: Clothing/Belt/sheath.rsi
   - type: Storage
-    maxSlots: 1
+    grid:
+    - 0,0,1,1
     whitelist:
       tags:
         - CaptainSabre
   - type: Clothing
     sprite: Clothing/Belt/holster.rsi
   - type: Storage
-    maxSlots: 3
+    grid:
+    - 0,0,3,1
 
 - type: entity
   parent: ClothingBeltStorageBase
   - type: Clothing
     sprite: Clothing/Belt/wand.rsi
   - type: Storage
-    maxSlots: 8
+    grid:
+    - 0,0,15,1
     whitelist:
       tags:
       - WizardWand
index e6685eb47a771d78558e2391c95a8bb6a6f5db22..6cbf5a69cae6d755385fe88840d77eceb7f5b8c1 100644 (file)
@@ -12,7 +12,8 @@
       visible: false
   - type: Clothing
   - type: Storage
-    maxSlots: 15
+    grid:
+    - 0,0,7,3
     maxItemSize: Small
     whitelist:
       tags:
index be8f02d8035eed30423a22f2700351d54d4ab919..81fddf379b6070c5630e3e25857e85fa52ccc70d 100644 (file)
@@ -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
index 5274a69bdb3a3d947a38a1bc6e093db26e81284f..12deec76e9593b1c4125b3ad85b17ea50c30ac76 100644 (file)
   - type: Clothing
     sprite: Clothing/Head/Hats/chefhat.rsi
   - type: Storage
-    maxSlots: 1
+    grid:
+    - 0,0,0,0
   - type: UserInterface
     interfaces:
       - key: enum.StorageUiKey.Key
     size: Small
     sprite: Clothing/Head/Hats/magician.rsi
   - type: Storage
-    maxSlots: 1
+    grid:
+    - 0,0,0,0
   - type: UserInterface
     interfaces:
       - key: enum.StorageUiKey.Key
index 8b86d8e253a5d7b72cfa9819ec3edcc6877f185d..4fbdc88526d8123c3265055c4b90c554e42bc903 100644 (file)
@@ -29,7 +29,8 @@
   id: ClothingOuterStorageBase
   components:
   - type: Storage
-    maxTotalWeight: 6
+    grid:
+    - 0,0,2,1
   - type: ContainerContainer
     containers:
       storagebase: !type:Container
index c7152b23d92521ee0e6f378e081123c8b197a41f..fcd14d2b9a5faceea7671faa693ad6f0db96b2ad 100644 (file)
@@ -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 (file)
index 0000000..e3c8ffd
--- /dev/null
@@ -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
index 8d4919a977d4f3de872639cfbe7139a2fa203d56..bdb908af36158f9fee9c94fca957303ff4ded256 100644 (file)
     node: bot
   - type: Storage
     maxItemSize: Huge
-    maxTotalWeight: 40
+    grid:
+    - 0,0,9,3
   - type: Access
     groups:
     - Cargo
index fec821419bf6e3a741fc16c7fd777ca634b647a8..a5a8393764658f6d26dc10710a6487390a6fee76 100644 (file)
   - type: Item
     size: Normal
   - type: Storage
-    maxSlots: 6
+    grid:
+    - 0,0,5,1
     whitelist:
       tags:
         - Cola
index 41aadf05fd9cde9481244d9989120b3283cd70f0..3e73714a6333a806fbc03103cdb0a917a7b4f2b2 100644 (file)
@@ -38,7 +38,8 @@
       map: ["pink-box6"]
       visible: false
   - type: Storage
-    maxSlots: 6
+    grid:
+    - 0,0,2,1
     whitelist:
       tags:
       - Donut
       map: ["box12"]
       visible: false
   - type: Storage
-    maxSlots: 12
+    grid:
+    - 0,0,5,1
     whitelist:
       tags:
       - Egg
       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
       map: ["box6"]
       visible: false
   - type: Storage
-    maxSlots: 6
+    grid:
+    - 0,0,2,1
   - type: Item
     sprite: Objects/Consumable/Food/Baked/nuggets.rsi
     size: Small
     sprite: Objects/Consumable/Food/Baked/donkpocket.rsi
     state: box
   - type: Storage
-    maxTotalWeight: 12
+    grid:
+    - 0,0,3,2
     whitelist:
       tags:
       - DonkPocket
   - type: Item
     sprite: Objects/Storage/Happyhonk/clown.rsi
     heldPrefix: box
+  - type: Storage
+    grid:
+    - 0,0,3,3
   - type: Tag
     tags:
     - Trash
   name: syndicate snack box
   components:
   - type: Storage
-    maxSlots: 9
+    grid:
+    - 0,0,5,2
   - type: StorageFill
     contents:
     # toy
index bea963b4b6c6890e7e870d933552620322395914..54bc12d4fa597453dfd38a18ec876e3791614682 100644 (file)
@@ -14,7 +14,8 @@
     sprite: Objects/Consumable/Smokeables/Cigarettes/Cartons/green.rsi\r
     size: Normal\r
   - type: Storage\r
-    maxSlots: 6\r
+    grid:\r
+    - 0,0,5,1\r
   - type: StorageFill\r
     contents:\r
       - id: CigPackGreen\r
index 6bc14cfb77e018d001b7337f9fb818fe4a3501fe..77c121e4c8347e342675425b6ac91c78b7e06fde 100644 (file)
@@ -43,7 +43,8 @@
       Steel: 50\r
   - type: SpaceGarbage\r
   - type: Storage\r
-    maxSlots: 5\r
+    grid:\r
+    - 0,0,4,1\r
   - type: Item\r
     size: Small\r
   - type: StorageFill\r
       Steel: 50\r
   - type: SpaceGarbage\r
   - type: Storage\r
-    maxSlots: 10\r
-    maxTotalWeight: 20\r
+    grid:\r
+    - 0,0,4,1\r
   - type: Item\r
     size: Small\r
   - type: StorageFill\r
index 324a28caca5d9d699483f8cd5c801a4032a877e7..44edce5e070d1d3d7b088a20a37ee2b52c1b49b4 100644 (file)
@@ -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
index eede84cd4b3b70030d2eefc45bdd655f707ae9e7..813c1352b410c5da1ce57b36899f5044dfc00506 100644 (file)
@@ -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
index d78def0c94d6df902b09f93918b3c22ce00a24e5..d8f11cdea29d33d530a007baafc44b1b02fe1d8f 100644 (file)
@@ -23,7 +23,8 @@
           False: {state: empty_icon}
   - type: Storage
     maxItemSize: Small
-    maxTotalWeight: 10
+    grid:
+    - 0,0,3,2
     whitelist:
       components:
         - Pill
index 46cc2dbd4fddb09efe1a535fbff15773b25f62d9..e693f9b7c4d581cc820a51d018d5b3479d2cf2dd 100644 (file)
     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
index f2d547dd8c85efba91c751f72a9f30abd7123801..1a579572d9748be99a2f2cda4732e260890cb3e5 100644 (file)
@@ -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
index 442e712851a892afb34b4bae8e0a167d7172141e..edb1a81239120007e73dc83765d9d46616efbd06 100644 (file)
@@ -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
index c04801f0c7511ceb6696c67ad555ca74b1f26f44..aa8823d70d864105748e4c02ef2882bb1908ed5f 100644 (file)
@@ -7,7 +7,8 @@
   - type: Item
     size: Ginormous
   - type: Storage
-    maxTotalWeight: 24
+    grid:
+    - 0,0,5,3
   - type: Tag
     tags:
     - Briefcase
   components:
   - type: Item
     size: Huge
-  - type: Storage
-    maxSlots: 6
-    maxTotalWeight: 24
-  - type: Tag
-    tags:
-    - Briefcase
 
 - type: entity
   name: brown briefcase
index d1ceae2c605374e58237392dcdf1d86c12a62466..f178fffae05c5d1ac3c6c9820ee188d0654ac5fe 100644 (file)
@@ -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
index 34de8d7e3065b7507f16c81dcafa8fd424a34779..a191666fb485d495cd272ebac108a23c3382e04b 100644 (file)
     sprite: Objects/Misc/bureaucracy.rsi
     size: Small
   - type: Storage
-    maxSlots: 10
     maxItemSize: Small
-    maxTotalWeight: 20
+    grid:
+    - 0,0,4,3
     whitelist:
       tags:
         - Document
     quickEquip: false
     sprite: Objects/Misc/clipboard.rsi
   - type: Storage
-    maxSlots: 15
-    maxTotalWeight: 30
+    grid:
+    - 0,0,5,3
     whitelist:
       tags:
         - Document
     quickEquip: false
     sprite: Objects/Misc/qm_clipboard.rsi
   - type: Storage
-    maxSlots: 20
+    grid:
+    - 0,0,4,3
     quickInsert: true
     whitelist:
       tags:
index 08ec019bf789c536a5312a38c0241ff161e1974a..18edca6c718e63d0d52e7587521b525b6cd281d9 100644 (file)
         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:
index 3e72257d5cb4d99a6cadce5169cdc00dc72492d8..1a23a8791c0984b313695ffa596e2a1d9de8f464 100644 (file)
@@ -37,7 +37,8 @@
     slots:
     - Belt
   - type: Storage
-    maxSlots: 1
+    grid:
+    - 0,0,0,1
   - type: UserInterface
     interfaces:
     - key: enum.StorageUiKey.Key
index a221b2ca1f8543cdfc1ce2deecce609da2dd9dff..4d35b51ffe48525af448b1583e3ef2a699912784 100644 (file)
@@ -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:
index 026c47f176532e6bef5a370cf4da6600393e05bc..7e02106e6a7bf0f730843a662f7fbb2264341ebf 100644 (file)
     slots:
     - belt
   - type: Storage
-    maxSlots: 40
+    grid:
+    - 0,0,7,4
     maxItemSize: Small
     quickInsert: true
     areaInsert: true
index 7f61cf69d4b88ef1cca541271e10e35197f2dfe0..567f91b15fd74e88225e0da7a1a4e3675376ad6b 100644 (file)
@@ -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
index 81f40c9d7cae89de0f691b821a2040e7e0bf2b59..ecd4cfd5d5929bef15ddbf2b55070a70870c8f59 100644 (file)
@@ -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
 
index 939af4a304086d7b7df2ee086b5715c7f1f38fdd..e4befcbb67066eaf0087029012ac1b2c0f6a4337 100644 (file)
@@ -15,7 +15,8 @@
     - type: Item
       size: Ginormous
     - type: Storage
-      maxSlots: 15
+      grid:
+      - 0,0,7,3
       quickInsert: true
       areaInsert: true
       whitelist:
index 1e73c47f9a5e94455360a13a8e58638796f15ef3..d7f2231ec992dc248d492ed666c819a771b1bcc8 100644 (file)
@@ -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
index dd9d771ebef792900b62142f21c9dd88fe3bd4e1..bbad13c831448dc7963c1f7432a27a6764798708 100644 (file)
@@ -15,7 +15,8 @@
     - MachineUpgrading
   - type: PartExchanger
   - type: Storage
-    maxSlots: 30
+    grid:
+    - 0,0,9,2
     quickInsert: true
     areaInsert: true
     whitelist:
index 231d82fb76a04df5839be9e183f95338c173c8bd..a36bfaf676be2d93f151d3bdde5871adbc471b65 100644 (file)
@@ -16,8 +16,9 @@
   - type: Item
     size: Ginormous
   - type: Storage
-    maxSlots: 10
     maxItemSize: Normal
+    grid:
+    - 0,0,9,3
     quickInsert: true
     areaInsert: true
     whitelist:
index e940b6cb9f711c94a133acfcf0a4cb59a6f39112..7ef0e6f5d2b0d3fc67e4a4bf7d4d0a5db3ef211e 100644 (file)
   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:
     tags:
       - PillCanister
   - type: Storage
-    maxTotalWeight: 10
+    grid:
+    - 0,0,4,1
     quickInsert: true
     areaInsert: true
     areaInsertRadius: 1
index 98affd271c8445d5f7994e1d36f92cb406f6ab11..e8601fcf355dba81f091437caf76c38384607e15 100644 (file)
@@ -4,8 +4,8 @@
   abstract: true
   components:
   - type: Storage
-    maxSlots: 7
-    maxTotalWeight: 14
+    grid:
+    - 0,0,6,1
   - type: Item
     size: Small
 
     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]
index 0acc760a5742f812fbfbc742c27eea0472e7314d..30bcdf3739d3742d90e6099b188fdf28d980371a 100644 (file)
@@ -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
     sprite: Objects/Tools/Toolboxes/toolbox_syn.rsi
   - type: Storage
     maxItemSize: Huge
-    maxSlots: 8
-    maxTotalWeight: 32
+    grid:
+    - 0,0,7,3
   - type: MeleeWeapon
     damage:
       types:
index c978d2da05d3d462d69564669108533fa1a3feec..ac9d39006508fb969e1465b0638281210277853c 100644 (file)
@@ -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
   - 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
index 6d5e0e3dcddd10f42dc190c29e74138a6a00d576..c638fac2075492ef87d30dfb4fa65c2f9e4bc09d 100644 (file)
@@ -47,7 +47,8 @@
   - type: Pullable
   - type: Occluder
   - type: Storage
-    maxSlots: 32
+    grid:
+    - 0,0,15,3
     maxItemSize: Normal
     whitelist:
       tags:
index 29061a06b13a04872c3f0c2fb3941a160ed5ef53..e3629fcf07f97461fd39432d0f06c51d930c5221 100644 (file)
@@ -25,7 +25,8 @@
       - !type:DoActsBehavior
         acts: [ "Destruction" ]
   - type: Storage
-    maxSlots: 6
+    grid:
+    - 0,0,5,3
     maxItemSize: Normal
   - type: ContainerContainer
     containers:
index f0ce9c46d36d0cf39b26561855ec2a634aaf7536..df25ed523873f946854c8952c75b7d92accf4969 100644 (file)
@@ -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:
index 6bad8c319fb937d4ad4af07eee7319b070902723..961d7854c08b8ced17b2e85689a2f36a4720dce8 100644 (file)
@@ -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
index d5c330f47edd145014fc8a972c6525e13cacb173..bad84227c851f815df988af8986c49a5f298bfea 100644 (file)
@@ -38,7 +38,8 @@
       - !type:DoActsBehavior
         acts: ["Destruction"]
   - type: Storage
-    maxSlots: 10
+    grid:
+    - 0,0,4,3
     maxItemSize: Small
     whitelist:
       tags:
index c9297d901b89f4eeb7258a03d8a5c3a1186f1206..fdf28fe9a0112475198690f644733866812e9662 100644 (file)
@@ -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
index 47d1fe3db4700a3cb7b7512a90b3608db2f64e76..f02a7e9fd6fd2051324875f098f82064de80e3ea 100644 (file)
@@ -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 (file)
index 0000000..3fe8540
Binary files /dev/null and b/Resources/Textures/Interface/Default/Storage/back.png differ
diff --git a/Resources/Textures/Interface/Default/Storage/exit.png b/Resources/Textures/Interface/Default/Storage/exit.png
new file mode 100644 (file)
index 0000000..186c447
Binary files /dev/null and b/Resources/Textures/Interface/Default/Storage/exit.png differ
diff --git a/Resources/Textures/Interface/Default/Storage/piece_bottom.png b/Resources/Textures/Interface/Default/Storage/piece_bottom.png
new file mode 100644 (file)
index 0000000..a860ddd
Binary files /dev/null and b/Resources/Textures/Interface/Default/Storage/piece_bottom.png differ
diff --git a/Resources/Textures/Interface/Default/Storage/piece_bottomLeft.png b/Resources/Textures/Interface/Default/Storage/piece_bottomLeft.png
new file mode 100644 (file)
index 0000000..43676e7
Binary files /dev/null and b/Resources/Textures/Interface/Default/Storage/piece_bottomLeft.png differ
diff --git a/Resources/Textures/Interface/Default/Storage/piece_bottomRight.png b/Resources/Textures/Interface/Default/Storage/piece_bottomRight.png
new file mode 100644 (file)
index 0000000..59a49cb
Binary files /dev/null and b/Resources/Textures/Interface/Default/Storage/piece_bottomRight.png differ
diff --git a/Resources/Textures/Interface/Default/Storage/piece_center.png b/Resources/Textures/Interface/Default/Storage/piece_center.png
new file mode 100644 (file)
index 0000000..27916d4
Binary files /dev/null and b/Resources/Textures/Interface/Default/Storage/piece_center.png differ
diff --git a/Resources/Textures/Interface/Default/Storage/piece_left.png b/Resources/Textures/Interface/Default/Storage/piece_left.png
new file mode 100644 (file)
index 0000000..a99eead
Binary files /dev/null and b/Resources/Textures/Interface/Default/Storage/piece_left.png differ
diff --git a/Resources/Textures/Interface/Default/Storage/piece_right.png b/Resources/Textures/Interface/Default/Storage/piece_right.png
new file mode 100644 (file)
index 0000000..ef7bd80
Binary files /dev/null and b/Resources/Textures/Interface/Default/Storage/piece_right.png differ
diff --git a/Resources/Textures/Interface/Default/Storage/piece_top.png b/Resources/Textures/Interface/Default/Storage/piece_top.png
new file mode 100644 (file)
index 0000000..219ec30
Binary files /dev/null and b/Resources/Textures/Interface/Default/Storage/piece_top.png differ
diff --git a/Resources/Textures/Interface/Default/Storage/piece_topLeft.png b/Resources/Textures/Interface/Default/Storage/piece_topLeft.png
new file mode 100644 (file)
index 0000000..bfe9134
Binary files /dev/null and b/Resources/Textures/Interface/Default/Storage/piece_topLeft.png differ
diff --git a/Resources/Textures/Interface/Default/Storage/piece_topRight.png b/Resources/Textures/Interface/Default/Storage/piece_topRight.png
new file mode 100644 (file)
index 0000000..c8a48b1
Binary files /dev/null and b/Resources/Textures/Interface/Default/Storage/piece_topRight.png differ
diff --git a/Resources/Textures/Interface/Default/Storage/sidebar_bottom.png b/Resources/Textures/Interface/Default/Storage/sidebar_bottom.png
new file mode 100644 (file)
index 0000000..5933925
Binary files /dev/null and b/Resources/Textures/Interface/Default/Storage/sidebar_bottom.png differ
diff --git a/Resources/Textures/Interface/Default/Storage/sidebar_fat.png b/Resources/Textures/Interface/Default/Storage/sidebar_fat.png
new file mode 100644 (file)
index 0000000..0223dca
Binary files /dev/null and b/Resources/Textures/Interface/Default/Storage/sidebar_fat.png differ
diff --git a/Resources/Textures/Interface/Default/Storage/sidebar_mid.png b/Resources/Textures/Interface/Default/Storage/sidebar_mid.png
new file mode 100644 (file)
index 0000000..20b5329
Binary files /dev/null and b/Resources/Textures/Interface/Default/Storage/sidebar_mid.png differ
diff --git a/Resources/Textures/Interface/Default/Storage/sidebar_top.png b/Resources/Textures/Interface/Default/Storage/sidebar_top.png
new file mode 100644 (file)
index 0000000..798c03d
Binary files /dev/null and b/Resources/Textures/Interface/Default/Storage/sidebar_top.png differ
diff --git a/Resources/Textures/Interface/Default/Storage/tile_blocked.png b/Resources/Textures/Interface/Default/Storage/tile_blocked.png
new file mode 100644 (file)
index 0000000..663f9b5
Binary files /dev/null and b/Resources/Textures/Interface/Default/Storage/tile_blocked.png differ
diff --git a/Resources/Textures/Interface/Default/Storage/tile_empty.png b/Resources/Textures/Interface/Default/Storage/tile_empty.png
new file mode 100644 (file)
index 0000000..551ac8f
Binary files /dev/null and b/Resources/Textures/Interface/Default/Storage/tile_empty.png differ
index b3ba05def2628eebfcf42c0a802b28d3f4fc8221..28bbb9f3d0ceb6880b1628a1388c1adda208b4e7 100644 (file)
@@ -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