From 91b185e3c2cf4350ccfb93576a0ae7fe8af9f491 Mon Sep 17 00:00:00 2001 From: Leon Friedrich <60421075+ElectroJr@users.noreply.github.com> Date: Sat, 20 Nov 2021 18:26:01 +1300 Subject: [PATCH] Rejigging Item slots (#4933) * itemslot overhaul * remove "shared" prefix * handle component shutdown * comments, cleanup, tests * comments and minor tweak * rename ItemSlotManagerState * fix swapping * fix merge * Add ItemSlot verb text override * fix merge (IEntity -> entityUid) * Fix merge (LabelSystem) * Fix merge (nuke disk) * fix test --- .../Tests/InventoryHelpersTest.cs | 1 - .../Tests/PDA/PDAExtensionsTests.cs | 17 +- .../Tests/Utility/EntityWhitelistTest.cs | 2 +- .../Cabinet/ItemCabinetComponent.cs | 6 +- Content.Server/Cabinet/ItemCabinetSystem.cs | 82 ++-- .../Cargo/Components/CargoTelepadComponent.cs | 6 +- .../Label/Components/PaperLabelComponent.cs | 3 +- Content.Server/Labels/Label/LabelSystem.cs | 36 +- Content.Server/Nuke/NukeComponent.cs | 16 +- Content.Server/Nuke/NukeSystem.cs | 31 +- Content.Server/PDA/PDAComponent.cs | 16 +- Content.Server/PDA/PDASystem.cs | 63 +-- Content.Server/Sandbox/SandboxManager.cs | 6 +- .../Containers/ItemSlot/ItemSlotEvents.cs | 23 - .../Containers/ItemSlot/ItemSlotsComponent.cs | 124 +++++ .../Containers/ItemSlot/ItemSlotsSystem.cs | 455 ++++++++++++++++++ .../ItemSlot/SharedItemSlotsComponent.cs | 54 --- .../ItemSlot/SharedItemSlotsSystem.cs | 255 ---------- .../en-US/containers/item-slots-component.ftl | 6 +- .../Entities/Objects/Devices/nuke.yml | 20 +- .../Entities/Objects/Devices/pda.yml | 42 +- .../Entities/Objects/Fun/crayons.yml | 18 + .../Objects/Specific/Medical/morgue.yml | 12 +- .../Structures/Storage/Crates/base.yml | 12 +- .../Wallmounts/extinguisher_cabinet.yml | 25 +- .../Structures/Wallmounts/fireaxe_cabinet.yml | 24 +- .../Interface/VerbIcons/drop.svg.192dpi.png | Bin 0 -> 587 bytes .../VerbIcons/drop.svg.192dpi.png.yml | 4 + 28 files changed, 805 insertions(+), 554 deletions(-) delete mode 100644 Content.Shared/Containers/ItemSlot/ItemSlotEvents.cs create mode 100644 Content.Shared/Containers/ItemSlot/ItemSlotsComponent.cs create mode 100644 Content.Shared/Containers/ItemSlot/ItemSlotsSystem.cs delete mode 100644 Content.Shared/Containers/ItemSlot/SharedItemSlotsComponent.cs delete mode 100644 Content.Shared/Containers/ItemSlot/SharedItemSlotsSystem.cs create mode 100644 Resources/Textures/Interface/VerbIcons/drop.svg.192dpi.png create mode 100644 Resources/Textures/Interface/VerbIcons/drop.svg.192dpi.png.yml diff --git a/Content.IntegrationTests/Tests/InventoryHelpersTest.cs b/Content.IntegrationTests/Tests/InventoryHelpersTest.cs index 46de2637e8..51c489755f 100644 --- a/Content.IntegrationTests/Tests/InventoryHelpersTest.cs +++ b/Content.IntegrationTests/Tests/InventoryHelpersTest.cs @@ -42,7 +42,6 @@ namespace Content.IntegrationTests.Tests Slots: - idcard - type: PDA - idCard: AssistantIDCard "; [Test] public async Task SpawnItemInSlotTest() diff --git a/Content.IntegrationTests/Tests/PDA/PDAExtensionsTests.cs b/Content.IntegrationTests/Tests/PDA/PDAExtensionsTests.cs index 1b686497b0..bc90dd0095 100644 --- a/Content.IntegrationTests/Tests/PDA/PDAExtensionsTests.cs +++ b/Content.IntegrationTests/Tests/PDA/PDAExtensionsTests.cs @@ -29,13 +29,12 @@ namespace Content.IntegrationTests.Tests.PDA id: {PdaDummy} name: {PdaDummy} components: - - type: ItemSlots - slots: - pdaIdSlot: - whitelist: - components: - - IdCard - type: PDA + idSlot: + name: ID Card + whitelist: + components: + - IdCard - type: Item"; [Test] @@ -77,9 +76,9 @@ namespace Content.IntegrationTests.Tests.PDA var pdaComponent = dummyPda.GetComponent(); var pdaIdCard = sEntityManager.SpawnEntity(IdCardDummy, player.Transform.MapPosition); - var itemSlots = dummyPda.GetComponent(); - sEntityManager.EntitySysManager.GetEntitySystem() - .TryInsertContent(itemSlots, pdaIdCard, pdaComponent.IdSlot); + var itemSlots = dummyPda.GetComponent(); + sEntityManager.EntitySysManager.GetEntitySystem() + .TryInsert(dummyPda.Uid, pdaComponent.IdSlot, pdaIdCard); var pdaContainedId = pdaComponent.ContainedID; // The PDA in the hand should be found first diff --git a/Content.IntegrationTests/Tests/Utility/EntityWhitelistTest.cs b/Content.IntegrationTests/Tests/Utility/EntityWhitelistTest.cs index 08cacfadd6..c7352d98c4 100644 --- a/Content.IntegrationTests/Tests/Utility/EntityWhitelistTest.cs +++ b/Content.IntegrationTests/Tests/Utility/EntityWhitelistTest.cs @@ -98,7 +98,7 @@ namespace Content.IntegrationTests.Tests.Utility // Test from serialized var dummy = entityManager.SpawnEntity("WhitelistDummy", mapCoordinates); - var whitelistSer = dummy.GetComponent().Slots.Values.First().Whitelist; + var whitelistSer = dummy.GetComponent().Slots.Values.First().Whitelist; Assert.That(whitelistSer, Is.Not.Null); Assert.That(whitelistSer.Components, Is.Not.Null); diff --git a/Content.Server/Cabinet/ItemCabinetComponent.cs b/Content.Server/Cabinet/ItemCabinetComponent.cs index 8b2e4d3416..457ec9631e 100644 --- a/Content.Server/Cabinet/ItemCabinetComponent.cs +++ b/Content.Server/Cabinet/ItemCabinetComponent.cs @@ -1,3 +1,4 @@ +using Content.Shared.Containers.ItemSlots; using Content.Shared.Sound; using Robust.Shared.GameObjects; using Robust.Shared.Serialization.Manager.Attributes; @@ -21,10 +22,11 @@ namespace Content.Server.Cabinet public SoundSpecifier DoorSound { get; set; } = default!; /// - /// The slot name, used to get the actual item slot from the ItemSlotsComponent. + /// The that stores the actual item. The entity whitelist, sounds, and other + /// behaviours are specified by this definition. /// [DataField("cabinetSlot")] - public string CabinetSlot = "cabinetSlot"; + public ItemSlot CabinetSlot = new(); /// /// Whether the cabinet is currently open or not. diff --git a/Content.Server/Cabinet/ItemCabinetSystem.cs b/Content.Server/Cabinet/ItemCabinetSystem.cs index 8bc4e23717..a53b5ff738 100644 --- a/Content.Server/Cabinet/ItemCabinetSystem.cs +++ b/Content.Server/Cabinet/ItemCabinetSystem.cs @@ -4,54 +4,63 @@ using Content.Shared.Containers.ItemSlots; using Content.Shared.Interaction; using Content.Shared.Verbs; using Robust.Shared.Audio; +using Robust.Shared.Containers; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Localization; using Robust.Shared.Player; -using System; namespace Content.Server.Cabinet { public class ItemCabinetSystem : EntitySystem { - [Dependency] private readonly SharedItemSlotsSystem _itemSlotsSystem = default!; + [Dependency] private readonly ItemSlotsSystem _itemSlotsSystem = default!; public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(OnInteractUsing); - SubscribeLocalEvent(OnInteractHand); + SubscribeLocalEvent(OnComponentInit); + SubscribeLocalEvent(OnComponentRemove); + SubscribeLocalEvent(OnComponentStartup); + SubscribeLocalEvent(OnActivateInWorld); - SubscribeLocalEvent(InitializeAppearance); - SubscribeLocalEvent(OnItemSlotChanged); SubscribeLocalEvent(AddToggleOpenVerb); + + SubscribeLocalEvent(OnContainerModified); + SubscribeLocalEvent(OnContainerModified); } - private void InitializeAppearance(EntityUid uid, ItemCabinetComponent component, ComponentStartup args) + private void OnComponentInit(EntityUid uid, ItemCabinetComponent cabinet, ComponentInit args) { - UpdateAppearance(uid, component); + _itemSlotsSystem.AddItemSlot(uid, cabinet.Name, cabinet.CabinetSlot); + } + private void OnComponentRemove(EntityUid uid, ItemCabinetComponent cabinet, ComponentRemove args) + { + _itemSlotsSystem.RemoveItemSlot(uid, cabinet.CabinetSlot); + } + + private void OnComponentStartup(EntityUid uid, ItemCabinetComponent cabinet, ComponentStartup args) + { + UpdateAppearance(uid, cabinet); + _itemSlotsSystem.SetLock(uid, cabinet.CabinetSlot.ID, !cabinet.Opened); } private void UpdateAppearance(EntityUid uid, ItemCabinetComponent? cabinet = null, - SharedItemSlotsComponent? itemSlots = null, SharedAppearanceComponent? appearance = null) { - if (!Resolve(uid, ref cabinet, ref itemSlots, ref appearance, false)) + if (!Resolve(uid, ref cabinet, ref appearance, false)) return; appearance.SetData(ItemCabinetVisuals.IsOpen, cabinet.Opened); - - if (!itemSlots.Slots.TryGetValue(cabinet.CabinetSlot, out var slot)) - return; - - appearance.SetData(ItemCabinetVisuals.ContainsItem, slot.HasEntity); + appearance.SetData(ItemCabinetVisuals.ContainsItem, cabinet.CabinetSlot.HasItem); } - private void OnItemSlotChanged(EntityUid uid, ItemCabinetComponent cabinet, ItemSlotChangedEvent args) + private void OnContainerModified(EntityUid uid, ItemCabinetComponent cabinet, ContainerModifiedMessage args) { - UpdateAppearance(uid, cabinet, args.SlotsComponent); + if (args.Container.ID == cabinet.CabinetSlot.ID) + UpdateAppearance(uid, cabinet); } private void AddToggleOpenVerb(EntityUid uid, ItemCabinetComponent cabinet, GetActivationVerbsEvent args) @@ -75,44 +84,6 @@ namespace Content.Server.Cabinet args.Verbs.Add(toggleVerb); } - /// - /// Try insert an item if the cabinet is opened. Otherwise, just try open it. - /// - private void OnInteractUsing(EntityUid uid, ItemCabinetComponent comp, InteractUsingEvent args) - { - if (args.Handled) - return; - - if (!comp.Opened) - ToggleItemCabinet(uid, comp); - else - _itemSlotsSystem.TryInsertContent(uid, args.Used, args.User); - - args.Handled = true; - } - - /// - /// If the cabinet is opened and has an entity, try and take it. Otherwise toggle the cabinet open/closed; - /// - private void OnInteractHand(EntityUid uid, ItemCabinetComponent comp, InteractHandEvent args) - { - if (args.Handled) - return; - - if (!EntityManager.TryGetComponent(uid, out SharedItemSlotsComponent itemSlots)) - return; - - if (!itemSlots.Slots.TryGetValue(comp.CabinetSlot, out var slot)) - return; - - if (comp.Opened && slot.HasEntity) - _itemSlotsSystem.TryEjectContent(uid, comp.CabinetSlot, args.User); - else - ToggleItemCabinet(uid, comp); - - args.Handled = true; - } - private void OnActivateInWorld(EntityUid uid, ItemCabinetComponent comp, ActivateInWorldEvent args) { if (args.Handled) @@ -132,6 +103,7 @@ namespace Content.Server.Cabinet cabinet.Opened = !cabinet.Opened; SoundSystem.Play(Filter.Pvs(uid), cabinet.DoorSound.GetSound(), uid, AudioHelpers.WithVariation(0.15f)); + _itemSlotsSystem.SetLock(uid, cabinet.CabinetSlot.ID, !cabinet.Opened); UpdateAppearance(uid, cabinet); } diff --git a/Content.Server/Cargo/Components/CargoTelepadComponent.cs b/Content.Server/Cargo/Components/CargoTelepadComponent.cs index 3ae292c72d..4428a96661 100644 --- a/Content.Server/Cargo/Components/CargoTelepadComponent.cs +++ b/Content.Server/Cargo/Components/CargoTelepadComponent.cs @@ -20,6 +20,7 @@ namespace Content.Server.Cargo.Components { //This entire class is a PLACEHOLDER for the cargo shuttle. + //welp only need auto-docking now. [RegisterComponent] public class CargoTelepadComponent : Component @@ -136,10 +137,9 @@ namespace Content.Server.Cargo.Components ("approver", data.Approver))); // attempt to attach the label - if (_entityManager.TryGetComponent(product.Uid, out PaperLabelComponent label) && - _entityManager.TryGetComponent(product.Uid, out SharedItemSlotsComponent slots)) + if (_entityManager.TryGetComponent(product.Uid, out PaperLabelComponent label)) { - EntitySystem.Get().TryInsertContent(slots, printed, label.LabelSlot); + EntitySystem.Get().TryInsert(OwnerUid, label.LabelSlot, printed); } } diff --git a/Content.Server/Labels/Label/Components/PaperLabelComponent.cs b/Content.Server/Labels/Label/Components/PaperLabelComponent.cs index ed74b2547b..68231d1bd9 100644 --- a/Content.Server/Labels/Label/Components/PaperLabelComponent.cs +++ b/Content.Server/Labels/Label/Components/PaperLabelComponent.cs @@ -1,3 +1,4 @@ +using Content.Shared.Containers.ItemSlots; using Robust.Shared.GameObjects; using Robust.Shared.Serialization.Manager.Attributes; @@ -12,6 +13,6 @@ namespace Content.Server.Labels.Components public override string Name => "PaperLabel"; [DataField("labelSlot")] - public string LabelSlot = "labelSlot"; + public ItemSlot LabelSlot = new(); } } diff --git a/Content.Server/Labels/Label/LabelSystem.cs b/Content.Server/Labels/Label/LabelSystem.cs index 4fd4485cde..0cf535f0e4 100644 --- a/Content.Server/Labels/Label/LabelSystem.cs +++ b/Content.Server/Labels/Label/LabelSystem.cs @@ -4,11 +4,11 @@ using Content.Shared.Containers.ItemSlots; using Content.Shared.Examine; using Content.Shared.Labels; using JetBrains.Annotations; +using Robust.Shared.Containers; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Localization; using Robust.Shared.Utility; -using System; namespace Content.Server.Labels { @@ -18,26 +18,35 @@ namespace Content.Server.Labels [UsedImplicitly] public class LabelSystem : EntitySystem { - [Dependency] private readonly SharedItemSlotsSystem _itemSlotsSystem = default!; + [Dependency] private readonly ItemSlotsSystem _itemSlotsSystem = default!; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnExamine); - SubscribeLocalEvent(InitializePaperLabel); - SubscribeLocalEvent(OnItemSlotChanged); + SubscribeLocalEvent(OnComponentInit); + SubscribeLocalEvent(OnComponentRemove); + SubscribeLocalEvent(OnContainerModified); + SubscribeLocalEvent(OnContainerModified); SubscribeLocalEvent(OnExamined); } - private void InitializePaperLabel(EntityUid uid, PaperLabelComponent component, ComponentInit args) + private void OnComponentInit(EntityUid uid, PaperLabelComponent component, ComponentInit args) { + _itemSlotsSystem.AddItemSlot(uid, component.Name, component.LabelSlot); + if (!EntityManager.TryGetComponent(uid, out SharedAppearanceComponent appearance)) return; appearance.SetData(PaperLabelVisuals.HasLabel, false); } + private void OnComponentRemove(EntityUid uid, PaperLabelComponent component, ComponentRemove args) + { + _itemSlotsSystem.RemoveItemSlot(uid, component.LabelSlot); + } + private void OnExamine(EntityUid uid, LabelComponent? label, ExaminedEvent args) { if (!Resolve(uid, ref label)) @@ -53,12 +62,7 @@ namespace Content.Server.Labels private void OnExamined(EntityUid uid, PaperLabelComponent comp, ExaminedEvent args) { - if (!EntityManager.TryGetComponent(uid, out SharedItemSlotsComponent slots)) - return; - - var label = _itemSlotsSystem.PeekItemInSlot(slots, comp.LabelSlot); - - if (label == null) + if (comp.LabelSlot.Item == null) return; if (!args.IsInDetailsRange) @@ -67,8 +71,8 @@ namespace Content.Server.Labels return; } - if (!EntityManager.TryGetComponent(label.Uid, out PaperComponent paper)) - // should never happen + if (!EntityManager.TryGetComponent(comp.LabelSlot.Item.Uid, out PaperComponent paper)) + // Assuming yaml has the correct entity whitelist, this should not happen. return; if (string.IsNullOrWhiteSpace(paper.Content)) @@ -83,15 +87,15 @@ namespace Content.Server.Labels } - private void OnItemSlotChanged(EntityUid uid, PaperLabelComponent component, ItemSlotChangedEvent args) + private void OnContainerModified(EntityUid uid, PaperLabelComponent label, ContainerModifiedMessage args) { - if (args.SlotName != component.LabelSlot) + if (args.Container.ID != label.LabelSlot.ID) return; if (!EntityManager.TryGetComponent(uid, out SharedAppearanceComponent appearance)) return; - appearance.SetData(PaperLabelVisuals.HasLabel, args.ContainedItem != null); + appearance.SetData(PaperLabelVisuals.HasLabel, label.LabelSlot.HasItem); } } } diff --git a/Content.Server/Nuke/NukeComponent.cs b/Content.Server/Nuke/NukeComponent.cs index abb9747d59..a6f7c107f0 100644 --- a/Content.Server/Nuke/NukeComponent.cs +++ b/Content.Server/Nuke/NukeComponent.cs @@ -28,11 +28,12 @@ namespace Content.Server.Nuke public int Timer = 180; /// - /// Slot name for to store nuclear disk inside bomb. - /// See for mor info. + /// The that stores the nuclear disk. The entity whitelist, sounds, and some other + /// behaviours are specified by this definition. Make sure the whitelist, is correct + /// otherwise a blank bit of paper will work as a "disk". /// - [DataField("slot")] - public string DiskSlotName = "DiskSlot"; + [DataField("diskSlot")] + public ItemSlot DiskSlot = new(); /// /// Annihilation radius in which all human players will be gibed @@ -71,13 +72,6 @@ namespace Content.Server.Nuke [ViewVariables] public float RemainingTime; - /// - /// Does bomb contains valid entity inside ? - /// If it is, user can anchor bomb or enter nuclear code to arm it. - /// - [ViewVariables] - public bool DiskInserted = false; - /// /// Curent nuclear code buffer. Entered manually by players. /// If valid it will allow arm/disarm bomb. diff --git a/Content.Server/Nuke/NukeSystem.cs b/Content.Server/Nuke/NukeSystem.cs index b78bbc7478..58e6571ce8 100644 --- a/Content.Server/Nuke/NukeSystem.cs +++ b/Content.Server/Nuke/NukeSystem.cs @@ -19,6 +19,7 @@ using Content.Server.Coordinates.Helpers; using Content.Shared.Audio; using Content.Shared.Sound; using Robust.Shared.Audio; +using Robust.Shared.Containers; namespace Content.Server.Nuke { @@ -26,7 +27,7 @@ namespace Content.Server.Nuke { [Dependency] private readonly NukeCodeSystem _codes = default!; [Dependency] private readonly ActionBlockerSystem _actionBlocker = default!; - [Dependency] private readonly SharedItemSlotsSystem _itemSlots = default!; + [Dependency] private readonly ItemSlotsSystem _itemSlots = default!; [Dependency] private readonly PopupSystem _popups = default!; [Dependency] private readonly IEntityLookup _lookup = default!; [Dependency] private readonly IChatManager _chat = default!; @@ -39,7 +40,8 @@ namespace Content.Server.Nuke SubscribeLocalEvent(OnInit); SubscribeLocalEvent(OnRemove); SubscribeLocalEvent(OnActivate); - SubscribeLocalEvent(OnItemSlotChanged); + SubscribeLocalEvent(OnItemSlotChanged); + SubscribeLocalEvent(OnItemSlotChanged); // anchoring logic SubscribeLocalEvent(OnAnchorAttempt); @@ -59,6 +61,7 @@ namespace Content.Server.Nuke private void OnInit(EntityUid uid, NukeComponent component, ComponentInit args) { component.RemainingTime = component.Timer; + _itemSlots.AddItemSlot(uid, component.Name, component.DiskSlot); } public override void Update(float frameTime) @@ -93,14 +96,14 @@ namespace Content.Server.Nuke private void OnRemove(EntityUid uid, NukeComponent component, ComponentRemove args) { _tickingBombs.Remove(uid); + _itemSlots.RemoveItemSlot(uid, component.DiskSlot); } - private void OnItemSlotChanged(EntityUid uid, NukeComponent component, ItemSlotChangedEvent args) + private void OnItemSlotChanged(EntityUid uid, NukeComponent component, ContainerModifiedMessage args) { - if (args.SlotName != component.DiskSlotName) + if (args.Container.ID != component.DiskSlot.ID) return; - component.DiskInserted = args.ContainedItem != null; UpdateStatus(uid, component); UpdateUserInterface(uid, component); } @@ -137,7 +140,7 @@ namespace Content.Server.Nuke private void CheckAnchorAttempt(EntityUid uid, NukeComponent component, BaseAnchoredAttemptEvent args) { // cancel any anchor attempt without nuke disk - if (!component.DiskInserted) + if (!component.DiskSlot.HasItem) { var msg = Loc.GetString("nuke-component-cant-anchor"); _popups.PopupEntity(msg, uid, Filter.Entities(args.User)); @@ -160,15 +163,15 @@ namespace Content.Server.Nuke #region UI Events private void OnEjectButtonPressed(EntityUid uid, NukeComponent component, NukeEjectMessage args) { - if (!component.DiskInserted) + if (!component.DiskSlot.HasItem) return; - _itemSlots.TryEjectContent(uid, component.DiskSlotName, args.Session.AttachedEntity); + _itemSlots.TryEjectToHands(uid, component.DiskSlot, args.Session.AttachedEntityUid); } private async void OnAnchorButtonPressed(EntityUid uid, NukeComponent component, NukeAnchorMessage args) { - if (!component.DiskInserted) + if (!component.DiskSlot.HasItem) return; if (!EntityManager.TryGetComponent(uid, out TransformComponent? transform)) @@ -218,7 +221,7 @@ namespace Content.Server.Nuke private void OnArmButtonPressed(EntityUid uid, NukeComponent component, NukeArmedMessage args) { - if (!component.DiskInserted) + if (!component.DiskSlot.HasItem) return; if (component.Status == NukeStatus.AWAIT_ARM) @@ -240,12 +243,12 @@ namespace Content.Server.Nuke switch (component.Status) { case NukeStatus.AWAIT_DISK: - if (component.DiskInserted) + if (component.DiskSlot.HasItem) component.Status = NukeStatus.AWAIT_CODE; break; case NukeStatus.AWAIT_CODE: { - if (!component.DiskInserted) + if (!component.DiskSlot.HasItem) { component.Status = NukeStatus.AWAIT_DISK; component.EnteredCode = ""; @@ -299,7 +302,7 @@ namespace Content.Server.Nuke if (EntityManager.TryGetComponent(uid, out TransformComponent transform)) anchored = transform.Anchored; - var allowArm = component.DiskInserted && + var allowArm = component.DiskSlot.HasItem && (component.Status == NukeStatus.AWAIT_ARM || component.Status == NukeStatus.ARMED); @@ -307,7 +310,7 @@ namespace Content.Server.Nuke { Status = component.Status, RemainingTime = (int) component.RemainingTime, - DiskInserted = component.DiskInserted, + DiskInserted = component.DiskSlot.HasItem, IsAnchored = anchored, AllowArm = allowArm, EnteredCodeLength = component.EnteredCode.Length, diff --git a/Content.Server/PDA/PDAComponent.cs b/Content.Server/PDA/PDAComponent.cs index 414d87b7d9..f20524b80c 100644 --- a/Content.Server/PDA/PDAComponent.cs +++ b/Content.Server/PDA/PDAComponent.cs @@ -1,8 +1,11 @@ using System; using System.Collections.Generic; using Content.Server.Access.Components; +using Content.Shared.Containers.ItemSlots; using Robust.Shared.GameObjects; +using Robust.Shared.Prototypes; using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; using Robust.Shared.ViewVariables; namespace Content.Server.PDA @@ -12,16 +15,19 @@ namespace Content.Server.PDA { public override string Name => "PDA"; - [DataField("idSlot")] - public string IdSlot = "pdaIdSlot"; + [DataField("idSlot")] + public ItemSlot IdSlot = new(); [DataField("penSlot")] - public string PenSlot = "pdaPenSlot"; + public ItemSlot PenSlot = new(); - [ViewVariables] [DataField("idCard")] public string? StartingIdCard; + // Really this should just be using ItemSlot.StartingItem. However, seeing as we have so many different starting + // PDA's and no nice way to inherit the other fields from the ItemSlot data definition, this makes the yaml much + // nicer to read. + [DataField("idCard", customTypeSerializer: typeof(PrototypeIdSerializer))] + public string? IdCard; [ViewVariables] public IdCardComponent? ContainedID; - [ViewVariables] public bool PenInserted; [ViewVariables] public bool FlashlightOn; [ViewVariables] public string? OwnerName; diff --git a/Content.Server/PDA/PDASystem.cs b/Content.Server/PDA/PDASystem.cs index fd0dd4a8df..391d2ce00b 100644 --- a/Content.Server/PDA/PDASystem.cs +++ b/Content.Server/PDA/PDASystem.cs @@ -9,6 +9,7 @@ using Content.Shared.Containers.ItemSlots; using Content.Shared.Interaction; using Content.Shared.PDA; using Robust.Server.GameObjects; +using Robust.Shared.Containers; using Robust.Shared.GameObjects; using Robust.Shared.IoC; @@ -16,7 +17,7 @@ namespace Content.Server.PDA { public class PDASystem : EntitySystem { - [Dependency] private readonly SharedItemSlotsSystem _slotsSystem = default!; + [Dependency] private readonly ItemSlotsSystem _itemSlotsSystem = default!; [Dependency] private readonly UplinkSystem _uplinkSystem = default!; [Dependency] private readonly UnpoweredFlashlightSystem _unpoweredFlashlight = default!; @@ -25,10 +26,12 @@ namespace Content.Server.PDA base.Initialize(); SubscribeLocalEvent(OnComponentInit); - SubscribeLocalEvent(OnMapInit); + SubscribeLocalEvent(OnComponentRemove); + SubscribeLocalEvent(OnActivateInWorld); SubscribeLocalEvent(OnUse); - SubscribeLocalEvent(OnItemSlotChanged); + SubscribeLocalEvent(OnItemInserted); + SubscribeLocalEvent(OnItemRemoved); SubscribeLocalEvent(OnLightToggle); SubscribeLocalEvent(OnUplinkInit); @@ -41,19 +44,16 @@ namespace Content.Server.PDA if (ui != null) ui.OnReceiveMessage += (msg) => OnUIMessage(pda, msg); - UpdatePDAAppearance(pda); + if (pda.IdCard != null) + pda.IdSlot.StartingItem = pda.IdCard; + _itemSlotsSystem.AddItemSlot(uid, $"{pda.Name}-id", pda.IdSlot); + _itemSlotsSystem.AddItemSlot(uid, $"{pda.Name}-pen", pda.PenSlot); } - private void OnMapInit(EntityUid uid, PDAComponent pda, MapInitEvent args) + private void OnComponentRemove(EntityUid uid, PDAComponent pda, ComponentRemove args) { - // try to place ID inside item slot - if (!string.IsNullOrEmpty(pda.StartingIdCard)) - { - // if pda prototype doesn't have slots, ID will drop down on ground - var idCard = EntityManager.SpawnEntity(pda.StartingIdCard, pda.Owner.Transform.Coordinates); - if (EntityManager.TryGetComponent(uid, out SharedItemSlotsComponent? itemSlots)) - _slotsSystem.TryInsertContent(itemSlots, idCard, pda.IdSlot); - } + _itemSlotsSystem.RemoveItemSlot(uid, pda.IdSlot); + _itemSlotsSystem.RemoveItemSlot(uid, pda.PenSlot); } private void OnUse(EntityUid uid, PDAComponent pda, UseInHandEvent args) @@ -70,22 +70,19 @@ namespace Content.Server.PDA args.Handled = OpenUI(pda, args.User); } - private void OnItemSlotChanged(EntityUid uid, PDAComponent pda, ItemSlotChangedEvent args) + private void OnItemInserted(EntityUid uid, PDAComponent pda, EntInsertedIntoContainerMessage args) { - // check if ID slot changed - if (args.SlotName == pda.IdSlot) - { - var item = args.ContainedItem; - if (item == null || !EntityManager.TryGetComponent(item.Value, out IdCardComponent ? idCard)) - pda.ContainedID = null; - else - pda.ContainedID = idCard; - } - else if (args.SlotName == pda.PenSlot) - { - var item = args.ContainedItem; - pda.PenInserted = item != null; - } + if (args.Container.ID == pda.IdSlot.ID) + pda.ContainedID = args.Entity.GetComponentOrNull(); + + UpdatePDAAppearance(pda); + UpdatePDAUserInterface(pda); + } + + private void OnItemRemoved(EntityUid uid, PDAComponent pda, EntRemovedFromContainerMessage args) + { + if (args.Container.ID == pda.IdSlot.ID) + pda.ContainedID = null; UpdatePDAAppearance(pda); UpdatePDAUserInterface(pda); @@ -142,11 +139,15 @@ namespace Content.Server.PDA var hasUplink = pda.Owner.HasComponent(); var ui = pda.Owner.GetUIOrNull(PDAUiKey.Key); - ui?.SetState(new PDAUpdateState(pda.FlashlightOn, pda.PenInserted, ownerInfo, hasUplink)); + ui?.SetState(new PDAUpdateState(pda.FlashlightOn, pda.PenSlot.HasItem, ownerInfo, hasUplink)); } private void OnUIMessage(PDAComponent pda, ServerBoundUserInterfaceMessage msg) { + // cast EntityUid? to EntityUid + if (msg.Session.AttachedEntityUid is not EntityUid playerUid) + return; + switch (msg.Message) { case PDARequestUpdateInterfaceMessage _: @@ -161,12 +162,12 @@ namespace Content.Server.PDA case PDAEjectIDMessage _: { - _slotsSystem.TryEjectContent(pda.Owner.Uid, pda.IdSlot, msg.Session.AttachedEntity); + _itemSlotsSystem.TryEjectToHands(pda.Owner.Uid, pda.IdSlot, playerUid); break; } case PDAEjectPenMessage _: { - _slotsSystem.TryEjectContent(pda.Owner.Uid, pda.PenSlot, msg.Session.AttachedEntity); + _itemSlotsSystem.TryEjectToHands(pda.Owner.Uid, pda.PenSlot, playerUid); break; } case PDAShowUplinkMessage _: diff --git a/Content.Server/Sandbox/SandboxManager.cs b/Content.Server/Sandbox/SandboxManager.cs index b1a6453632..a85ad94cb0 100644 --- a/Content.Server/Sandbox/SandboxManager.cs +++ b/Content.Server/Sandbox/SandboxManager.cs @@ -136,10 +136,10 @@ namespace Content.Server.Sandbox if (pda.ContainedID == null) { var newID = CreateFreshId(); - if (pda.Owner.TryGetComponent(out SharedItemSlotsComponent? itemSlots)) + if (pda.Owner.TryGetComponent(out ItemSlotsComponent? itemSlots)) { - _entityManager.EntitySysManager.GetEntitySystem(). - TryInsertContent(itemSlots, newID, pda.IdSlot); + _entityManager.EntitySysManager.GetEntitySystem(). + TryInsert(wornItem.Owner.Uid, pda.IdSlot, newID); } } else diff --git a/Content.Shared/Containers/ItemSlot/ItemSlotEvents.cs b/Content.Shared/Containers/ItemSlot/ItemSlotEvents.cs deleted file mode 100644 index e2c7ffb54f..0000000000 --- a/Content.Shared/Containers/ItemSlot/ItemSlotEvents.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Robust.Shared.GameObjects; - -namespace Content.Shared.Containers.ItemSlots -{ - /// - /// Item was placed in or removed from one of the slots in - /// - public class ItemSlotChangedEvent : EntityEventArgs - { - public SharedItemSlotsComponent SlotsComponent; - public string SlotName; - public ItemSlot Slot; - public readonly EntityUid? ContainedItem; - - public ItemSlotChangedEvent(SharedItemSlotsComponent slotsComponent, string slotName, ItemSlot slot) - { - SlotsComponent = slotsComponent; - SlotName = slotName; - Slot = slot; - ContainedItem = slot.ContainerSlot.ContainedEntity?.Uid; - } - } -} diff --git a/Content.Shared/Containers/ItemSlot/ItemSlotsComponent.cs b/Content.Shared/Containers/ItemSlot/ItemSlotsComponent.cs new file mode 100644 index 0000000000..0c450f8a6f --- /dev/null +++ b/Content.Shared/Containers/ItemSlot/ItemSlotsComponent.cs @@ -0,0 +1,124 @@ +using Content.Shared.Sound; +using Content.Shared.Whitelist; +using Robust.Shared.Analyzers; +using Robust.Shared.Containers; +using Robust.Shared.GameObjects; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; +using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; +using Robust.Shared.ViewVariables; +using System; +using System.Collections.Generic; + +namespace Content.Shared.Containers.ItemSlots +{ + /// + /// Used for entities that can hold items in different slots. Needed by ItemSlotSystem to support basic + /// insert/eject interactions. + /// + [RegisterComponent] + [Friend(typeof(ItemSlotsSystem))] + public class ItemSlotsComponent : Component + { + public override string Name => "ItemSlots"; + + [ViewVariables] + [DataField("slots")] + public Dictionary Slots = new(); + } + + [Serializable, NetSerializable] + public sealed class ItemSlotsComponentState : ComponentState + { + public readonly Dictionary SlotLocked; + + public ItemSlotsComponentState(Dictionary slots) + { + SlotLocked = new(slots.Count); + + foreach (var (key, slot) in slots) + { + SlotLocked[key] = slot.Locked; + } + } + } + + /// + /// This is effectively a wrapper for a ContainerSlot that adds content functionality like entity whitelists and + /// insert/eject sounds. + /// + [DataDefinition] + [Friend(typeof(ItemSlotsSystem))] + public class ItemSlot + { + [DataField("whitelist")] + public EntityWhitelist? Whitelist; + + [DataField("insertSound")] + public SoundSpecifier? InsertSound; + // maybe default to /Audio/Weapons/Guns/MagIn/batrifle_magin.ogg ?? + + [DataField("ejectSound")] + public SoundSpecifier? EjectSound; + // maybe default to /Audio/Machines/id_swipe.ogg? + + /// + /// The name of this item slot. This will be shown to the user in the verb menu. + /// + /// + /// This will be passed through Loc.GetString. If the name is an empty string, then verbs will use the name + /// of the currently held or currently inserted entity instead. + /// + [DataField("name")] + public string Name = string.Empty; + + [DataField("startingItem", customTypeSerializer: typeof(PrototypeIdSerializer))] + public string? StartingItem; + + /// + /// Whether or not an item can currently be ejected or inserted from this slot. + /// + /// + /// This doesn't have to mean the slot is somehow physically locked. In the case of the item cabinet, the + /// cabinet may simply be closed at the moment and needs to be opened first. + /// + [DataField("locked")] + [ViewVariables(VVAccess.ReadWrite)] + public bool Locked = false; + + /// + /// Whether the item slots system will attempt to eject this item to the user's hands when interacted with. + /// + /// + /// For most item slots, this is probably not the case (eject is usually an alt-click interaction). But + /// there are some exceptions. For example item cabinets and charging stations should probably eject their + /// contents when clicked on normally. + /// + [DataField("ejectOnInteract")] + public bool EjectOnInteract = false; + + /// + /// Override the insert verb text. Defaults to [insert category] -> [item-name]. If not null, the verb will + /// not be given a category. + /// + [DataField("insertVerbText")] + public string? InsertVerbText; + + /// + /// Override the insert verb text. Defaults to [eject category] -> [item-name]. If not null, the verb will + /// not be given a category. + /// + [DataField("ejectVerbText")] + public string? EjectVerbText; + + [ViewVariables] + public ContainerSlot ContainerSlot = default!; + + public string ID => ContainerSlot.ID; + + // Convenience properties + public bool HasItem => ContainerSlot.ContainedEntity != null; + public IEntity? Item => ContainerSlot.ContainedEntity; + } +} diff --git a/Content.Shared/Containers/ItemSlot/ItemSlotsSystem.cs b/Content.Shared/Containers/ItemSlot/ItemSlotsSystem.cs new file mode 100644 index 0000000000..9e2cabf784 --- /dev/null +++ b/Content.Shared/Containers/ItemSlot/ItemSlotsSystem.cs @@ -0,0 +1,455 @@ +using Content.Shared.ActionBlocker; +using Content.Shared.Hands.Components; +using Content.Shared.Interaction; +using Content.Shared.Verbs; +using Robust.Shared.Audio; +using Robust.Shared.Containers; +using Robust.Shared.GameObjects; +using Robust.Shared.GameStates; +using Robust.Shared.IoC; +using Robust.Shared.Localization; +using Robust.Shared.Player; +using Robust.Shared.Utility; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Content.Shared.Containers.ItemSlots +{ + /// + /// A class that handles interactions related to inserting/ejecting items into/from an item slot. + /// + public class ItemSlotsSystem : EntitySystem + { + [Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnStartup); + SubscribeLocalEvent(Oninitialize); + + SubscribeLocalEvent(OnInteractUsing); + SubscribeLocalEvent(OnInteractHand); + + SubscribeLocalEvent(AddEjectVerbs); + SubscribeLocalEvent(AddInteractionVerbsVerbs); + + SubscribeLocalEvent(GetItemSlotsState); + SubscribeLocalEvent(HandleItemSlotsState); + } + + #region ComponentManagement + /// + /// Spawn in starting items for any item slots that should have one. + /// + private void OnStartup(EntityUid uid, ItemSlotsComponent itemSlots, ComponentStartup args) + { + foreach (var slot in itemSlots.Slots.Values) + { + if (slot.HasItem || string.IsNullOrEmpty(slot.StartingItem)) + continue; + + var item = EntityManager.SpawnEntity(slot.StartingItem, itemSlots.Owner.Transform.Coordinates); + slot.ContainerSlot.Insert(item); + } + } + + /// + /// Ensure item slots have containers. + /// + private void Oninitialize(EntityUid uid, ItemSlotsComponent itemSlots, ComponentInit args) + { + foreach (var (id, slot) in itemSlots.Slots) + { + slot.ContainerSlot = ContainerHelpers.EnsureContainer(itemSlots.Owner, id); + } + } + + /// + /// Given a new item slot, store it in the and ensure the slot has an item + /// container. + /// + public void AddItemSlot(EntityUid uid, string id, ItemSlot slot) + { + var itemSlots = EntityManager.EnsureComponent(uid); + slot.ContainerSlot = ContainerHelpers.EnsureContainer(itemSlots.Owner, id); + DebugTools.Assert(!itemSlots.Slots.ContainsKey(id)); + itemSlots.Slots[id] = slot; + } + + /// + /// Remove an item slot. This should generally be called whenever a component that added a slot is being + /// removed. + /// + public void RemoveItemSlot(EntityUid uid, ItemSlot slot, ItemSlotsComponent? itemSlots = null) + { + slot.ContainerSlot.Shutdown(); + + // Don't log missing resolves. when an entity has all of its components removed, the ItemSlotsComponent may + // have been removed before some other component that added an item slot (and is now trying to remove it). + if (!Resolve(uid, ref itemSlots, logMissing: false)) + return; + + itemSlots.Slots.Remove(slot.ID); + + if (itemSlots.Slots.Count == 0) + EntityManager.RemoveComponent(uid, itemSlots); + } + #endregion + + #region Interactions + /// + /// Attempt to take an item from a slot, if any are set to EjectOnInteract. + /// + private void OnInteractHand(EntityUid uid, ItemSlotsComponent itemSlots, InteractHandEvent args) + { + if (args.Handled) + return; + + foreach (var slot in itemSlots.Slots.Values) + { + if (slot.Locked || !slot.EjectOnInteract || slot.Item == null) + continue; + + args.Handled = true; + TryEjectToHands(uid, slot, args.UserUid); + break; + } + } + + /// + /// Tries to insert a held item in any fitting item slot. If a valid slot already contains an item, it will + /// swap it out and place the old one in the user's hand. + /// + /// + /// This only handles the event if the user has an applicable entity that can be inserted. This allows for + /// other interactions to still happen (e.g., open UI, or toggle-open), despite the user holding an item. + /// Maybe this is undesirable. + /// + private void OnInteractUsing(EntityUid uid, ItemSlotsComponent itemSlots, InteractUsingEvent args) + { + if (args.Handled) + return; + + if (!EntityManager.TryGetComponent(args.UserUid, out SharedHandsComponent? hands)) + return; + + foreach (var slot in itemSlots.Slots.Values) + { + if (!CanInsert(args.UsedUid, slot, swap: true)) + continue; + + // Drop the held item onto the floor. Return if the user cannot drop. + if (!hands.Drop(args.Used)) + return; + + if (slot.Item != null) + hands.TryPutInAnyHand(slot.Item); + + Insert(uid, slot, args.Used); + args.Handled = true; + return; + } + } + #endregion + + #region Insert + /// + /// Insert an item into a slot. This does not perform checks, so make sure to also use or just use instead. + /// + private void Insert(EntityUid uid, ItemSlot slot, IEntity item) + { + slot.ContainerSlot.Insert(item); + // ContainerSlot automatically raises a directed EntInsertedIntoContainerMessage + + if (slot.InsertSound != null) + SoundSystem.Play(Filter.Pvs(uid), slot.InsertSound.GetSound(), uid); + } + + /// + /// Check whether a given item can be inserted into a slot. Unless otherwise specified, this will return + /// false if the slot is already filled. + /// + public bool CanInsert(EntityUid uid, ItemSlot slot, bool swap = false) + { + if (slot.Locked) + return false; + + if (!swap && slot.HasItem) + return false; + + if (slot.Whitelist != null && !slot.Whitelist.IsValid(uid)) + return false; + + // We should also check ContainerSlot.CanInsert, but that prevents swapping interactions. Given that + // ContainerSlot.CanInsert gets called when the item is actually inserted anyways, we can just get away with + // fudging CanInsert and not performing those checks. + return true; + } + + /// + /// Tries to insert item into a specific slot. + /// + /// False if failed to insert item + public bool TryInsert(EntityUid uid, string id, IEntity item, ItemSlotsComponent? itemSlots = null) + { + if (!Resolve(uid, ref itemSlots)) + return false; + + if (!itemSlots.Slots.TryGetValue(id, out var slot)) + return false; + + return TryInsert(uid, slot, item); + } + + /// + /// Tries to insert item into a specific slot. + /// + /// False if failed to insert item + public bool TryInsert(EntityUid uid, ItemSlot slot, IEntity item) + { + if (!CanInsert(item.Uid, slot)) + return false; + + Insert(uid, slot, item); + return true; + } + #endregion + + #region Eject + /// + /// Eject an item into a slot. This does not perform checks (e.g., is the slot locked?), so you should + /// probably just use instead. + /// + private void Eject(EntityUid uid, ItemSlot slot, IEntity item) + { + slot.ContainerSlot.Remove(item); + // ContainerSlot automatically raises a directed EntRemovedFromContainerMessage + + if (slot.EjectSound != null) + SoundSystem.Play(Filter.Pvs(uid), slot.EjectSound.GetSound(), uid); + } + + /// + /// Try to eject an item from a slot. + /// + /// False if item slot is locked or has no item inserted + public bool TryEject(EntityUid uid, ItemSlot slot, [NotNullWhen(true)] out IEntity? item) + { + item = null; + + if (slot.Locked || slot.Item == null) + return false; + + item = slot.Item; + Eject(uid, slot, item); + return true; + } + + /// + /// Try to eject item from a slot. + /// + /// False if the id is not valid, the item slot is locked, or it has no item inserted + public bool TryEject(EntityUid uid, string id, [NotNullWhen(true)] out IEntity? item, ItemSlotsComponent? itemSlots = null) + { + item = null; + + if (!Resolve(uid, ref itemSlots)) + return false; + + if (!itemSlots.Slots.TryGetValue(id, out var slot)) + return false; + + return TryEject(uid, slot, out item); + } + + /// + /// Try to eject item from a slot directly into a user's hands. If they have no hands, the item will still + /// be ejected onto the floor. + /// + /// + /// False if the id is not valid, the item slot is locked, or it has no item inserted. True otherwise, even + /// if the user has no hands. + /// + public bool TryEjectToHands(EntityUid uid, ItemSlot slot, EntityUid? user) + { + if (!TryEject(uid, slot, out var item)) + return false; + + if (user != null && EntityManager.TryGetComponent(user.Value, out SharedHandsComponent? hands)) + hands.TryPutInAnyHand(item); + + return true; + } + #endregion + + #region Verbs + private void AddEjectVerbs(EntityUid uid, ItemSlotsComponent itemSlots, GetAlternativeVerbsEvent args) + { + if (args.Hands == null || !args.CanAccess ||!args.CanInteract || + !_actionBlockerSystem.CanPickup(args.User.Uid)) + { + return; + } + + foreach (var slot in itemSlots.Slots.Values) + { + if (slot.Locked || !slot.HasItem) + continue; + + if (slot.EjectOnInteract) + // For this item slot, ejecting/inserting is a primary interaction. Instead of an eject category + // alt-click verb, there will be a "Take item" primary interaction verb. + continue; + + var verbSubject = slot.Name != string.Empty + ? Loc.GetString(slot.Name) + : slot.Item!.Name ?? string.Empty; + + Verb verb = new(); + verb.Act = () => TryEjectToHands(uid, slot, args.User.Uid); + + if (slot.EjectVerbText == null) + { + verb.Text = verbSubject; + verb.Category = VerbCategory.Eject; + } + else + { + verb.Text = Loc.GetString(slot.EjectVerbText); + verb.IconTexture = "/Textures/Interface/VerbIcons/eject.svg.192dpi.png"; + } + + args.Verbs.Add(verb); + } + } + + private void AddInteractionVerbsVerbs(EntityUid uid, ItemSlotsComponent itemSlots, GetInteractionVerbsEvent args) + { + if (args.Hands == null || !args.CanAccess || !args.CanInteract) + return; + + // If there are any slots that eject on left-click, add a "Take " verb. + if (_actionBlockerSystem.CanPickup(args.User.Uid)) + { + foreach (var slot in itemSlots.Slots.Values) + { + if (!slot.EjectOnInteract || slot.Locked || !slot.HasItem) + continue; + + var verbSubject = slot.Name != string.Empty + ? Loc.GetString(slot.Name) + : slot.Item!.Name ?? string.Empty; + + Verb takeVerb = new(); + takeVerb.Act = () => TryEjectToHands(uid, slot, args.User.Uid); + takeVerb.IconTexture = "/Textures/Interface/VerbIcons/pickup.svg.192dpi.png"; + + if (slot.EjectVerbText == null) + takeVerb.Text = Loc.GetString("take-item-verb-text", ("subject", verbSubject)); + else + takeVerb.Text = Loc.GetString(slot.EjectVerbText); + + args.Verbs.Add(takeVerb); + } + } + + // Next, add the insert-item verbs + if (args.Using == null || !_actionBlockerSystem.CanDrop(args.User.Uid)) + return; + + foreach (var slot in itemSlots.Slots.Values) + { + if (!CanInsert(args.Using.Uid, slot)) + continue; + + var verbSubject = slot.Name != string.Empty + ? Loc.GetString(slot.Name) + : args.Using.Name ?? string.Empty; + + Verb insertVerb = new(); + insertVerb.Act = () => Insert(uid, slot, args.Using); + + if (slot.InsertVerbText != null) + { + insertVerb.Text = Loc.GetString(slot.InsertVerbText); + insertVerb.IconTexture = "/Textures/Interface/VerbIcons/insert.svg.192dpi.png"; + } + else if(slot.EjectOnInteract) + { + // Inserting/ejecting is a primary interaction for this entity. Instead of using the insert + // category, we will use a single "Place " verb. + insertVerb.Text = Loc.GetString("place-item-verb-text", ("subject", verbSubject)); + insertVerb.IconTexture = "/Textures/Interface/VerbIcons/drop.svg.192dpi.png"; + } + else + { + insertVerb.Category = VerbCategory.Insert; + insertVerb.Text = verbSubject; + } + + args.Verbs.Add(insertVerb); + } + } + #endregion + + /// + /// Get the contents of some item slot. + /// + public IEntity? GetItem(EntityUid uid, string id, ItemSlotsComponent? itemSlots = null) + { + if (!Resolve(uid, ref itemSlots)) + return null; + + return itemSlots.Slots.GetValueOrDefault(id)?.Item; + } + + /// + /// Lock an item slot. This stops items from being inserted into or ejected from this slot. + /// + public void SetLock(EntityUid uid, string id, bool locked, ItemSlotsComponent? itemSlots = null) + { + if (!Resolve(uid, ref itemSlots)) + return; + + if (!itemSlots.Slots.TryGetValue(id, out var slot)) + return; + + SetLock(itemSlots, slot, locked); + } + + /// + /// Lock an item slot. This stops items from being inserted into or ejected from this slot. + /// + public void SetLock(ItemSlotsComponent itemSlots, ItemSlot slot, bool locked) + { + slot.Locked = locked; + itemSlots.Dirty(); + } + + /// + /// Update the locked state of the managed item slots. + /// + /// + /// Note that the slot's ContainerSlot performs its own networking, so we don't need to send information + /// about the contained entity. + /// + private void HandleItemSlotsState(EntityUid uid, ItemSlotsComponent component, ref ComponentHandleState args) + { + if (args.Current is not ItemSlotsComponentState state) + return; + + foreach (var (id, locked) in state.SlotLocked) + { + component.Slots[id].Locked = locked; + } + } + + private void GetItemSlotsState(EntityUid uid, ItemSlotsComponent component, ref ComponentGetState args) + { + args.State = new ItemSlotsComponentState(component.Slots); + } + } +} diff --git a/Content.Shared/Containers/ItemSlot/SharedItemSlotsComponent.cs b/Content.Shared/Containers/ItemSlot/SharedItemSlotsComponent.cs deleted file mode 100644 index 1a301fd672..0000000000 --- a/Content.Shared/Containers/ItemSlot/SharedItemSlotsComponent.cs +++ /dev/null @@ -1,54 +0,0 @@ -using Content.Shared.Sound; -using Content.Shared.Whitelist; -using Robust.Shared.Containers; -using Robust.Shared.GameObjects; -using Robust.Shared.Localization; -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.Manager.Attributes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; -using Robust.Shared.ViewVariables; -using System; -using System.Collections.Generic; - -namespace Content.Shared.Containers.ItemSlots -{ - /// - /// Used for entities that can hold items in different slots - /// Allows basic insert/eject interaction - /// - [RegisterComponent] - public class SharedItemSlotsComponent : Component - { - public override string Name => "ItemSlots"; - - [ViewVariables] [DataField("slots")] public Dictionary Slots = new(); - } - - [Serializable] - [DataDefinition] - public class ItemSlot - { - [ViewVariables] [DataField("whitelist")] public EntityWhitelist? Whitelist; - [ViewVariables] [DataField("insertSound")] public SoundSpecifier? InsertSound; - [ViewVariables] [DataField("ejectSound")] public SoundSpecifier? EjectSound; - - /// - /// The name of this item slot. This will be shown to the user in the verb menu. - /// - [ViewVariables] public string Name - { - get => _name != string.Empty - ? Loc.GetString(_name) - : ContainerSlot.ContainedEntity?.Name ?? string.Empty; - set => _name = value; - } - [DataField("name")] private string _name = string.Empty; - - [DataField("item", customTypeSerializer: typeof(PrototypeIdSerializer))] - [ViewVariables] public string? StartingItem; - - [ViewVariables] public ContainerSlot ContainerSlot = default!; - - public bool HasEntity => ContainerSlot.ContainedEntity != null; - } -} diff --git a/Content.Shared/Containers/ItemSlot/SharedItemSlotsSystem.cs b/Content.Shared/Containers/ItemSlot/SharedItemSlotsSystem.cs deleted file mode 100644 index 96c89d0f54..0000000000 --- a/Content.Shared/Containers/ItemSlot/SharedItemSlotsSystem.cs +++ /dev/null @@ -1,255 +0,0 @@ -using Content.Shared.ActionBlocker; -using Content.Shared.Hands.Components; -using Content.Shared.Interaction; -using Content.Shared.Popups; -using Content.Shared.Verbs; -using Robust.Shared.Audio; -using Robust.Shared.Containers; -using Robust.Shared.GameObjects; -using Robust.Shared.IoC; -using Robust.Shared.Localization; -using Robust.Shared.Player; - -namespace Content.Shared.Containers.ItemSlots -{ - public class SharedItemSlotsSystem : EntitySystem - { - [Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!; - - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnComponentInit); - SubscribeLocalEvent(OnMapInit); - SubscribeLocalEvent(OnInteractUsing); - - SubscribeLocalEvent(AddEjectVerbs); - SubscribeLocalEvent(AddInsertVerbs); - } - - private void OnComponentInit(EntityUid uid, SharedItemSlotsComponent itemSlots, ComponentInit args) - { - // create container for each slot - foreach (var pair in itemSlots.Slots) - { - var slotName = pair.Key; - var slot = pair.Value; - - slot.ContainerSlot = ContainerHelpers.EnsureContainer(itemSlots.Owner, slotName); - } - } - - private void OnMapInit(EntityUid uid, SharedItemSlotsComponent itemSlots, MapInitEvent args) - { - foreach (var pair in itemSlots.Slots) - { - var slot = pair.Value; - var slotName = pair.Key; - - // Check if someone already put item inside container - if (slot.ContainerSlot.ContainedEntity != null) - continue; - - // Try to spawn item inside each slot - if (!string.IsNullOrEmpty(slot.StartingItem)) - { - var item = EntityManager.SpawnEntity(slot.StartingItem, itemSlots.Owner.Transform.Coordinates); - slot.ContainerSlot.Insert(item); - - RaiseLocalEvent(uid, new ItemSlotChangedEvent(itemSlots, slotName, slot)); - } - } - } - - private void AddEjectVerbs(EntityUid uid, SharedItemSlotsComponent component, GetAlternativeVerbsEvent args) - { - if (args.Hands == null || - !args.CanAccess || - !args.CanInteract || - !_actionBlockerSystem.CanPickup(args.User.Uid)) - return; - - foreach (var (slotName, slot) in component.Slots) - { - if (slot.ContainerSlot.ContainedEntity == null) - continue; - - Verb verb = new(); - verb.Text = slot.Name; - verb.Category = VerbCategory.Eject; - verb.Act = () => TryEjectContent(uid, slotName, args.User, component); - - args.Verbs.Add(verb); - } - } - - private void AddInsertVerbs(EntityUid uid, SharedItemSlotsComponent component, GetInteractionVerbsEvent args) - { - if (args.Using == null || - !args.CanAccess || - !args.CanInteract || - !_actionBlockerSystem.CanDrop(args.User.Uid)) - return; - - foreach (var (slotName, slot) in component.Slots) - { - if (!CanInsertContent(args.Using, slot)) - continue; - - Verb verb = new(); - verb.Text = slot.Name != string.Empty ? slot.Name : args.Using.Name; - verb.Category = VerbCategory.Insert; - verb.Act = () => InsertContent(component, slot, slotName, args.Using); - args.Verbs.Add(verb); - } - } - - private void OnInteractUsing(EntityUid uid, SharedItemSlotsComponent itemSlots, InteractUsingEvent args) - { - if (args.Handled) - return; - - args.Handled = TryInsertContent(uid, args.Used, args.User, itemSlots); - } - - /// - /// Tries to insert or swap an item in any fitting item slot from users hand. If a valid slot already contains an item, it will swap it out. - /// - /// False if failed to insert item - public bool TryInsertContent(EntityUid uid, IEntity item, IEntity user, SharedItemSlotsComponent? itemSlots = null, SharedHandsComponent? hands = null) - { - if (!Resolve(uid, ref itemSlots)) - return false; - - if (!Resolve(user.Uid, ref hands)) - { - itemSlots.Owner.PopupMessage(user, Loc.GetString("item-slots-try-insert-no-hands")); - return false; - } - - foreach (var (slotName, slot) in itemSlots.Slots) - { - // check if item allowed in whitelist - if (slot.Whitelist != null && !slot.Whitelist.IsValid(item.Uid)) - continue; - - // check if slot does not contain the item currently being inserted??? - if (slot.ContainerSlot.Contains(item)) - continue; - - // get item inside container - IEntity? swap = null; - if (slot.ContainerSlot.ContainedEntity != null) - swap = slot.ContainerSlot.ContainedEntity; - - // return if user can't drop active item in hand - if (!hands.Drop(item)) - return true; - - // swap item in hand and item in slot - if (swap != null) - hands.TryPutInAnyHand(swap); - - InsertContent(itemSlots, slot, slotName, item); - - return true; - } - - return false; - } - - public void InsertContent(SharedItemSlotsComponent itemSlots, ItemSlot slot, string slotName, IEntity item) - { - // insert item - slot.ContainerSlot.Insert(item); - RaiseLocalEvent(itemSlots.OwnerUid, new ItemSlotChangedEvent(itemSlots, slotName, slot)); - - // play sound - if (slot.InsertSound != null) - SoundSystem.Play(Filter.Pvs(itemSlots.Owner), slot.InsertSound.GetSound(), itemSlots.Owner); - } - - /// - /// Can a given item be inserted into a slot, without ejecting the current item in that slot. - /// - public bool CanInsertContent(IEntity item, ItemSlot slot) - { - if (slot.ContainerSlot.ContainedEntity != null) - return false; - - // check if item allowed in whitelist - if (slot.Whitelist != null && !slot.Whitelist.IsValid(item.Uid)) - return false; - - return true; - } - - /// - /// Tries to insert item in known slot. Doesn't interact with user - /// - /// False if failed to insert item - public bool TryInsertContent(SharedItemSlotsComponent itemSlots, IEntity item, string slotName) - { - if (!itemSlots.Slots.TryGetValue(slotName, out var slot)) - return false; - - if (!CanInsertContent(item, slot)) - return false; - - InsertContent(itemSlots, slot, slotName, item); - return true; - } - - /// - /// Check if slot has some content in it (without ejecting item) - /// - /// Null if doesn't have any content - public IEntity? PeekItemInSlot(SharedItemSlotsComponent itemSlots, string slotName) - { - if (!itemSlots.Slots.TryGetValue(slotName, out var slot)) - return null; - - var item = slot.ContainerSlot.ContainedEntity; - return item; - } - - /// - /// Try to eject item from slot to users hands - /// - public bool TryEjectContent(EntityUid uid, string slotName, IEntity? user, SharedItemSlotsComponent? itemSlots = null) - { - if (!Resolve(uid, ref itemSlots)) - return false; - - if (!itemSlots.Slots.TryGetValue(slotName, out var slot)) - return false; - - if (slot.ContainerSlot.ContainedEntity == null) - return false; - - var item = slot.ContainerSlot.ContainedEntity; - if (!slot.ContainerSlot.Remove(item)) - return false; - - // try eject item to users hand - if (user != null) - { - if (user.TryGetComponent(out SharedHandsComponent? hands)) - { - hands.TryPutInAnyHand(item); - } - else - { - itemSlots.Owner.PopupMessage(user, Loc.GetString("item-slots-try-insert-no-hands")); - } - } - - if (slot.EjectSound != null) - SoundSystem.Play(Filter.Pvs(itemSlots.Owner), slot.EjectSound.GetSound(), itemSlots.Owner); - - RaiseLocalEvent(itemSlots.OwnerUid, new ItemSlotChangedEvent(itemSlots, slotName, slot)); - return true; - } - } -} diff --git a/Resources/Locale/en-US/containers/item-slots-component.ftl b/Resources/Locale/en-US/containers/item-slots-component.ftl index 77aca397d3..bf95cbab68 100644 --- a/Resources/Locale/en-US/containers/item-slots-component.ftl +++ b/Resources/Locale/en-US/containers/item-slots-component.ftl @@ -1,4 +1,2 @@ -item-slots-try-insert-no-hands = You have no hands. - -# EjectItemVerb -eject-item-verb-text-default = Eject {$item} +take-item-verb-text = Take {$subject} +place-item-verb-text = Place {$subject} diff --git a/Resources/Prototypes/Entities/Objects/Devices/nuke.yml b/Resources/Prototypes/Entities/Objects/Devices/nuke.yml index 850fb47532..991d6673a6 100644 --- a/Resources/Prototypes/Entities/Objects/Devices/nuke.yml +++ b/Resources/Prototypes/Entities/Objects/Devices/nuke.yml @@ -22,18 +22,16 @@ mask: - VaultImpassable - type: Nuke + diskSlot: + name: Disk + insertSound: + path: /Audio/Machines/terminal_insert_disc.ogg + ejectSound: + path: /Audio/Machines/terminal_insert_disc.ogg + whitelist: + tags: + - NukeDisk - type: InteractionOutline - - type: ItemSlots - slots: - DiskSlot: - name: Disk - insertSound: - path: /Audio/Machines/terminal_insert_disc.ogg - ejectSound: - path: /Audio/Machines/terminal_insert_disc.ogg - whitelist: - tags: - - NukeDisk - type: UserInterface interfaces: - key: enum.NukeUiKey.Key diff --git a/Resources/Prototypes/Entities/Objects/Devices/pda.yml b/Resources/Prototypes/Entities/Objects/Devices/pda.yml index ea3edc0ac6..912355e86c 100644 --- a/Resources/Prototypes/Entities/Objects/Devices/pda.yml +++ b/Resources/Prototypes/Entities/Objects/Devices/pda.yml @@ -34,23 +34,19 @@ type: PDABoundUserInterface - key: enum.UplinkUiKey.Key type: UplinkBoundUserInterface - - type: ItemSlots - slots: - pdaPenSlot: - item: "Pen" - whitelist: - tags: - - Write - pdaIdSlot: - name: ID Card - insertSound: - path: /Audio/Weapons/Guns/MagIn/batrifle_magin.ogg - ejectSound: - path: /Audio/Machines/id_swipe.ogg - whitelist: - components: - - IdCard - type: PDA + penSlot: + startingItem: Pen + whitelist: + tags: + - Write + idSlot: + name: ID Card + ejectSound: /Audio/Machines/id_swipe.ogg + insertSound: /Audio/Weapons/Guns/MagIn/batrifle_magin.ogg + whitelist: + components: + - IdCard - type: DoorBumpOpener - type: entity @@ -100,6 +96,15 @@ components: - type: PDA idCard: ClownIDCard + penSlot: + startingItem: CrayonOrange # no pink crayon?!? + # Maybe this is a bad idea. + # At least they can't just spam alt-click it. + # You need to remove the ID & alternate between inserting and ejecting + ejectSound: /Audio/Items/bikehorn.ogg + whitelist: + tags: + - Write - type: Appearance visuals: - type: PDAVisualizer @@ -135,6 +140,11 @@ components: - type: PDA idCard: MimeIDCard + idSlot: + name: ID Card + whitelist: + components: + - IdCard - type: Appearance visuals: - type: PDAVisualizer diff --git a/Resources/Prototypes/Entities/Objects/Fun/crayons.yml b/Resources/Prototypes/Entities/Objects/Fun/crayons.yml index 322157c4f2..b690b2e45f 100644 --- a/Resources/Prototypes/Entities/Objects/Fun/crayons.yml +++ b/Resources/Prototypes/Entities/Objects/Fun/crayons.yml @@ -33,6 +33,8 @@ capacity: 5 - type: Tag tags: + - Write + - Crayon - CrayonWhite - type: entity @@ -52,6 +54,8 @@ capacity: 5 - type: Tag tags: + - Write + - Crayon - CrayonWhite - type: entity @@ -71,6 +75,8 @@ capacity: 5 - type: Tag tags: + - Write + - Crayon - CrayonBlack - type: entity @@ -90,6 +96,8 @@ capacity: 5 - type: Tag tags: + - Write + - Crayon - CrayonRed - type: entity @@ -109,6 +117,8 @@ capacity: 5 - type: Tag tags: + - Write + - Crayon - CrayonOrange - type: entity @@ -128,6 +138,8 @@ capacity: 5 - type: Tag tags: + - Write + - Crayon - CrayonYellow - type: entity @@ -147,6 +159,8 @@ capacity: 5 - type: Tag tags: + - Write + - Crayon - CrayonGreen - type: entity @@ -166,6 +180,8 @@ capacity: 5 - type: Tag tags: + - Write + - Crayon - CrayonBlue - type: entity @@ -185,6 +201,8 @@ capacity: 5 - type: Tag tags: + - Write + - Crayon - CrayonPurple - type: entity diff --git a/Resources/Prototypes/Entities/Objects/Specific/Medical/morgue.yml b/Resources/Prototypes/Entities/Objects/Specific/Medical/morgue.yml index aaba5379cc..eed9ac0607 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Medical/morgue.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Medical/morgue.yml @@ -34,12 +34,12 @@ openSound: path: /Audio/Misc/zip.ogg - type: PaperLabel - - type: ItemSlots - slots: - labelSlot: - whitelist: - components: - - Paper + labelSlot: + insertVerbText: Attach Label + ejectVerbText: Remove Label + whitelist: + components: + - Paper - type: Appearance visuals: - type: StorageVisualizer diff --git a/Resources/Prototypes/Entities/Structures/Storage/Crates/base.yml b/Resources/Prototypes/Entities/Structures/Storage/Crates/base.yml index a7196f62fd..ae547fad37 100644 --- a/Resources/Prototypes/Entities/Structures/Storage/Crates/base.yml +++ b/Resources/Prototypes/Entities/Structures/Storage/Crates/base.yml @@ -54,9 +54,9 @@ state_open: crate_open state_closed: crate_door - type: PaperLabel - - type: ItemSlots - slots: - labelSlot: - whitelist: - components: - - Paper + labelSlot: + insertVerbText: Attach Label + ejectVerbText: Remove Label + whitelist: + components: + - Paper diff --git a/Resources/Prototypes/Entities/Structures/Wallmounts/extinguisher_cabinet.yml b/Resources/Prototypes/Entities/Structures/Wallmounts/extinguisher_cabinet.yml index 2b94a4b154..72f8b9691f 100644 --- a/Resources/Prototypes/Entities/Structures/Wallmounts/extinguisher_cabinet.yml +++ b/Resources/Prototypes/Entities/Structures/Wallmounts/extinguisher_cabinet.yml @@ -18,6 +18,11 @@ - state: closed map: ["enum.ItemCabinetVisualLayers.Door"] - type: ItemCabinet + cabinetSlot: + ejectOnInteract: true + whitelist: + components: + - FireExtinguisher doorSound: path: /Audio/Machines/machine_switch.ogg - type: Appearance @@ -25,12 +30,6 @@ - type: ItemCabinetVisualizer openState: open closedState: closed - - type: ItemSlots - slots: - cabinetSlot: - whitelist: - components: - - FireExtinguisher placement: mode: SnapgridCenter @@ -48,14 +47,12 @@ suffix: Filled components: - type: ItemCabinet - spawnPrototype: FireExtinguisher - - type: ItemSlots - slots: - cabinetSlot: - item: FireExtinguisher - whitelist: - components: - - FireExtinguisher + cabinetSlot: + ejectOnInteract: true + startingItem: FireExtinguisher + whitelist: + components: + - FireExtinguisher - type: entity id: ExtinguisherCabinetFilledOpen diff --git a/Resources/Prototypes/Entities/Structures/Wallmounts/fireaxe_cabinet.yml b/Resources/Prototypes/Entities/Structures/Wallmounts/fireaxe_cabinet.yml index 19d67e21a5..e0fdfd84f5 100644 --- a/Resources/Prototypes/Entities/Structures/Wallmounts/fireaxe_cabinet.yml +++ b/Resources/Prototypes/Entities/Structures/Wallmounts/fireaxe_cabinet.yml @@ -16,6 +16,11 @@ - state: glass map: ["enum.ItemCabinetVisualLayers.Door"] - type: ItemCabinet + cabinetSlot: + ejectOnInteract: true + whitelist: + tags: + - FireAxe doorSound: path: /Audio/Machines/machine_switch.ogg - type: Appearance @@ -23,12 +28,6 @@ - type: ItemCabinetVisualizer closedState: glass openState: glass-up - - type: ItemSlots - slots: - cabinetSlot: - whitelist: - tags: - - FireAxe placement: mode: SnapgridCenter @@ -46,13 +45,12 @@ suffix: Filled components: - type: ItemCabinet - - type: ItemSlots - slots: - cabinetSlot: - item: FireAxe - whitelist: - tags: - - FireAxe + cabinetSlot: + startingItem: FireAxe + ejectOnInteract: true + whitelist: + tags: + - FireAxe - type: entity id: FireAxeCabinetFilledOpen diff --git a/Resources/Textures/Interface/VerbIcons/drop.svg.192dpi.png b/Resources/Textures/Interface/VerbIcons/drop.svg.192dpi.png new file mode 100644 index 0000000000000000000000000000000000000000..ff95db70f1c84571519eeed945d238d109131eae GIT binary patch literal 587 zcmV-R0<`^!P)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGxhX4Q_hXIe}@nrx202y>eSaefwW^{L9 za%BK_cXuvnZfkR6VQ^(GZ*pgw?mQX*00Fy6L_t(|UhSAcN&`UsLC`OQobHU{_=My3cy*H3 zVHh6e@j5_&9cC&!d73+Ca&}7ukh5DNfSlbD0p#qK2q0&-L;yLvB?8FVElC0JU)mk! zTrQh~yKzUkoJ8thz!hfSVg&4Tf#V&+9VavAC2);tDuT$rVJ;6}NMP=JKr<0?<(SOiOXVikZe!n0=tYk6WBfZSkQB6!FX>j1<& zf|oq;2tdptc*+y6033TZvF!UcrrPN2n*SV1Zf@>dPxwq;!OK zj=w{SFo^EgyqRASUfkva^)2W%#E;oc?BY= z5!NHGKxk@&^~oy`I~8HQ@(Kk1i?Duq1)_f=^dql8SR?c&uRxq1ua)^2D+q!h2!h{i ZHb2939b3^4sSp4F002ovPDHLkV1gX%?hyb0 literal 0 HcmV?d00001 diff --git a/Resources/Textures/Interface/VerbIcons/drop.svg.192dpi.png.yml b/Resources/Textures/Interface/VerbIcons/drop.svg.192dpi.png.yml new file mode 100644 index 0000000000..827ff9df88 --- /dev/null +++ b/Resources/Textures/Interface/VerbIcons/drop.svg.192dpi.png.yml @@ -0,0 +1,4 @@ +# For now, this icon is literally just the pickup icon rotated 180 degrees. +# But maybe this will change in the future? +sample: + filter: true