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
This commit is contained in:
Leon Friedrich
2021-11-20 18:26:01 +13:00
committed by GitHub
parent 19c5fed53a
commit 91b185e3c2
28 changed files with 805 additions and 554 deletions

View File

@@ -42,7 +42,6 @@ namespace Content.IntegrationTests.Tests
Slots:
- idcard
- type: PDA
idCard: AssistantIDCard
";
[Test]
public async Task SpawnItemInSlotTest()

View File

@@ -29,13 +29,12 @@ namespace Content.IntegrationTests.Tests.PDA
id: {PdaDummy}
name: {PdaDummy}
components:
- type: ItemSlots
slots:
pdaIdSlot:
- type: PDA
idSlot:
name: ID Card
whitelist:
components:
- IdCard
- type: PDA
- type: Item";
[Test]
@@ -77,9 +76,9 @@ namespace Content.IntegrationTests.Tests.PDA
var pdaComponent = dummyPda.GetComponent<PDAComponent>();
var pdaIdCard = sEntityManager.SpawnEntity(IdCardDummy, player.Transform.MapPosition);
var itemSlots = dummyPda.GetComponent<SharedItemSlotsComponent>();
sEntityManager.EntitySysManager.GetEntitySystem<SharedItemSlotsSystem>()
.TryInsertContent(itemSlots, pdaIdCard, pdaComponent.IdSlot);
var itemSlots = dummyPda.GetComponent<ItemSlotsComponent>();
sEntityManager.EntitySysManager.GetEntitySystem<ItemSlotsSystem>()
.TryInsert(dummyPda.Uid, pdaComponent.IdSlot, pdaIdCard);
var pdaContainedId = pdaComponent.ContainedID;
// The PDA in the hand should be found first

View File

@@ -98,7 +98,7 @@ namespace Content.IntegrationTests.Tests.Utility
// Test from serialized
var dummy = entityManager.SpawnEntity("WhitelistDummy", mapCoordinates);
var whitelistSer = dummy.GetComponent<SharedItemSlotsComponent>().Slots.Values.First().Whitelist;
var whitelistSer = dummy.GetComponent<ItemSlotsComponent>().Slots.Values.First().Whitelist;
Assert.That(whitelistSer, Is.Not.Null);
Assert.That(whitelistSer.Components, Is.Not.Null);

View File

@@ -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!;
/// <summary>
/// The slot name, used to get the actual item slot from the ItemSlotsComponent.
/// The <see cref="ItemSlot"/> that stores the actual item. The entity whitelist, sounds, and other
/// behaviours are specified by this <see cref="ItemSlot"/> definition.
/// </summary>
[DataField("cabinetSlot")]
public string CabinetSlot = "cabinetSlot";
public ItemSlot CabinetSlot = new();
/// <summary>
/// Whether the cabinet is currently open or not.

View File

@@ -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<ItemCabinetComponent, InteractUsingEvent>(OnInteractUsing);
SubscribeLocalEvent<ItemCabinetComponent, InteractHandEvent>(OnInteractHand);
SubscribeLocalEvent<ItemCabinetComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<ItemCabinetComponent, ComponentRemove>(OnComponentRemove);
SubscribeLocalEvent<ItemCabinetComponent, ComponentStartup>(OnComponentStartup);
SubscribeLocalEvent<ItemCabinetComponent, ActivateInWorldEvent>(OnActivateInWorld);
SubscribeLocalEvent<ItemCabinetComponent, ComponentStartup>(InitializeAppearance);
SubscribeLocalEvent<ItemCabinetComponent, ItemSlotChangedEvent>(OnItemSlotChanged);
SubscribeLocalEvent<ItemCabinetComponent, GetActivationVerbsEvent>(AddToggleOpenVerb);
SubscribeLocalEvent<ItemCabinetComponent, EntInsertedIntoContainerMessage>(OnContainerModified);
SubscribeLocalEvent<ItemCabinetComponent, EntRemovedFromContainerMessage>(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);
}
/// <summary>
/// Try insert an item if the cabinet is opened. Otherwise, just try open it.
/// </summary>
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;
}
/// <summary>
/// If the cabinet is opened and has an entity, try and take it. Otherwise toggle the cabinet open/closed;
/// </summary>
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);
}

View File

@@ -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<SharedItemSlotsSystem>().TryInsertContent(slots, printed, label.LabelSlot);
EntitySystem.Get<ItemSlotsSystem>().TryInsert(OwnerUid, label.LabelSlot, printed);
}
}

View File

@@ -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();
}
}

View File

@@ -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<LabelComponent, ExaminedEvent>(OnExamine);
SubscribeLocalEvent<PaperLabelComponent, ComponentInit>(InitializePaperLabel);
SubscribeLocalEvent<PaperLabelComponent, ItemSlotChangedEvent>(OnItemSlotChanged);
SubscribeLocalEvent<PaperLabelComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<PaperLabelComponent, ComponentRemove>(OnComponentRemove);
SubscribeLocalEvent<PaperLabelComponent, EntInsertedIntoContainerMessage>(OnContainerModified);
SubscribeLocalEvent<PaperLabelComponent, EntRemovedFromContainerMessage>(OnContainerModified);
SubscribeLocalEvent<PaperLabelComponent, ExaminedEvent>(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);
}
}
}

View File

@@ -28,11 +28,12 @@ namespace Content.Server.Nuke
public int Timer = 180;
/// <summary>
/// Slot name for to store nuclear disk inside bomb.
/// See <see cref="SharedItemSlotsComponent"/> for mor info.
/// The <see cref="ItemSlot"/> that stores the nuclear disk. The entity whitelist, sounds, and some other
/// behaviours are specified by this <see cref="ItemSlot"/> definition. Make sure the whitelist, is correct
/// otherwise a blank bit of paper will work as a "disk".
/// </summary>
[DataField("slot")]
public string DiskSlotName = "DiskSlot";
[DataField("diskSlot")]
public ItemSlot DiskSlot = new();
/// <summary>
/// Annihilation radius in which all human players will be gibed
@@ -71,13 +72,6 @@ namespace Content.Server.Nuke
[ViewVariables]
public float RemainingTime;
/// <summary>
/// Does bomb contains valid entity inside <see cref="DiskSlotName"/>?
/// If it is, user can anchor bomb or enter nuclear code to arm it.
/// </summary>
[ViewVariables]
public bool DiskInserted = false;
/// <summary>
/// Curent nuclear code buffer. Entered manually by players.
/// If valid it will allow arm/disarm bomb.

View File

@@ -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<NukeComponent, ComponentInit>(OnInit);
SubscribeLocalEvent<NukeComponent, ComponentRemove>(OnRemove);
SubscribeLocalEvent<NukeComponent, ActivateInWorldEvent>(OnActivate);
SubscribeLocalEvent<NukeComponent, ItemSlotChangedEvent>(OnItemSlotChanged);
SubscribeLocalEvent<NukeComponent, EntInsertedIntoContainerMessage>(OnItemSlotChanged);
SubscribeLocalEvent<NukeComponent, EntRemovedFromContainerMessage>(OnItemSlotChanged);
// anchoring logic
SubscribeLocalEvent<NukeComponent, AnchorAttemptEvent>(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,

View File

@@ -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
@@ -13,15 +16,18 @@ namespace Content.Server.PDA
public override string Name => "PDA";
[DataField("idSlot")]
public string IdSlot = "pdaIdSlot";
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<EntityPrototype>))]
public string? IdCard;
[ViewVariables] public IdCardComponent? ContainedID;
[ViewVariables] public bool PenInserted;
[ViewVariables] public bool FlashlightOn;
[ViewVariables] public string? OwnerName;

View File

@@ -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<PDAComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<PDAComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<PDAComponent, ComponentRemove>(OnComponentRemove);
SubscribeLocalEvent<PDAComponent, ActivateInWorldEvent>(OnActivateInWorld);
SubscribeLocalEvent<PDAComponent, UseInHandEvent>(OnUse);
SubscribeLocalEvent<PDAComponent, ItemSlotChangedEvent>(OnItemSlotChanged);
SubscribeLocalEvent<PDAComponent, EntInsertedIntoContainerMessage>(OnItemInserted);
SubscribeLocalEvent<PDAComponent, EntRemovedFromContainerMessage>(OnItemRemoved);
SubscribeLocalEvent<PDAComponent, LightToggleEvent>(OnLightToggle);
SubscribeLocalEvent<PDAComponent, UplinkInitEvent>(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)
if (args.Container.ID == pda.IdSlot.ID)
pda.ContainedID = args.Entity.GetComponentOrNull<IdCardComponent>();
UpdatePDAAppearance(pda);
UpdatePDAUserInterface(pda);
}
private void OnItemRemoved(EntityUid uid, PDAComponent pda, EntRemovedFromContainerMessage args)
{
var item = args.ContainedItem;
if (item == null || !EntityManager.TryGetComponent(item.Value, out IdCardComponent ? idCard))
if (args.Container.ID == pda.IdSlot.ID)
pda.ContainedID = null;
else
pda.ContainedID = idCard;
}
else if (args.SlotName == pda.PenSlot)
{
var item = args.ContainedItem;
pda.PenInserted = item != null;
}
UpdatePDAAppearance(pda);
UpdatePDAUserInterface(pda);
@@ -142,11 +139,15 @@ namespace Content.Server.PDA
var hasUplink = pda.Owner.HasComponent<UplinkComponent>();
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 _:

View File

@@ -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<SharedItemSlotsSystem>().
TryInsertContent(itemSlots, newID, pda.IdSlot);
_entityManager.EntitySysManager.GetEntitySystem<ItemSlotsSystem>().
TryInsert(wornItem.Owner.Uid, pda.IdSlot, newID);
}
}
else

View File

@@ -1,23 +0,0 @@
using Robust.Shared.GameObjects;
namespace Content.Shared.Containers.ItemSlots
{
/// <summary>
/// Item was placed in or removed from one of the slots in <see cref="SharedItemSlotsComponent"/>
/// </summary>
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;
}
}
}

View File

@@ -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
{
/// <summary>
/// Used for entities that can hold items in different slots. Needed by ItemSlotSystem to support basic
/// insert/eject interactions.
/// </summary>
[RegisterComponent]
[Friend(typeof(ItemSlotsSystem))]
public class ItemSlotsComponent : Component
{
public override string Name => "ItemSlots";
[ViewVariables]
[DataField("slots")]
public Dictionary<string, ItemSlot> Slots = new();
}
[Serializable, NetSerializable]
public sealed class ItemSlotsComponentState : ComponentState
{
public readonly Dictionary<string, bool> SlotLocked;
public ItemSlotsComponentState(Dictionary<string, ItemSlot> slots)
{
SlotLocked = new(slots.Count);
foreach (var (key, slot) in slots)
{
SlotLocked[key] = slot.Locked;
}
}
}
/// <summary>
/// This is effectively a wrapper for a ContainerSlot that adds content functionality like entity whitelists and
/// insert/eject sounds.
/// </summary>
[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?
/// <summary>
/// The name of this item slot. This will be shown to the user in the verb menu.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
[DataField("name")]
public string Name = string.Empty;
[DataField("startingItem", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string? StartingItem;
/// <summary>
/// Whether or not an item can currently be ejected or inserted from this slot.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
[DataField("locked")]
[ViewVariables(VVAccess.ReadWrite)]
public bool Locked = false;
/// <summary>
/// Whether the item slots system will attempt to eject this item to the user's hands when interacted with.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
[DataField("ejectOnInteract")]
public bool EjectOnInteract = false;
/// <summary>
/// Override the insert verb text. Defaults to [insert category] -> [item-name]. If not null, the verb will
/// not be given a category.
/// </summary>
[DataField("insertVerbText")]
public string? InsertVerbText;
/// <summary>
/// Override the insert verb text. Defaults to [eject category] -> [item-name]. If not null, the verb will
/// not be given a category.
/// </summary>
[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;
}
}

View File

@@ -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
{
/// <summary>
/// A class that handles interactions related to inserting/ejecting items into/from an item slot.
/// </summary>
public class ItemSlotsSystem : EntitySystem
{
[Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ItemSlotsComponent, ComponentStartup>(OnStartup);
SubscribeLocalEvent<ItemSlotsComponent, ComponentInit>(Oninitialize);
SubscribeLocalEvent<ItemSlotsComponent, InteractUsingEvent>(OnInteractUsing);
SubscribeLocalEvent<ItemSlotsComponent, InteractHandEvent>(OnInteractHand);
SubscribeLocalEvent<ItemSlotsComponent, GetAlternativeVerbsEvent>(AddEjectVerbs);
SubscribeLocalEvent<ItemSlotsComponent, GetInteractionVerbsEvent>(AddInteractionVerbsVerbs);
SubscribeLocalEvent<ItemSlotsComponent, ComponentGetState>(GetItemSlotsState);
SubscribeLocalEvent<ItemSlotsComponent, ComponentHandleState>(HandleItemSlotsState);
}
#region ComponentManagement
/// <summary>
/// Spawn in starting items for any item slots that should have one.
/// </summary>
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);
}
}
/// <summary>
/// Ensure item slots have containers.
/// </summary>
private void Oninitialize(EntityUid uid, ItemSlotsComponent itemSlots, ComponentInit args)
{
foreach (var (id, slot) in itemSlots.Slots)
{
slot.ContainerSlot = ContainerHelpers.EnsureContainer<ContainerSlot>(itemSlots.Owner, id);
}
}
/// <summary>
/// Given a new item slot, store it in the <see cref="ItemSlotsComponent"/> and ensure the slot has an item
/// container.
/// </summary>
public void AddItemSlot(EntityUid uid, string id, ItemSlot slot)
{
var itemSlots = EntityManager.EnsureComponent<ItemSlotsComponent>(uid);
slot.ContainerSlot = ContainerHelpers.EnsureContainer<ContainerSlot>(itemSlots.Owner, id);
DebugTools.Assert(!itemSlots.Slots.ContainsKey(id));
itemSlots.Slots[id] = slot;
}
/// <summary>
/// Remove an item slot. This should generally be called whenever a component that added a slot is being
/// removed.
/// </summary>
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
/// <summary>
/// Attempt to take an item from a slot, if any are set to EjectOnInteract.
/// </summary>
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;
}
}
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
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
/// <summary>
/// Insert an item into a slot. This does not perform checks, so make sure to also use <see
/// cref="CanInsert"/> or just use <see cref="TryInsert"/> instead.
/// </summary>
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);
}
/// <summary>
/// Check whether a given item can be inserted into a slot. Unless otherwise specified, this will return
/// false if the slot is already filled.
/// </summary>
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;
}
/// <summary>
/// Tries to insert item into a specific slot.
/// </summary>
/// <returns>False if failed to insert item</returns>
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);
}
/// <summary>
/// Tries to insert item into a specific slot.
/// </summary>
/// <returns>False if failed to insert item</returns>
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
/// <summary>
/// Eject an item into a slot. This does not perform checks (e.g., is the slot locked?), so you should
/// probably just use <see cref="TryEject"/> instead.
/// </summary>
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);
}
/// <summary>
/// Try to eject an item from a slot.
/// </summary>
/// <returns>False if item slot is locked or has no item inserted</returns>
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;
}
/// <summary>
/// Try to eject item from a slot.
/// </summary>
/// <returns>False if the id is not valid, the item slot is locked, or it has no item inserted</returns>
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);
}
/// <summary>
/// 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.
/// </summary>
/// <returns>
/// 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.
/// </returns>
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 <item>" 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 <item>" 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
/// <summary>
/// Get the contents of some item slot.
/// </summary>
public IEntity? GetItem(EntityUid uid, string id, ItemSlotsComponent? itemSlots = null)
{
if (!Resolve(uid, ref itemSlots))
return null;
return itemSlots.Slots.GetValueOrDefault(id)?.Item;
}
/// <summary>
/// Lock an item slot. This stops items from being inserted into or ejected from this slot.
/// </summary>
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);
}
/// <summary>
/// Lock an item slot. This stops items from being inserted into or ejected from this slot.
/// </summary>
public void SetLock(ItemSlotsComponent itemSlots, ItemSlot slot, bool locked)
{
slot.Locked = locked;
itemSlots.Dirty();
}
/// <summary>
/// Update the locked state of the managed item slots.
/// </summary>
/// <remarks>
/// Note that the slot's ContainerSlot performs its own networking, so we don't need to send information
/// about the contained entity.
/// </remarks>
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);
}
}
}

View File

@@ -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
{
/// <summary>
/// Used for entities that can hold items in different slots
/// Allows basic insert/eject interaction
/// </summary>
[RegisterComponent]
public class SharedItemSlotsComponent : Component
{
public override string Name => "ItemSlots";
[ViewVariables] [DataField("slots")] public Dictionary<string, ItemSlot> 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;
/// <summary>
/// The name of this item slot. This will be shown to the user in the verb menu.
/// </summary>
[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<EntityPrototype>))]
[ViewVariables] public string? StartingItem;
[ViewVariables] public ContainerSlot ContainerSlot = default!;
public bool HasEntity => ContainerSlot.ContainedEntity != null;
}
}

View File

@@ -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<SharedItemSlotsComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<SharedItemSlotsComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<SharedItemSlotsComponent, InteractUsingEvent>(OnInteractUsing);
SubscribeLocalEvent<SharedItemSlotsComponent, GetAlternativeVerbsEvent>(AddEjectVerbs);
SubscribeLocalEvent<SharedItemSlotsComponent, GetInteractionVerbsEvent>(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<ContainerSlot>(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);
}
/// <summary>
/// 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.
/// </summary>
/// <returns>False if failed to insert item</returns>
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);
}
/// <summary>
/// Can a given item be inserted into a slot, without ejecting the current item in that slot.
/// </summary>
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;
}
/// <summary>
/// Tries to insert item in known slot. Doesn't interact with user
/// </summary>
/// <returns>False if failed to insert item</returns>
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;
}
/// <summary>
/// Check if slot has some content in it (without ejecting item)
/// </summary>
/// <returns>Null if doesn't have any content</returns>
public IEntity? PeekItemInSlot(SharedItemSlotsComponent itemSlots, string slotName)
{
if (!itemSlots.Slots.TryGetValue(slotName, out var slot))
return null;
var item = slot.ContainerSlot.ContainedEntity;
return item;
}
/// <summary>
/// Try to eject item from slot to users hands
/// </summary>
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;
}
}
}

View File

@@ -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}

View File

@@ -22,10 +22,7 @@
mask:
- VaultImpassable
- type: Nuke
- type: InteractionOutline
- type: ItemSlots
slots:
DiskSlot:
diskSlot:
name: Disk
insertSound:
path: /Audio/Machines/terminal_insert_disc.ogg
@@ -34,6 +31,7 @@
whitelist:
tags:
- NukeDisk
- type: InteractionOutline
- type: UserInterface
interfaces:
- key: enum.NukeUiKey.Key

View File

@@ -34,23 +34,19 @@
type: PDABoundUserInterface
- key: enum.UplinkUiKey.Key
type: UplinkBoundUserInterface
- type: ItemSlots
slots:
pdaPenSlot:
item: "Pen"
- type: PDA
penSlot:
startingItem: Pen
whitelist:
tags:
- Write
pdaIdSlot:
idSlot:
name: ID Card
insertSound:
path: /Audio/Weapons/Guns/MagIn/batrifle_magin.ogg
ejectSound:
path: /Audio/Machines/id_swipe.ogg
ejectSound: /Audio/Machines/id_swipe.ogg
insertSound: /Audio/Weapons/Guns/MagIn/batrifle_magin.ogg
whitelist:
components:
- IdCard
- type: PDA
- 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

View File

@@ -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

View File

@@ -34,9 +34,9 @@
openSound:
path: /Audio/Misc/zip.ogg
- type: PaperLabel
- type: ItemSlots
slots:
labelSlot:
insertVerbText: Attach Label
ejectVerbText: Remove Label
whitelist:
components:
- Paper

View File

@@ -54,9 +54,9 @@
state_open: crate_open
state_closed: crate_door
- type: PaperLabel
- type: ItemSlots
slots:
labelSlot:
insertVerbText: Attach Label
ejectVerbText: Remove Label
whitelist:
components:
- Paper

View File

@@ -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,11 +47,9 @@
suffix: Filled
components:
- type: ItemCabinet
spawnPrototype: FireExtinguisher
- type: ItemSlots
slots:
cabinetSlot:
item: FireExtinguisher
ejectOnInteract: true
startingItem: FireExtinguisher
whitelist:
components:
- FireExtinguisher

View File

@@ -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,10 +45,9 @@
suffix: Filled
components:
- type: ItemCabinet
- type: ItemSlots
slots:
cabinetSlot:
item: FireAxe
startingItem: FireAxe
ejectOnInteract: true
whitelist:
tags:
- FireAxe

Binary file not shown.

After

Width:  |  Height:  |  Size: 587 B

View File

@@ -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