Grid Inventory (#21931)

* Grid Inventory

* oh boy we keep cracking on

* auto insertion is kinda working? gross, too!

* pieces and proper layouts

* fix the sprites

* mousing over grid pieces... finally

* dragging deez nuts all over the screen

* eek!

* dragging is 90% less horrendous

* auto-rotating

* flatten

* Rotation at last

* fix rotation and change keybind for removing items.

* rebinding and keybinding

* wow! look at that! configurable with a button! cool!

* dragging is a bit cooler, eh?

* hover insert, my beloved

* add some grids for storage, fix 1x1 storages, fix multiple inputs at once

* el navigation

* oh yeah some stuff i forgor

* more fixes and QOL stuff

* the griddening

* the last of it (yippee)

* sloth review :)
This commit is contained in:
Nemanja
2023-12-04 18:04:39 -05:00
committed by GitHub
parent 4221ed2d4b
commit cc8984d096
99 changed files with 2014 additions and 619 deletions

View File

@@ -1,10 +1,10 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Shared.ActionBlocker;
using Content.Shared.CombatMode;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Destructible;
using Content.Shared.DoAfter;
using Content.Shared.FixedPoint;
using Content.Shared.Hands.Components;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Implants.Components;
@@ -17,7 +17,6 @@ using Content.Shared.Stacks;
using Content.Shared.Storage.Components;
using Content.Shared.Timing;
using Content.Shared.Verbs;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Containers;
using Robust.Shared.Map;
@@ -35,15 +34,16 @@ public abstract class SharedStorageSystem : EntitySystem
[Dependency] private readonly EntityLookupSystem _entityLookupSystem = default!;
[Dependency] protected readonly SharedEntityStorageSystem EntityStorage = default!;
[Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
[Dependency] private readonly SharedItemSystem _item = default!;
[Dependency] protected readonly SharedItemSystem ItemSystem = default!;
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
[Dependency] private readonly SharedHandsSystem _sharedHandsSystem = default!;
[Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] protected readonly SharedAudioSystem Audio = default!;
[Dependency] private readonly SharedCombatModeSystem _combatMode = default!;
[Dependency] protected readonly SharedTransformSystem _transform = default!;
[Dependency] protected readonly SharedTransformSystem TransformSystem = default!;
[Dependency] private readonly SharedStackSystem _stack = default!;
[Dependency] private readonly SharedUserInterfaceSystem _ui = default!;
[Dependency] protected readonly UseDelaySystem UseDelay = default!;
private EntityQuery<ItemComponent> _itemQuery;
@@ -69,17 +69,18 @@ public abstract class SharedStorageSystem : EntitySystem
SubscribeLocalEvent<StorageComponent, OpenStorageImplantEvent>(OnImplantActivate);
SubscribeLocalEvent<StorageComponent, AfterInteractEvent>(AfterInteract);
SubscribeLocalEvent<StorageComponent, DestructionEventArgs>(OnDestroy);
SubscribeLocalEvent<StorageComponent, StorageComponent.StorageInsertItemMessage>(OnInsertItemMessage);
SubscribeLocalEvent<StorageComponent, BoundUIOpenedEvent>(OnBoundUIOpen);
SubscribeLocalEvent<MetaDataComponent, StackCountChangedEvent>(OnStackCountChanged);
SubscribeLocalEvent<StorageComponent, EntInsertedIntoContainerMessage>(OnContainerModified);
SubscribeLocalEvent<StorageComponent, EntRemovedFromContainerMessage>(OnContainerModified);
SubscribeLocalEvent<StorageComponent, EntInsertedIntoContainerMessage>(OnEntInserted);
SubscribeLocalEvent<StorageComponent, EntRemovedFromContainerMessage>(OnEntRemoved);
SubscribeLocalEvent<StorageComponent, ContainerIsInsertingAttemptEvent>(OnInsertAttempt);
SubscribeLocalEvent<StorageComponent, AreaPickupDoAfterEvent>(OnDoAfter);
SubscribeLocalEvent<StorageComponent, StorageInteractWithItemEvent>(OnInteractWithItem);
SubscribeAllEvent<StorageInteractWithItemEvent>(OnInteractWithItem);
SubscribeAllEvent<StorageSetItemLocationEvent>(OnSetItemLocation);
SubscribeAllEvent<StorageInsertItemIntoLocationEvent>(OnInsertItemIntoLocation);
}
private void OnComponentInit(EntityUid uid, StorageComponent storageComp, ComponentInit args)
@@ -132,7 +133,7 @@ public abstract class SharedStorageSystem : EntitySystem
PlayerInsertHeldEntity(uid, args.User, storageComp);
// Always handle it, even if insertion fails.
// We don't want to trigger any AfterInteract logic here.
// Example bug: placing wires if item doesn't fit in backpack.
// Example issue would be placing wires if item doesn't fit in backpack.
args.Handled = true;
}
@@ -145,6 +146,7 @@ public abstract class SharedStorageSystem : EntitySystem
return;
OpenStorageUI(uid, args.User, storageComp);
args.Handled = true;
}
/// <summary>
@@ -152,11 +154,11 @@ public abstract class SharedStorageSystem : EntitySystem
/// </summary>
private void OnImplantActivate(EntityUid uid, StorageComponent storageComp, OpenStorageImplantEvent args)
{
// TODO: Make this an action or something.
if (args.Handled || !_xformQuery.TryGetComponent(uid, out var xform))
if (args.Handled)
return;
OpenStorageUI(uid, xform.ParentUid, storageComp);
OpenStorageUI(uid, args.Performer, storageComp);
args.Handled = true;
}
/// <summary>
@@ -224,8 +226,8 @@ public abstract class SharedStorageSystem : EntitySystem
var position = EntityCoordinates.FromMap(
parent.IsValid() ? parent : uid,
transformEnt.MapPosition,
_transform
TransformSystem.GetMapCoordinates(transformEnt),
TransformSystem
);
args.Handled = true;
@@ -270,8 +272,8 @@ public abstract class SharedStorageSystem : EntitySystem
var position = EntityCoordinates.FromMap(
xform.ParentUid.IsValid() ? xform.ParentUid : uid,
new MapCoordinates(_transform.GetWorldPosition(targetXform), targetXform.MapID),
_transform
new MapCoordinates(TransformSystem.GetWorldPosition(targetXform), targetXform.MapID),
TransformSystem
);
var angle = targetXform.LocalRotation;
@@ -284,7 +286,7 @@ public abstract class SharedStorageSystem : EntitySystem
}
}
// If we picked up atleast one thing, play a sound and do a cool animation!
// If we picked up at least one thing, play a sound and do a cool animation!
if (successfullyInserted.Count > 0)
{
Audio.PlayPvs(component.StorageInsertSound, uid);
@@ -300,7 +302,7 @@ public abstract class SharedStorageSystem : EntitySystem
private void OnDestroy(EntityUid uid, StorageComponent storageComp, DestructionEventArgs args)
{
var coordinates = _transform.GetMoverCoordinates(uid);
var coordinates = TransformSystem.GetMoverCoordinates(uid);
// Being destroyed so need to recalculate.
_containerSystem.EmptyContainer(storageComp.Container, destination: coordinates);
@@ -311,16 +313,24 @@ public abstract class SharedStorageSystem : EntitySystem
/// item in the user's hand if it is currently empty, or interact with the item using the user's currently
/// held item.
/// </summary>
private void OnInteractWithItem(EntityUid uid, StorageComponent storageComp, StorageInteractWithItemEvent args)
private void OnInteractWithItem(StorageInteractWithItemEvent msg, EntitySessionEventArgs args)
{
if (args.Session.AttachedEntity is not { } player)
if (args.SenderSession.AttachedEntity is not { } player)
return;
var entity = GetEntity(args.InteractedItemUID);
var uid = GetEntity(msg.StorageUid);
var entity = GetEntity(msg.InteractedItemUid);
if (!TryComp<StorageComponent>(uid, out var storageComp))
return;
if (!_ui.TryGetUi(uid, StorageComponent.StorageUiKey.Key, out var bui) ||
!bui.SubscribedSessions.Contains(args.SenderSession))
return;
if (!Exists(entity))
{
Log.Error($"Player {args.Session} interacted with non-existent item {args.InteractedItemUID} stored in {ToPrettyString(uid)}");
Log.Error($"Player {args.SenderSession} interacted with non-existent item {msg.InteractedItemUid} stored in {ToPrettyString(uid)}");
return;
}
@@ -346,12 +356,58 @@ public abstract class SharedStorageSystem : EntitySystem
_interactionSystem.InteractUsing(player, hands.ActiveHandEntity.Value, entity, Transform(entity).Coordinates, checkCanInteract: false);
}
private void OnInsertItemMessage(EntityUid uid, StorageComponent storageComp, StorageComponent.StorageInsertItemMessage args)
private void OnSetItemLocation(StorageSetItemLocationEvent msg, EntitySessionEventArgs args)
{
if (args.Session.AttachedEntity == null)
if (args.SenderSession.AttachedEntity is not { } player)
return;
PlayerInsertHeldEntity(uid, args.Session.AttachedEntity.Value, storageComp);
var storageEnt = GetEntity(msg.StorageEnt);
var itemEnt = GetEntity(msg.ItemEnt);
if (!TryComp<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.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) || !_sharedHandsSystem.IsHolding(player, itemEnt, out _))
return;
InsertAt((storageEnt, storageComp), (itemEnt, null), msg.Location, out _, player);
}
private void OnBoundUIOpen(EntityUid uid, StorageComponent storageComp, BoundUIOpenedEvent args)
@@ -363,17 +419,45 @@ public abstract class SharedStorageSystem : EntitySystem
}
}
private void OnContainerModified(EntityUid uid, StorageComponent component, ContainerModifiedMessage args)
private void OnEntInserted(Entity<StorageComponent> entity, ref EntInsertedIntoContainerMessage args)
{
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
if (component.Container == null)
if (entity.Comp.Container == null)
return;
if (args.Container.ID != StorageComponent.ContainerId)
return;
UpdateAppearance((uid, component, null));
UpdateUI((uid, component));
if (!entity.Comp.StoredItems.ContainsKey(GetNetEntity(args.Entity)))
{
if (!TryGetAvailableGridSpace((entity.Owner, entity.Comp), (args.Entity, null), out var location))
{
_containerSystem.Remove(args.Entity, args.Container, force: true);
return;
}
entity.Comp.StoredItems[GetNetEntity(args.Entity)] = location.Value;
Dirty(entity, entity.Comp);
}
UpdateAppearance((entity, entity.Comp, null));
UpdateUI((entity, entity.Comp));
}
private void OnEntRemoved(Entity<StorageComponent> entity, ref EntRemovedFromContainerMessage args)
{
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
if (entity.Comp.Container == null)
return;
if (args.Container.ID != StorageComponent.ContainerId)
return;
entity.Comp.StoredItems.Remove(GetNetEntity(args.Entity));
Dirty(entity, entity.Comp);
UpdateAppearance((entity, entity.Comp, null));
UpdateUI((entity, entity.Comp));
}
private void OnInsertAttempt(EntityUid uid, StorageComponent component, ContainerIsInsertingAttemptEvent args)
@@ -396,18 +480,8 @@ public abstract class SharedStorageSystem : EntitySystem
if (storage.Container == null)
return; // component hasn't yet been initialized.
int capacity;
int used;
if (storage.MaxSlots == null)
{
used = GetCumulativeItemSizes(uid, storage);
capacity = storage.MaxTotalWeight;
}
else
{
capacity = storage.MaxSlots.Value;
used = storage.Container.ContainedEntities.Count;
}
var capacity = storage.Grid.GetArea();
var used = GetCumulativeItemAreas((uid, storage));
_appearance.SetData(uid, StorageVisuals.StorageUsed, used, appearance);
_appearance.SetData(uid, StorageVisuals.Capacity, capacity, appearance);
@@ -450,8 +524,17 @@ public abstract class SharedStorageSystem : EntitySystem
/// <param name="reason">If returning false, the reason displayed to the player</param>
/// <param name="storageComp"></param>
/// <param name="item"></param>
/// <param name="ignoreStacks"></param>
/// <param name="ignoreLocation"></param>
/// <returns>true if it can be inserted, false otherwise</returns>
public bool CanInsert(EntityUid uid, EntityUid insertEnt, out string? reason, StorageComponent? storageComp = null, ItemComponent? item = null, bool ignoreStacks = false)
public bool CanInsert(
EntityUid uid,
EntityUid insertEnt,
out string? reason,
StorageComponent? storageComp = null,
ItemComponent? item = null,
bool ignoreStacks = false,
bool ignoreLocation = false)
{
if (!Resolve(uid, ref storageComp) || !Resolve(insertEnt, ref item, false))
{
@@ -485,38 +568,58 @@ public abstract class SharedStorageSystem : EntitySystem
return true;
}
var maxSize = _item.GetSizePrototype(GetMaxItemSize((uid, storageComp)));
if (_item.GetSizePrototype(item.Size) > maxSize)
var maxSize = ItemSystem.GetSizePrototype(GetMaxItemSize((uid, storageComp)));
if (ItemSystem.GetSizePrototype(item.Size) > maxSize)
{
reason = "comp-storage-too-big";
return false;
}
if (TryComp<StorageComponent>(insertEnt, out var insertStorage)
&& _item.GetSizePrototype(GetMaxItemSize((insertEnt, insertStorage))) >= maxSize)
&& ItemSystem.GetSizePrototype(GetMaxItemSize((insertEnt, insertStorage))) >= maxSize)
{
reason = "comp-storage-too-big";
return false;
}
if (storageComp.MaxSlots != null)
if (!ignoreLocation && !storageComp.StoredItems.ContainsKey(GetNetEntity(insertEnt)))
{
if (storageComp.Container.ContainedEntities.Count >= storageComp.MaxSlots)
if (!TryGetAvailableGridSpace((uid, storageComp), (insertEnt, item), out _))
{
reason = "comp-storage-insufficient-capacity";
return false;
}
}
else if (_item.GetItemSizeWeight(item.Size) + GetCumulativeItemSizes(uid, storageComp) > storageComp.MaxTotalWeight)
{
reason = "comp-storage-insufficient-capacity";
return false;
}
reason = null;
return true;
}
/// <summary>
/// Inserts into the storage container at a given location
/// </summary>
/// <returns>true if the entity was inserted, false otherwise. This will also return true if a stack was partially
/// inserted.</returns>
public bool InsertAt(
Entity<StorageComponent?> uid,
Entity<ItemComponent?> insertEnt,
ItemStorageLocation location,
out EntityUid? stackedEntity,
EntityUid? user = null,
bool playSound = true)
{
stackedEntity = null;
if (!Resolve(uid, ref uid.Comp))
return false;
if (!ItemFitsInGridLocation(insertEnt, uid, location))
return false;
uid.Comp.StoredItems[GetNetEntity(insertEnt)] = location;
Dirty(uid, uid.Comp);
return Insert(uid, insertEnt, out stackedEntity, out _, user: user, storageComp: uid.Comp, playSound: playSound);
}
/// <summary>
/// Inserts into the storage container
/// </summary>
@@ -653,6 +756,148 @@ public abstract class SharedStorageSystem : EntitySystem
return true;
}
/// <summary>
/// Attempts to set the location of an item already inside of a storage container.
/// </summary>
public bool TrySetItemStorageLocation(Entity<ItemComponent?> itemEnt, Entity<StorageComponent?> storageEnt, ItemStorageLocation location)
{
if (!Resolve(itemEnt, ref itemEnt.Comp) || !Resolve(storageEnt, ref storageEnt.Comp))
return false;
if (!storageEnt.Comp.Container.ContainedEntities.Contains(itemEnt))
return false;
if (!ItemFitsInGridLocation(itemEnt, storageEnt, location.Position, location.Rotation))
return false;
storageEnt.Comp.StoredItems[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;
var gridBounds = storageEnt.Comp.Grid.GetBoundingBox();
if (!gridBounds.Contains(position))
return false;
var itemShape = ItemSystem.GetAdjustedItemShape(itemEnt, rotation, position);
foreach (var box in itemShape)
{
for (var offsetY = box.Bottom; offsetY <= box.Top; offsetY++)
{
for (var offsetX = box.Left; offsetX <= box.Right; offsetX++)
{
var pos = (offsetX, offsetY);
if (!IsGridSpaceEmpty(itemEnt, storageEnt, pos))
return false;
}
}
}
return true;
}
/// <summary>
/// Checks if a space on a grid is valid and not occupied by any other pieces.
/// </summary>
public bool IsGridSpaceEmpty(Entity<ItemComponent?> itemEnt, Entity<StorageComponent?> storageEnt, Vector2i location)
{
if (!Resolve(storageEnt, ref storageEnt.Comp))
return false;
var validGrid = false;
foreach (var grid in storageEnt.Comp.Grid)
{
if (grid.Contains(location))
{
validGrid = true;
break;
}
}
if (!validGrid)
return false;
foreach (var (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>
@@ -661,13 +906,7 @@ public abstract class SharedStorageSystem : EntitySystem
if (!Resolve(uid, ref uid.Comp))
return false;
//todo maybe this shouldn't be authoritative over weight? idk.
if (uid.Comp.MaxSlots != null)
{
return uid.Comp.Container.ContainedEntities.Count < uid.Comp.MaxSlots || HasSpaceInStacks(uid);
}
return GetCumulativeItemSizes(uid, uid.Comp) < uid.Comp.MaxTotalWeight || HasSpaceInStacks(uid);
return GetCumulativeItemAreas(uid) < uid.Comp.Grid.GetArea() || HasSpaceInStacks(uid);
}
private bool HasSpaceInStacks(Entity<StorageComponent?> uid, string? stackType = null)
@@ -695,17 +934,17 @@ public abstract class SharedStorageSystem : EntitySystem
/// <summary>
/// Returns the sum of all the ItemSizes of the items inside of a storage.
/// </summary>
public int GetCumulativeItemSizes(EntityUid uid, StorageComponent? component = null)
public int GetCumulativeItemAreas(Entity<StorageComponent?> entity)
{
if (!Resolve(uid, ref component))
if (!Resolve(entity, ref entity.Comp))
return 0;
var sum = 0;
foreach (var item in component.Container.ContainedEntities)
foreach (var item in entity.Comp.Container.ContainedEntities)
{
if (!_itemQuery.TryGetComponent(item, out var itemComp))
continue;
sum += _item.GetItemSizeWeight(itemComp.Size);
sum += ItemSystem.GetItemShape((item, itemComp)).GetArea();
}
return sum;
@@ -722,7 +961,7 @@ public abstract class SharedStorageSystem : EntitySystem
if (!_itemQuery.TryGetComponent(uid, out var item))
return DefaultStorageMaxItemSize;
var size = _item.GetSizePrototype(item.Size);
var size = ItemSystem.GetSizePrototype(item.Size);
// if there is no max item size specified, the value used
// is one below the item size of the storage entity, clamped at ItemSize.Tiny
@@ -742,17 +981,6 @@ public abstract class SharedStorageSystem : EntitySystem
}
}
public FixedPoint2 GetStorageFillPercentage(Entity<StorageComponent?> uid)
{
if (!Resolve(uid, ref uid.Comp))
return 0;
var slotPercent = FixedPoint2.New(uid.Comp.Container.ContainedEntities.Count) / uid.Comp.MaxSlots ?? FixedPoint2.Zero;
var weightPercent = FixedPoint2.New(GetCumulativeItemSizes(uid)) / uid.Comp.MaxTotalWeight;
return FixedPoint2.Max(slotPercent, weightPercent);
}
/// <summary>
/// Plays a clientside pickup animation for the specified uid.
/// </summary>