diff --git a/Content.Client/Input/ContentContexts.cs b/Content.Client/Input/ContentContexts.cs index 03f4f3f38b..589de6d6a7 100644 --- a/Content.Client/Input/ContentContexts.cs +++ b/Content.Client/Input/ContentContexts.cs @@ -32,6 +32,7 @@ namespace Content.Client.Input common.AddFunction(ContentKeyFunctions.ToggleFullscreen); common.AddFunction(ContentKeyFunctions.MoveStoredItem); common.AddFunction(ContentKeyFunctions.RotateStoredItem); + common.AddFunction(ContentKeyFunctions.SaveItemLocation); common.AddFunction(ContentKeyFunctions.Point); common.AddFunction(ContentKeyFunctions.ZoomOut); common.AddFunction(ContentKeyFunctions.ZoomIn); diff --git a/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs b/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs index f0537079b9..aca9efcfe2 100644 --- a/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs +++ b/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs @@ -183,6 +183,7 @@ namespace Content.Client.Options.UI.Tabs AddButton(ContentKeyFunctions.SwapHands); AddButton(ContentKeyFunctions.MoveStoredItem); AddButton(ContentKeyFunctions.RotateStoredItem); + AddButton(ContentKeyFunctions.SaveItemLocation); AddHeader("ui-options-header-interaction-adv"); AddButton(ContentKeyFunctions.SmartEquipBackpack); diff --git a/Content.Client/UserInterface/Systems/Storage/Controls/StorageContainer.cs b/Content.Client/UserInterface/Systems/Storage/Controls/StorageContainer.cs index bd952fe957..e39ac5d322 100644 --- a/Content.Client/UserInterface/Systems/Storage/Controls/StorageContainer.cs +++ b/Content.Client/UserInterface/Systems/Storage/Controls/StorageContainer.cs @@ -12,6 +12,7 @@ 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; @@ -355,6 +356,40 @@ public sealed class StorageContainer : BaseWindow origin, currentLocation.Rotation); + foreach (var locations in storageComponent.SavedLocations) + { + if (!_entity.TryGetComponent(currentEnt, out var meta) || meta.EntityName != locations.Key) + continue; + + float spot = 0; + var marked = new List(); + + foreach (var location in locations.Value) + { + var shape = itemSystem.GetAdjustedItemShape(currentEnt, location); + var bound = shape.GetBoundingBox(); + + var spotFree = storageSystem.ItemFitsInGridLocation(currentEnt, StorageEntity.Value, location); + + if (spotFree) + spot++; + + for (var y = bound.Bottom; y <= bound.Top; y++) + { + for (var x = bound.Left; x <= bound.Right; x++) + { + if (TryGetBackgroundCell(x, y, out var cell) && shape.Contains(x, y) && !marked.Contains(cell)) + { + marked.Add(cell); + cell.ModulateSelfOverride = spotFree + ? Color.FromHsv((0.18f, 1 / spot, 0.5f / spot + 0.5f, 1f)) + : Color.FromHex("#2222CC"); + } + } + } + } + } + var validColor = usingInHand ? Color.Goldenrod : Color.FromHex("#1E8000"); for (var y = itemBounding.Bottom; y <= itemBounding.Top; y++) diff --git a/Content.Client/UserInterface/Systems/Storage/StorageUIController.cs b/Content.Client/UserInterface/Systems/Storage/StorageUIController.cs index 5f3ae3a2da..b865b54dd0 100644 --- a/Content.Client/UserInterface/Systems/Storage/StorageUIController.cs +++ b/Content.Client/UserInterface/Systems/Storage/StorageUIController.cs @@ -240,6 +240,16 @@ public sealed class StorageUIController : UIController, IOnSystemChanged().DoExamine(control.Entity); diff --git a/Content.Shared/Input/ContentKeyFunctions.cs b/Content.Shared/Input/ContentKeyFunctions.cs index ee4a4e9023..cf874434ec 100644 --- a/Content.Shared/Input/ContentKeyFunctions.cs +++ b/Content.Shared/Input/ContentKeyFunctions.cs @@ -36,6 +36,7 @@ namespace Content.Shared.Input public static readonly BoundKeyFunction SwapHands = "SwapHands"; public static readonly BoundKeyFunction MoveStoredItem = "MoveStoredItem"; public static readonly BoundKeyFunction RotateStoredItem = "RotateStoredItem"; + public static readonly BoundKeyFunction SaveItemLocation = "SaveItemLocation"; public static readonly BoundKeyFunction ThrowItemInHand = "ThrowItemInHand"; public static readonly BoundKeyFunction TryPullObject = "TryPullObject"; public static readonly BoundKeyFunction MovePulledObject = "MovePulledObject"; diff --git a/Content.Shared/Storage/EntitySystems/SharedStorageSystem.cs b/Content.Shared/Storage/EntitySystems/SharedStorageSystem.cs index ef1b2b7c44..9d364dded0 100644 --- a/Content.Shared/Storage/EntitySystems/SharedStorageSystem.cs +++ b/Content.Shared/Storage/EntitySystems/SharedStorageSystem.cs @@ -97,6 +97,7 @@ public abstract class SharedStorageSystem : EntitySystem SubscribeAllEvent(OnSetItemLocation); SubscribeAllEvent(OnInsertItemIntoLocation); SubscribeAllEvent(OnRemoveItem); + SubscribeAllEvent(OnSaveItemLocation); SubscribeLocalEvent(OnReclaimed); @@ -117,7 +118,8 @@ public abstract class SharedStorageSystem : EntitySystem Grid = new List(component.Grid), IsUiOpen = component.IsUiOpen, MaxItemSize = component.MaxItemSize, - StoredItems = storedItems + StoredItems = storedItems, + SavedLocations = component.SavedLocations }; } @@ -138,6 +140,8 @@ public abstract class SharedStorageSystem : EntitySystem var ent = EnsureEntity(nent, uid); component.StoredItems[ent] = location; } + + component.SavedLocations = state.SavedLocations; } public override void Shutdown() @@ -536,6 +540,35 @@ public abstract class SharedStorageSystem : EntitySystem InsertAt((storageEnt, storageComp), (itemEnt, null), msg.Location, out _, player, stackAutomatically: false); } + // TODO: if/when someone cleans up this shitcode please make all these + // handlers use a shared helper for checking that the ui is open etc, thanks + private void OnSaveItemLocation(StorageSaveItemLocationEvent msg, EntitySessionEventArgs args) + { + if (args.SenderSession.AttachedEntity is not {} player) + return; + + var storage = GetEntity(msg.Storage); + var item = GetEntity(msg.Item); + + if (!TryComp(storage, out var storageComp)) + return; + + if (!_ui.TryGetUi(storage, StorageComponent.StorageUiKey.Key, out var bui) || + !bui.SubscribedSessions.Contains(args.SenderSession)) + return; + + if (!Exists(item)) + { + Log.Error($"Player {args.SenderSession} saved location of non-existent item {msg.Item} stored in {ToPrettyString(storage)}"); + return; + } + + if (!ActionBlocker.CanInteract(player, item)) + return; + + SaveItemLocation(storage, item); + } + private void OnBoundUIOpen(EntityUid uid, StorageComponent storageComp, BoundUIOpenedEvent args) { if (!storageComp.IsUiOpen) @@ -945,6 +978,10 @@ public abstract class SharedStorageSystem : EntitySystem if (!Resolve(storageEnt, ref storageEnt.Comp) || !Resolve(itemEnt, ref itemEnt.Comp)) return false; + // if the item has an available saved location, use that + if (FindSavedLocation(storageEnt, itemEnt, out storageLocation)) + return true; + var storageBounding = storageEnt.Comp.Grid.GetBoundingBox(); Angle startAngle; @@ -987,6 +1024,76 @@ public abstract class SharedStorageSystem : EntitySystem return false; } + /// + /// Tries to find a saved location for an item from its name. + /// If none are saved or they are all blocked nothing is returned. + /// + public bool FindSavedLocation( + Entity ent, + Entity item, + [NotNullWhen(true)] out ItemStorageLocation? storageLocation) + { + storageLocation = null; + if (!Resolve(ent, ref ent.Comp)) + return false; + + var name = Name(item); + if (!ent.Comp.SavedLocations.TryGetValue(name, out var list)) + return false; + + foreach (var location in list) + { + if (ItemFitsInGridLocation(item, ent, location)) + { + storageLocation = location; + return true; + } + } + + return false; + } + + /// + /// Saves an item's location in the grid for later insertion to use. + /// + public void SaveItemLocation(Entity ent, Entity item) + { + if (!Resolve(ent, ref ent.Comp)) + return; + + // needs to actually be stored in it somewhere to save it + if (!ent.Comp.StoredItems.TryGetValue(item, out var location)) + return; + + var name = Name(item, item.Comp); + if (ent.Comp.SavedLocations.TryGetValue(name, out var list)) + { + // iterate to make sure its not already been saved + for (int i = 0; i < list.Count; i++) + { + var saved = list[i]; + + if (saved == location) + { + list.Remove(location); + return; + } + } + + list.Add(location); + } + else + { + list = new List() + { + location + }; + ent.Comp.SavedLocations[name] = list; + } + + Dirty(ent, ent.Comp); + } + /// /// Checks if an item fits into a specific spot on a storage grid. /// @@ -1165,6 +1272,8 @@ public abstract class SharedStorageSystem : EntitySystem public Dictionary StoredItems = new(); + public Dictionary> SavedLocations = new(); + public List Grid = new(); public ProtoId? MaxItemSize; diff --git a/Content.Shared/Storage/ItemStorageLocation.cs b/Content.Shared/Storage/ItemStorageLocation.cs index a43be5a44f..25d55e1e1a 100644 --- a/Content.Shared/Storage/ItemStorageLocation.cs +++ b/Content.Shared/Storage/ItemStorageLocation.cs @@ -8,16 +8,16 @@ public partial record struct ItemStorageLocation /// /// The rotation, stored a cardinal direction in order to reduce rounding errors. /// - [DataField] - private Direction _rotation; + [DataField("_rotation")] + public Direction Direction; /// /// The rotation of the piece in storage. /// public Angle Rotation { - get => _rotation.ToAngle(); - set => _rotation = value.GetCardinalDir(); + get => Direction.ToAngle(); + set => Direction = value.GetCardinalDir(); } /// diff --git a/Content.Shared/Storage/StorageComponent.cs b/Content.Shared/Storage/StorageComponent.cs index 98e80de025..2cae12f07a 100644 --- a/Content.Shared/Storage/StorageComponent.cs +++ b/Content.Shared/Storage/StorageComponent.cs @@ -32,6 +32,14 @@ namespace Content.Shared.Storage [DataField, ViewVariables(VVAccess.ReadWrite)] public Dictionary StoredItems = new(); + /// + /// A dictionary storing each saved item to its location in the grid. + /// When trying to quick insert an item, if there is an empty location with the same name it will be placed there. + /// Multiple items with the same name can be saved, they will be checked individually. + /// + [DataField] + public Dictionary> SavedLocations = new(); + /// /// A list of boxes that comprise a combined grid that determines the location that items can be stored. /// @@ -171,6 +179,20 @@ namespace Content.Shared.Storage } } + [Serializable, NetSerializable] + public sealed class StorageSaveItemLocationEvent : EntityEventArgs + { + public readonly NetEntity Item; + + public readonly NetEntity Storage; + + public StorageSaveItemLocationEvent(NetEntity item, NetEntity storage) + { + Item = item; + Storage = storage; + } + } + /// /// Network event for displaying an animation of entities flying into a storage entity diff --git a/Resources/Locale/en-US/escape-menu/ui/options-menu.ftl b/Resources/Locale/en-US/escape-menu/ui/options-menu.ftl index 46f5df48ad..ff56d54274 100644 --- a/Resources/Locale/en-US/escape-menu/ui/options-menu.ftl +++ b/Resources/Locale/en-US/escape-menu/ui/options-menu.ftl @@ -135,6 +135,7 @@ 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-function-save-item-location = Save item location ui-options-static-storage-ui = Lock storage window to hotbar ui-options-function-smart-equip-backpack = Smart-equip to backpack diff --git a/Resources/keybinds.yml b/Resources/keybinds.yml index 346156159a..886fec35de 100644 --- a/Resources/keybinds.yml +++ b/Resources/keybinds.yml @@ -171,6 +171,9 @@ binds: - function: RotateStoredItem type: State key: MouseRight +- function: SaveItemLocation + type: State + key: MouseMiddle - function: Drop type: State key: Q