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);
_deferCommands.Add(_inputManager.SaveToUserData);
}
+ private void HandleStaticStorageUI(BaseButton.ButtonToggledEventArgs args)
+ {
+ _cfg.SetCVar(CCVars.StaticStorageUI, args.Pressed);
+ _cfg.SaveToFile();
+ }
+
public KeyRebindTab()
{
IoCManager.InjectDependencies(this);
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);
-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);
}
}
+
-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);
}
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 />
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);
}
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]);
}
+++ /dev/null
-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();
- }
-}
+++ /dev/null
-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;
- }
- }
-}
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;
{
private InventoryUIController? _inventory;
private HandsUIController? _hands;
+ private StorageUIController? _storage;
public override void Initialize()
{
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()
<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"
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"
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);
}
--- /dev/null
+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);
+ }
+}
--- /dev/null
+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);
+ }
+}
--- /dev/null
+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;
+ }
+}
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)
{
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;
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
};
+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;
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);
// 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);
}
}
}
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
*/
{
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);
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";
[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]
[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)
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;
{
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;
+ }
}
+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;
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;
[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;
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)
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;
}
return;
OpenStorageUI(uid, args.User, storageComp);
+ args.Handled = true;
}
/// <summary>
/// </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>
var position = EntityCoordinates.FromMap(
parent.IsValid() ? parent : uid,
- transformEnt.MapPosition,
- _transform
+ TransformSystem.GetMapCoordinates(transformEnt),
+ TransformSystem
);
args.Handled = true;
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;
}
}
- // 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);
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);
/// 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;
}
_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)
}
}
- 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)
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);
/// <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))
{
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>
}
/// <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)
/// <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;
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
}
}
- 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>
--- /dev/null
+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;
+ }
+};
using Robust.Shared.Containers;
using Robust.Shared.GameStates;
using Robust.Shared.Map;
+using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
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")]
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>
--- /dev/null
+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;
+ }
+}
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
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
- type: StorageFill
contents:
- id: BoxSurvival
+ - id: BoxForensicPad
- id: Lighter
- id: CigPackBlack
- - id: BoxForensicPad
- id: HandLabeler
- type: entity
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
- state: box
- state: light
- type: Storage
- maxTotalWeight: 24
+ grid:
+ - 0,0,5,3
whitelist:
components:
- LightBulb
- state: box
- state: lighttube
- type: Storage
- maxTotalWeight: 24
+ grid:
+ - 0,0,5,3
whitelist:
components:
- LightBulb
- 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
id: BoxMouthSwab
components:
- type: Storage
- maxTotalWeight: 20
+ grid:
+ - 0,0,4,3
- type: StorageFill
contents:
- id: DiseaseSwab
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
description: A box full of zipties.
components:
- type: Storage
- maxTotalWeight: 20
+ grid:
+ - 0,0,4,3
whitelist:
components:
- Handcuff
description: A box of forensic pads.
components:
- type: Storage
- maxTotalWeight: 20
+ grid:
+ - 0,0,4,3
- type: StorageFill
contents:
- id: ForensicPad
- type: Item
size: Ginormous
- type: Storage
- maxSlots: 8
maxItemSize: Normal
+ grid:
+ - 0,0,7,1
- type: StorageFill
contents:
- id: ExGrenade
size: Ginormous
- type: Storage
maxItemSize: Huge
+ grid:
+ - 0,0,6,3
- type: StorageFill
contents:
- id: WeaponSniperHristov
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
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
size: Ginormous
- type: Storage
maxItemSize: Huge
- maxTotalWeight: 200 #5 duffels worth of gear
+ grid:
+ - 0,0,19,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
-
+
- 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
visible: false
- type: Clothing
- type: Storage
- maxSlots: 15
+ grid:
+ - 0,0,7,3
maxItemSize: Small
whitelist:
tags:
- 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
- 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
id: ClothingOuterStorageBase
components:
- type: Storage
- maxTotalWeight: 6
+ grid:
+ - 0,0,2,1
- type: ContainerContainer
containers:
storagebase: !type:Container
id: ClothingShoesStorageBase
components:
- type: Storage
- maxSlots: 1
+ grid:
+ - 0,0,1,1
maxItemSize: Normal
- type: ContainerContainer
containers:
--- /dev/null
+- 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
node: bot
- type: Storage
maxItemSize: Huge
- maxTotalWeight: 40
+ grid:
+ - 0,0,9,3
- type: Access
groups:
- Cargo
- type: Item
size: Normal
- type: Storage
- maxSlots: 6
+ grid:
+ - 0,0,5,1
whitelist:
tags:
- Cola
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
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
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
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
map: ["cigar8"]
visible: false
- type: Storage
- maxSlots: 8
+ grid:
+ - 0,0,3,1
- type: Item
sprite: Objects/Consumable/Smokeables/Cigars/case.rsi
size: Small
False: {state: empty_icon}
- type: Storage
maxItemSize: Small
- maxTotalWeight: 10
+ grid:
+ - 0,0,3,2
whitelist:
components:
- Pill
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
- type: Item
size: Small
- type: Storage
- maxTotalWeight: 8
+ grid:
+ - 0,0,3,1
whitelist:
tags:
- Dice
sprite: Objects/Fun/dice.rsi
state: magicdicebag
- type: Storage
- maxTotalWeight: 20
+ grid:
+ - 0,0,4,3
- 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
- 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
sprite: Objects/Storage/medalcase.rsi
size: Normal
- type: Storage
- maxSlots: 8
- maxTotalWeight: 16
+ grid:
+ - 0,0,7,1
- type: StorageFill
contents:
- id: ClothingNeckGoldmedal
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:
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:
slots:
- Belt
- type: Storage
- maxSlots: 1
+ grid:
+ - 0,0,0,1
- type: UserInterface
interfaces:
- key: enum.StorageUiKey.Key
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:
slots:
- belt
- type: Storage
- maxSlots: 40
+ grid:
+ - 0,0,7,4
maxItemSize: Small
quickInsert: true
areaInsert: true
- state: icon-0
map: ["enum.StorageFillLayers.Fill"]
- type: Storage
- maxTotalWeight: 48
maxItemSize: Small
+ grid:
+ - 0,0,7,5
quickInsert: true
areaInsert: true
storageOpenSound:
parent: TrashBagBlue
components:
- type: Storage
- maxSlots: 100
maxItemSize: Huge
- maxTotalWeight: 200
+ grid:
+ - 0,0,19,9
quickInsert: true
areaInsert: true
areaInsertRadius: 1000
type: StorageBoundUserInterface
- type: Storage
maxItemSize: Normal
- maxTotalWeight: 40
+ grid:
+ - 0,0,7,4
- type: TileFrictionModifier
modifier: 0.4 # makes it slide
- type: Item
size: Ginormous
- type: Storage
- maxSlots: 15
+ grid:
+ - 0,0,7,3
quickInsert: true
areaInsert: true
whitelist:
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
- MachineUpgrading
- type: PartExchanger
- type: Storage
- maxSlots: 30
+ grid:
+ - 0,0,9,2
quickInsert: true
areaInsert: true
whitelist:
- type: Item
size: Ginormous
- type: Storage
- maxSlots: 10
maxItemSize: Normal
+ grid:
+ - 0,0,9,3
quickInsert: true
areaInsert: true
whitelist:
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
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]
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:
container: storagebase
- type: PneumaticCannon
- type: Storage
- maxSlots: 8
maxItemSize: Normal
- maxTotalWeight: 32
+ grid:
+ - 0,0,7,3
blacklist:
tags:
- CannonRestrict
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
- type: Pullable
- type: Occluder
- type: Storage
- maxSlots: 32
+ grid:
+ - 0,0,15,3
maxItemSize: Normal
whitelist:
tags:
- !type:DoActsBehavior
acts: [ "Destruction" ]
- type: Storage
- maxSlots: 6
+ grid:
+ - 0,0,5,3
maxItemSize: Normal
- type: ContainerContainer
containers:
description: A cabinet for all your filing needs.
components:
- type: Storage
- maxSlots: 12
+ grid:
+ - 0,0,9,3
maxItemSize: Normal
whitelist:
tags:
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:
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
- !type:DoActsBehavior
acts: ["Destruction"]
- type: Storage
- maxSlots: 10
+ grid:
+ - 0,0,4,3
maxItemSize: Small
whitelist:
tags:
storagebase: !type:Container
ents: [ ]
- type: Storage
- maxSlots: 5
maxItemSize: Huge
- maxTotalWeight: 40
+ grid:
+ - 0,0,10,5
- type: artifactEffect
id: EffectPhasing
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
- 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