Make ItemCabinet use ItemSlots (#4771)
* pda slot names * cabinets use item slots * fix yaml * fix tests
This commit is contained in:
@@ -31,7 +31,7 @@ namespace Content.IntegrationTests.Tests.PDA
|
|||||||
components:
|
components:
|
||||||
- type: ItemSlots
|
- type: ItemSlots
|
||||||
slots:
|
slots:
|
||||||
pda_id_slot:
|
pdaIdSlot:
|
||||||
whitelist:
|
whitelist:
|
||||||
components:
|
components:
|
||||||
- IdCard
|
- IdCard
|
||||||
@@ -79,7 +79,7 @@ namespace Content.IntegrationTests.Tests.PDA
|
|||||||
|
|
||||||
var itemSlots = dummyPda.GetComponent<SharedItemSlotsComponent>();
|
var itemSlots = dummyPda.GetComponent<SharedItemSlotsComponent>();
|
||||||
sEntityManager.EntitySysManager.GetEntitySystem<SharedItemSlotsSystem>()
|
sEntityManager.EntitySysManager.GetEntitySystem<SharedItemSlotsSystem>()
|
||||||
.TryInsertContent(itemSlots, pdaIdCard, PDAComponent.IDSlotName);
|
.TryInsertContent(itemSlots, pdaIdCard, pdaComponent.IdSlot);
|
||||||
var pdaContainedId = pdaComponent.ContainedID;
|
var pdaContainedId = pdaComponent.ContainedID;
|
||||||
|
|
||||||
// The PDA in the hand should be found first
|
// The PDA in the hand should be found first
|
||||||
|
|||||||
@@ -1,16 +1,11 @@
|
|||||||
using System.IO;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Content.Server.Cabinet;
|
using Content.Shared.Containers.ItemSlots;
|
||||||
using Content.Server.GameObjects.Components;
|
|
||||||
using Content.Shared.Whitelist;
|
using Content.Shared.Whitelist;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using NUnit.Framework.Internal;
|
using NUnit.Framework.Internal;
|
||||||
using Robust.Shared.GameObjects;
|
using Robust.Shared.GameObjects;
|
||||||
using Robust.Shared.IoC;
|
|
||||||
using Robust.Shared.Map;
|
using Robust.Shared.Map;
|
||||||
using Robust.Shared.Maths;
|
|
||||||
using Robust.Shared.Prototypes;
|
|
||||||
using Robust.Shared.Serialization.Manager;
|
|
||||||
|
|
||||||
namespace Content.IntegrationTests.Tests.Utility
|
namespace Content.IntegrationTests.Tests.Utility
|
||||||
{
|
{
|
||||||
@@ -30,14 +25,16 @@ namespace Content.IntegrationTests.Tests.Utility
|
|||||||
- type: entity
|
- type: entity
|
||||||
id: WhitelistDummy
|
id: WhitelistDummy
|
||||||
components:
|
components:
|
||||||
- type: ItemCabinet
|
- type: ItemSlots
|
||||||
whitelist:
|
slots:
|
||||||
prototypes:
|
slotName:
|
||||||
- ValidPrototypeDummy
|
whitelist:
|
||||||
components:
|
prototypes:
|
||||||
- {ValidComponent}
|
- ValidPrototypeDummy
|
||||||
tags:
|
components:
|
||||||
- ValidTag
|
- {ValidComponent}
|
||||||
|
tags:
|
||||||
|
- ValidTag
|
||||||
|
|
||||||
- type: entity
|
- type: entity
|
||||||
id: InvalidComponentDummy
|
id: InvalidComponentDummy
|
||||||
@@ -102,7 +99,7 @@ namespace Content.IntegrationTests.Tests.Utility
|
|||||||
|
|
||||||
// Test from serialized
|
// Test from serialized
|
||||||
var dummy = entityManager.SpawnEntity("WhitelistDummy", mapCoordinates);
|
var dummy = entityManager.SpawnEntity("WhitelistDummy", mapCoordinates);
|
||||||
var whitelistSer = dummy.GetComponent<ItemCabinetComponent>().Whitelist;
|
var whitelistSer = dummy.GetComponent<SharedItemSlotsComponent>().Slots.Values.First().Whitelist;
|
||||||
Assert.That(whitelistSer, Is.Not.Null);
|
Assert.That(whitelistSer, Is.Not.Null);
|
||||||
|
|
||||||
Assert.That(whitelistSer.Components, Is.Not.Null);
|
Assert.That(whitelistSer.Components, Is.Not.Null);
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
using Content.Shared.Sound;
|
using Content.Shared.Sound;
|
||||||
using Content.Shared.Whitelist;
|
|
||||||
using Robust.Shared.Containers;
|
|
||||||
using Robust.Shared.GameObjects;
|
using Robust.Shared.GameObjects;
|
||||||
using Robust.Shared.Serialization.Manager.Attributes;
|
using Robust.Shared.Serialization.Manager.Attributes;
|
||||||
using Robust.Shared.ViewVariables;
|
using Robust.Shared.ViewVariables;
|
||||||
@@ -8,8 +6,7 @@ using Robust.Shared.ViewVariables;
|
|||||||
namespace Content.Server.Cabinet
|
namespace Content.Server.Cabinet
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Used for entities that can hold one item that fits the whitelist, which can be extracted by interacting with
|
/// Used for entities that can be opened, closed, and can hold one item. E.g., fire extinguisher cabinets.
|
||||||
/// the entity, and can have an item fitting the whitelist placed back inside
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[RegisterComponent]
|
[RegisterComponent]
|
||||||
public class ItemCabinetComponent : Component
|
public class ItemCabinetComponent : Component
|
||||||
@@ -24,21 +21,10 @@ namespace Content.Server.Cabinet
|
|||||||
public SoundSpecifier DoorSound { get; set; } = default!;
|
public SoundSpecifier DoorSound { get; set; } = default!;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The prototype that should be spawned inside the cabinet when it is map initialized.
|
/// The slot name, used to get the actual item slot from the ItemSlotsComponent.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ViewVariables]
|
[DataField("cabinetSlot")]
|
||||||
[DataField("spawnPrototype")]
|
public string CabinetSlot = "cabinetSlot";
|
||||||
public string? SpawnPrototype { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A whitelist defining which entities are allowed into the cabinet.
|
|
||||||
/// </summary>
|
|
||||||
[ViewVariables]
|
|
||||||
[DataField("whitelist")]
|
|
||||||
public EntityWhitelist? Whitelist = null;
|
|
||||||
|
|
||||||
[ViewVariables]
|
|
||||||
public ContainerSlot ItemContainer = default!;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether the cabinet is currently open or not.
|
/// Whether the cabinet is currently open or not.
|
||||||
|
|||||||
@@ -1,52 +1,68 @@
|
|||||||
using Content.Server.Hands.Components;
|
|
||||||
using Content.Server.Items;
|
|
||||||
using Content.Shared.ActionBlocker;
|
|
||||||
using Content.Shared.Audio;
|
using Content.Shared.Audio;
|
||||||
using Content.Shared.Cabinet;
|
using Content.Shared.Cabinet;
|
||||||
using Content.Shared.Hands.Components;
|
using Content.Shared.Containers.ItemSlots;
|
||||||
using Content.Shared.Interaction;
|
using Content.Shared.Interaction;
|
||||||
using Content.Shared.Popups;
|
|
||||||
using Content.Shared.Verbs;
|
using Content.Shared.Verbs;
|
||||||
using Robust.Shared.Audio;
|
using Robust.Shared.Audio;
|
||||||
using Robust.Shared.Containers;
|
|
||||||
using Robust.Shared.GameObjects;
|
using Robust.Shared.GameObjects;
|
||||||
using Robust.Shared.IoC;
|
using Robust.Shared.IoC;
|
||||||
using Robust.Shared.Localization;
|
using Robust.Shared.Localization;
|
||||||
using Robust.Shared.Player;
|
using Robust.Shared.Player;
|
||||||
|
using System;
|
||||||
|
|
||||||
namespace Content.Server.Cabinet
|
namespace Content.Server.Cabinet
|
||||||
{
|
{
|
||||||
public class ItemCabinetSystem : EntitySystem
|
public class ItemCabinetSystem : EntitySystem
|
||||||
{
|
{
|
||||||
[Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!;
|
[Dependency] private readonly SharedItemSlotsSystem _itemSlotsSystem = default!;
|
||||||
|
|
||||||
public override void Initialize()
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
base.Initialize();
|
base.Initialize();
|
||||||
|
|
||||||
SubscribeLocalEvent<ItemCabinetComponent, MapInitEvent>(OnMapInitialize);
|
|
||||||
|
|
||||||
SubscribeLocalEvent<ItemCabinetComponent, InteractUsingEvent>(OnInteractUsing);
|
SubscribeLocalEvent<ItemCabinetComponent, InteractUsingEvent>(OnInteractUsing);
|
||||||
SubscribeLocalEvent<ItemCabinetComponent, InteractHandEvent>(OnInteractHand);
|
SubscribeLocalEvent<ItemCabinetComponent, InteractHandEvent>(OnInteractHand);
|
||||||
SubscribeLocalEvent<ItemCabinetComponent, ActivateInWorldEvent>(OnActivateInWorld);
|
SubscribeLocalEvent<ItemCabinetComponent, ActivateInWorldEvent>(OnActivateInWorld);
|
||||||
|
SubscribeLocalEvent<ItemCabinetComponent, ComponentStartup>(InitializeAppearance);
|
||||||
SubscribeLocalEvent<ItemCabinetComponent, TryEjectItemCabinetEvent>(OnTryEjectItemCabinet);
|
SubscribeLocalEvent<ItemCabinetComponent, ItemSlotChangedEvent>(OnItemSlotChanged);
|
||||||
SubscribeLocalEvent<ItemCabinetComponent, TryInsertItemCabinetEvent>(OnTryInsertItemCabinet);
|
|
||||||
SubscribeLocalEvent<ItemCabinetComponent, ToggleItemCabinetEvent>(OnToggleItemCabinet);
|
|
||||||
|
|
||||||
SubscribeLocalEvent<ItemCabinetComponent, GetInteractionVerbsEvent>(AddEjectInsertVerbs);
|
|
||||||
SubscribeLocalEvent<ItemCabinetComponent, GetActivationVerbsEvent>(AddToggleOpenVerb);
|
SubscribeLocalEvent<ItemCabinetComponent, GetActivationVerbsEvent>(AddToggleOpenVerb);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddToggleOpenVerb(EntityUid uid, ItemCabinetComponent component, GetActivationVerbsEvent args)
|
private void InitializeAppearance(EntityUid uid, ItemCabinetComponent component, ComponentStartup args)
|
||||||
|
{
|
||||||
|
UpdateAppearance(uid, component);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateAppearance(EntityUid uid,
|
||||||
|
ItemCabinetComponent? cabinet = null,
|
||||||
|
SharedItemSlotsComponent? itemSlots = null,
|
||||||
|
SharedAppearanceComponent? appearance = null)
|
||||||
|
{
|
||||||
|
if (!Resolve(uid, ref cabinet, ref itemSlots, ref appearance))
|
||||||
|
return;
|
||||||
|
|
||||||
|
appearance.SetData(ItemCabinetVisuals.IsOpen, cabinet.Opened);
|
||||||
|
|
||||||
|
if (!itemSlots.Slots.TryGetValue(cabinet.CabinetSlot, out var slot))
|
||||||
|
return;
|
||||||
|
|
||||||
|
appearance.SetData(ItemCabinetVisuals.ContainsItem, slot.HasEntity);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnItemSlotChanged(EntityUid uid, ItemCabinetComponent cabinet, ItemSlotChangedEvent args)
|
||||||
|
{
|
||||||
|
UpdateAppearance(uid, cabinet, args.SlotsComponent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddToggleOpenVerb(EntityUid uid, ItemCabinetComponent cabinet, GetActivationVerbsEvent args)
|
||||||
{
|
{
|
||||||
if (args.Hands == null || !args.CanAccess || !args.CanInteract)
|
if (args.Hands == null || !args.CanAccess || !args.CanInteract)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Toggle open verb
|
// Toggle open verb
|
||||||
Verb toggleVerb = new();
|
Verb toggleVerb = new();
|
||||||
toggleVerb.Act = () => OnToggleItemCabinet(uid, component);
|
toggleVerb.Act = () => ToggleItemCabinet(uid, cabinet);
|
||||||
if (component.Opened)
|
if (cabinet.Opened)
|
||||||
{
|
{
|
||||||
toggleVerb.Text = Loc.GetString("verb-categories-close");
|
toggleVerb.Text = Loc.GetString("verb-categories-close");
|
||||||
toggleVerb.IconTexture = "/Textures/Interface/VerbIcons/close.svg.192dpi.png";
|
toggleVerb.IconTexture = "/Textures/Interface/VerbIcons/close.svg.192dpi.png";
|
||||||
@@ -59,210 +75,65 @@ namespace Content.Server.Cabinet
|
|||||||
args.Verbs.Add(toggleVerb);
|
args.Verbs.Add(toggleVerb);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddEjectInsertVerbs(EntityUid uid, ItemCabinetComponent component, GetInteractionVerbsEvent args)
|
/// <summary>
|
||||||
{
|
/// Try insert an item if the cabinet is opened. Otherwise, just try open it.
|
||||||
if (args.Hands == null || !args.CanAccess || !args.CanInteract)
|
/// </summary>
|
||||||
return;
|
|
||||||
|
|
||||||
// "Eject" item verb
|
|
||||||
if (component.Opened &&
|
|
||||||
component.ItemContainer.ContainedEntity != null &&
|
|
||||||
_actionBlockerSystem.CanPickup(args.User))
|
|
||||||
{
|
|
||||||
Verb verb = new();
|
|
||||||
verb.Act = () =>
|
|
||||||
{
|
|
||||||
TakeItem(component, args.Hands, component.ItemContainer.ContainedEntity, args.User);
|
|
||||||
UpdateVisuals(component);
|
|
||||||
};
|
|
||||||
verb.Text = Loc.GetString("pick-up-verb-get-data-text");
|
|
||||||
verb.IconTexture = "/Textures/Interface/VerbIcons/pickup.svg.192dpi.png";
|
|
||||||
args.Verbs.Add(verb);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert item verb
|
|
||||||
if (component.Opened &&
|
|
||||||
args.Using != null &&
|
|
||||||
_actionBlockerSystem.CanDrop(args.User) &&
|
|
||||||
(component.Whitelist?.IsValid(args.Using) ?? true) &&
|
|
||||||
component.ItemContainer.CanInsert(args.Using))
|
|
||||||
{
|
|
||||||
Verb verb = new();
|
|
||||||
verb.Act = () =>
|
|
||||||
{
|
|
||||||
args.Hands.TryPutEntityIntoContainer(args.Using, component.ItemContainer);
|
|
||||||
UpdateVisuals(component);
|
|
||||||
};
|
|
||||||
verb.Category = VerbCategory.Insert;
|
|
||||||
verb.Text = args.Using.Name;
|
|
||||||
args.Verbs.Add(verb);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnMapInitialize(EntityUid uid, ItemCabinetComponent comp, MapInitEvent args)
|
|
||||||
{
|
|
||||||
var owner = EntityManager.GetEntity(uid);
|
|
||||||
comp.ItemContainer =
|
|
||||||
owner.EnsureContainer<ContainerSlot>("item_cabinet", out _);
|
|
||||||
|
|
||||||
if (comp.SpawnPrototype != null)
|
|
||||||
comp.ItemContainer.Insert(EntityManager.SpawnEntity(comp.SpawnPrototype, owner.Transform.Coordinates));
|
|
||||||
|
|
||||||
UpdateVisuals(comp);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnInteractUsing(EntityUid uid, ItemCabinetComponent comp, InteractUsingEvent args)
|
private void OnInteractUsing(EntityUid uid, ItemCabinetComponent comp, InteractUsingEvent args)
|
||||||
{
|
{
|
||||||
args.Handled = true;
|
if (args.Handled)
|
||||||
|
return;
|
||||||
|
|
||||||
if (!comp.Opened)
|
if (!comp.Opened)
|
||||||
{
|
ToggleItemCabinet(uid, comp);
|
||||||
RaiseLocalEvent(uid, new ToggleItemCabinetEvent(), false);
|
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
_itemSlotsSystem.TryInsertContent(uid, args.Used, args.User);
|
||||||
RaiseLocalEvent(uid, new TryInsertItemCabinetEvent(args.User, args.Used), false);
|
|
||||||
}
|
|
||||||
|
|
||||||
args.Handled = true;
|
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)
|
private void OnInteractHand(EntityUid uid, ItemCabinetComponent comp, InteractHandEvent args)
|
||||||
{
|
{
|
||||||
args.Handled = true;
|
if (args.Handled)
|
||||||
if (comp.Opened)
|
return;
|
||||||
{
|
|
||||||
if (comp.ItemContainer.ContainedEntity == null)
|
if (!EntityManager.TryGetComponent(uid, out SharedItemSlotsComponent itemSlots))
|
||||||
{
|
return;
|
||||||
RaiseLocalEvent(uid, new ToggleItemCabinetEvent(), false);
|
|
||||||
return;
|
if (!itemSlots.Slots.TryGetValue(comp.CabinetSlot, out var slot))
|
||||||
}
|
return;
|
||||||
RaiseLocalEvent(uid, new TryEjectItemCabinetEvent(args.User), false);
|
|
||||||
}
|
if (comp.Opened && slot.HasEntity)
|
||||||
|
_itemSlotsSystem.TryEjectContent(uid, comp.CabinetSlot, args.User);
|
||||||
else
|
else
|
||||||
{
|
ToggleItemCabinet(uid, comp);
|
||||||
RaiseLocalEvent(uid, new ToggleItemCabinetEvent(), false);
|
|
||||||
}
|
args.Handled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnActivateInWorld(EntityUid uid, ItemCabinetComponent comp, ActivateInWorldEvent args)
|
private void OnActivateInWorld(EntityUid uid, ItemCabinetComponent comp, ActivateInWorldEvent args)
|
||||||
{
|
{
|
||||||
|
if (args.Handled)
|
||||||
|
return;
|
||||||
|
|
||||||
args.Handled = true;
|
args.Handled = true;
|
||||||
RaiseLocalEvent(uid, new ToggleItemCabinetEvent(), false);
|
ToggleItemCabinet(uid, comp);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Toggles the ItemCabinet's state.
|
/// Toggles the ItemCabinet's state.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void OnToggleItemCabinet(EntityUid uid, ItemCabinetComponent comp, ToggleItemCabinetEvent? args = null)
|
private void ToggleItemCabinet(EntityUid uid, ItemCabinetComponent? cabinet = null)
|
||||||
{
|
{
|
||||||
comp.Opened = !comp.Opened;
|
if (!Resolve(uid, ref cabinet))
|
||||||
ClickLatchSound(comp);
|
|
||||||
UpdateVisuals(comp);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Tries to insert an entity into the ItemCabinet's slot from the user's hands.
|
|
||||||
/// </summary>
|
|
||||||
private static void OnTryInsertItemCabinet(EntityUid uid, ItemCabinetComponent comp, TryInsertItemCabinetEvent args)
|
|
||||||
{
|
|
||||||
if (comp.ItemContainer.ContainedEntity != null || args.Cancelled || (comp.Whitelist != null && !comp.Whitelist.IsValid(args.Item)))
|
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
if (!args.User.TryGetComponent<HandsComponent>(out var hands) || !hands.Drop(args.Item, comp.ItemContainer))
|
cabinet.Opened = !cabinet.Opened;
|
||||||
{
|
SoundSystem.Play(Filter.Pvs(uid), cabinet.DoorSound.GetSound(), uid, AudioHelpers.WithVariation(0.15f));
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
UpdateVisuals(comp);
|
UpdateAppearance(uid, cabinet);
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Tries to eject the ItemCabinet's item, either into the user's hands or onto the floor.
|
|
||||||
/// </summary>
|
|
||||||
private static void OnTryEjectItemCabinet(EntityUid uid, ItemCabinetComponent comp, TryEjectItemCabinetEvent args)
|
|
||||||
{
|
|
||||||
if (comp.ItemContainer.ContainedEntity == null || args.Cancelled)
|
|
||||||
return;
|
|
||||||
if (args.User.TryGetComponent(out SharedHandsComponent? hands))
|
|
||||||
{
|
|
||||||
// Put into hands
|
|
||||||
TakeItem(comp, hands, comp.ItemContainer.ContainedEntity, args.User);
|
|
||||||
}
|
|
||||||
else if (comp.ItemContainer.Remove(comp.ItemContainer.ContainedEntity))
|
|
||||||
{
|
|
||||||
comp.ItemContainer.ContainedEntity.Transform.Coordinates = args.User.Transform.Coordinates;
|
|
||||||
}
|
|
||||||
UpdateVisuals(comp);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Tries to eject the ItemCabinet's item, either into the user's hands. Used by both <see
|
|
||||||
/// cref="OnTryEjectItemCabinet"/> and the eject verbs.
|
|
||||||
/// </summary>
|
|
||||||
private static void TakeItem(ItemCabinetComponent comp, SharedHandsComponent hands, IEntity containedEntity, IEntity user)
|
|
||||||
{
|
|
||||||
if (containedEntity.HasComponent<ItemComponent>())
|
|
||||||
{
|
|
||||||
if (!hands.TryPutInActiveHandOrAny(containedEntity))
|
|
||||||
containedEntity.Transform.Coordinates = hands.Owner.Transform.Coordinates;
|
|
||||||
|
|
||||||
comp.Owner.PopupMessage(user,
|
|
||||||
Loc.GetString("comp-item-cabinet-successfully-taken",
|
|
||||||
("item", containedEntity),
|
|
||||||
("cabinet", comp.Owner)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void UpdateVisuals(ItemCabinetComponent comp)
|
|
||||||
{
|
|
||||||
if (comp.Owner.TryGetComponent(out SharedAppearanceComponent? appearance))
|
|
||||||
{
|
|
||||||
appearance.SetData(ItemCabinetVisuals.IsOpen, comp.Opened);
|
|
||||||
appearance.SetData(ItemCabinetVisuals.ContainsItem, comp.ItemContainer.ContainedEntity != null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ClickLatchSound(ItemCabinetComponent comp)
|
|
||||||
{
|
|
||||||
SoundSystem.Play(Filter.Pvs(comp.Owner), comp.DoorSound.GetSound(), comp.Owner, AudioHelpers.WithVariation(0.15f));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ToggleItemCabinetEvent : EntityEventArgs
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public class TryEjectItemCabinetEvent : CancellableEntityEventArgs
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The user who tried to eject the item.
|
|
||||||
/// </summary>
|
|
||||||
public IEntity User;
|
|
||||||
|
|
||||||
public TryEjectItemCabinetEvent(IEntity user)
|
|
||||||
{
|
|
||||||
User = user;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class TryInsertItemCabinetEvent : CancellableEntityEventArgs
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The user who tried to eject the item.
|
|
||||||
/// </summary>
|
|
||||||
public IEntity User;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The item to be inserted.
|
|
||||||
/// </summary>
|
|
||||||
public IEntity Item;
|
|
||||||
|
|
||||||
public TryInsertItemCabinetEvent(IEntity user, IEntity item)
|
|
||||||
{
|
|
||||||
User = user;
|
|
||||||
Item = item;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,8 +13,11 @@ namespace Content.Server.PDA
|
|||||||
{
|
{
|
||||||
public override string Name => "PDA";
|
public override string Name => "PDA";
|
||||||
|
|
||||||
public const string IDSlotName = "pda_id_slot";
|
[DataField("idSlot")]
|
||||||
public const string PenSlotName = "pda_pen_slot";
|
public string IdSlot = "pdaIdSlot";
|
||||||
|
|
||||||
|
[DataField("penSlot")]
|
||||||
|
public string PenSlot = "pdaPenSlot";
|
||||||
|
|
||||||
[ViewVariables] [DataField("idCard")] public string? StartingIdCard;
|
[ViewVariables] [DataField("idCard")] public string? StartingIdCard;
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ namespace Content.Server.PDA
|
|||||||
SubscribeLocalEvent<PDAComponent, MapInitEvent>(OnMapInit);
|
SubscribeLocalEvent<PDAComponent, MapInitEvent>(OnMapInit);
|
||||||
SubscribeLocalEvent<PDAComponent, ActivateInWorldEvent>(OnActivateInWorld);
|
SubscribeLocalEvent<PDAComponent, ActivateInWorldEvent>(OnActivateInWorld);
|
||||||
SubscribeLocalEvent<PDAComponent, UseInHandEvent>(OnUse);
|
SubscribeLocalEvent<PDAComponent, UseInHandEvent>(OnUse);
|
||||||
SubscribeLocalEvent<PDAComponent, ItemSlotChanged>(OnItemSlotChanged);
|
SubscribeLocalEvent<PDAComponent, ItemSlotChangedEvent>(OnItemSlotChanged);
|
||||||
SubscribeLocalEvent<PDAComponent, LightToggleEvent>(OnLightToggle);
|
SubscribeLocalEvent<PDAComponent, LightToggleEvent>(OnLightToggle);
|
||||||
|
|
||||||
SubscribeLocalEvent<PDAComponent, UplinkInitEvent>(OnUplinkInit);
|
SubscribeLocalEvent<PDAComponent, UplinkInitEvent>(OnUplinkInit);
|
||||||
@@ -53,7 +53,7 @@ namespace Content.Server.PDA
|
|||||||
// if pda prototype doesn't have slots, ID will drop down on ground
|
// if pda prototype doesn't have slots, ID will drop down on ground
|
||||||
var idCard = EntityManager.SpawnEntity(pda.StartingIdCard, pda.Owner.Transform.Coordinates);
|
var idCard = EntityManager.SpawnEntity(pda.StartingIdCard, pda.Owner.Transform.Coordinates);
|
||||||
if (EntityManager.TryGetComponent(uid, out SharedItemSlotsComponent? itemSlots))
|
if (EntityManager.TryGetComponent(uid, out SharedItemSlotsComponent? itemSlots))
|
||||||
_slotsSystem.TryInsertContent(itemSlots, idCard, PDAComponent.IDSlotName);
|
_slotsSystem.TryInsertContent(itemSlots, idCard, pda.IdSlot);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,10 +71,10 @@ namespace Content.Server.PDA
|
|||||||
args.Handled = OpenUI(pda, args.User);
|
args.Handled = OpenUI(pda, args.User);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnItemSlotChanged(EntityUid uid, PDAComponent pda, ItemSlotChanged args)
|
private void OnItemSlotChanged(EntityUid uid, PDAComponent pda, ItemSlotChangedEvent args)
|
||||||
{
|
{
|
||||||
// check if ID slot changed
|
// check if ID slot changed
|
||||||
if (args.SlotName == PDAComponent.IDSlotName)
|
if (args.SlotName == pda.IdSlot)
|
||||||
{
|
{
|
||||||
var item = args.ContainedItem;
|
var item = args.ContainedItem;
|
||||||
if (item == null || !EntityManager.TryGetComponent(item.Value, out IdCardComponent ? idCard))
|
if (item == null || !EntityManager.TryGetComponent(item.Value, out IdCardComponent ? idCard))
|
||||||
@@ -82,7 +82,7 @@ namespace Content.Server.PDA
|
|||||||
else
|
else
|
||||||
pda.ContainedID = idCard;
|
pda.ContainedID = idCard;
|
||||||
}
|
}
|
||||||
else if (args.SlotName == PDAComponent.PenSlotName)
|
else if (args.SlotName == pda.PenSlot)
|
||||||
{
|
{
|
||||||
var item = args.ContainedItem;
|
var item = args.ContainedItem;
|
||||||
pda.PenInserted = item != null;
|
pda.PenInserted = item != null;
|
||||||
@@ -162,14 +162,12 @@ namespace Content.Server.PDA
|
|||||||
|
|
||||||
case PDAEjectIDMessage _:
|
case PDAEjectIDMessage _:
|
||||||
{
|
{
|
||||||
if (pda.Owner.TryGetComponent(out SharedItemSlotsComponent? itemSlots))
|
_slotsSystem.TryEjectContent(pda.Owner.Uid, pda.IdSlot, msg.Session.AttachedEntity);
|
||||||
_slotsSystem.TryEjectContent(itemSlots, PDAComponent.IDSlotName, msg.Session.AttachedEntity);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case PDAEjectPenMessage _:
|
case PDAEjectPenMessage _:
|
||||||
{
|
{
|
||||||
if (pda.Owner.TryGetComponent(out SharedItemSlotsComponent? itemSlots))
|
_slotsSystem.TryEjectContent(pda.Owner.Uid, pda.PenSlot, msg.Session.AttachedEntity);
|
||||||
_slotsSystem.TryEjectContent(itemSlots, PDAComponent.PenSlotName, msg.Session.AttachedEntity);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case PDAShowUplinkMessage _:
|
case PDAShowUplinkMessage _:
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ namespace Content.Server.Sandbox
|
|||||||
if (pda.Owner.TryGetComponent(out SharedItemSlotsComponent? itemSlots))
|
if (pda.Owner.TryGetComponent(out SharedItemSlotsComponent? itemSlots))
|
||||||
{
|
{
|
||||||
_entityManager.EntitySysManager.GetEntitySystem<SharedItemSlotsSystem>().
|
_entityManager.EntitySysManager.GetEntitySystem<SharedItemSlotsSystem>().
|
||||||
TryInsertContent(itemSlots, newID, PDAComponent.IDSlotName);
|
TryInsertContent(itemSlots, newID, pda.IdSlot);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -5,14 +5,14 @@ namespace Content.Shared.Containers.ItemSlots
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Item was placed in or removed from one of the slots in <see cref="SharedItemSlotsComponent"/>
|
/// Item was placed in or removed from one of the slots in <see cref="SharedItemSlotsComponent"/>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ItemSlotChanged : EntityEventArgs
|
public class ItemSlotChangedEvent : EntityEventArgs
|
||||||
{
|
{
|
||||||
public SharedItemSlotsComponent SlotsComponent;
|
public SharedItemSlotsComponent SlotsComponent;
|
||||||
public string SlotName;
|
public string SlotName;
|
||||||
public ItemSlot Slot;
|
public ItemSlot Slot;
|
||||||
public readonly EntityUid? ContainedItem;
|
public readonly EntityUid? ContainedItem;
|
||||||
|
|
||||||
public ItemSlotChanged(SharedItemSlotsComponent slotsComponent, string slotName, ItemSlot slot)
|
public ItemSlotChangedEvent(SharedItemSlotsComponent slotsComponent, string slotName, ItemSlot slot)
|
||||||
{
|
{
|
||||||
SlotsComponent = slotsComponent;
|
SlotsComponent = slotsComponent;
|
||||||
SlotName = slotName;
|
SlotName = slotName;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using Content.Shared.Sound;
|
|||||||
using Content.Shared.Whitelist;
|
using Content.Shared.Whitelist;
|
||||||
using Robust.Shared.Containers;
|
using Robust.Shared.Containers;
|
||||||
using Robust.Shared.GameObjects;
|
using Robust.Shared.GameObjects;
|
||||||
|
using Robust.Shared.Localization;
|
||||||
using Robust.Shared.Prototypes;
|
using Robust.Shared.Prototypes;
|
||||||
using Robust.Shared.Serialization.Manager.Attributes;
|
using Robust.Shared.Serialization.Manager.Attributes;
|
||||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||||
@@ -31,9 +32,23 @@ namespace Content.Shared.Containers.ItemSlots
|
|||||||
[ViewVariables] [DataField("insertSound")] public SoundSpecifier? InsertSound;
|
[ViewVariables] [DataField("insertSound")] public SoundSpecifier? InsertSound;
|
||||||
[ViewVariables] [DataField("ejectSound")] public SoundSpecifier? EjectSound;
|
[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>))]
|
[DataField("item", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
|
||||||
[ViewVariables] public string? StartingItem;
|
[ViewVariables] public string? StartingItem;
|
||||||
|
|
||||||
[ViewVariables] public ContainerSlot ContainerSlot = default!;
|
[ViewVariables] public ContainerSlot ContainerSlot = default!;
|
||||||
|
|
||||||
|
public bool HasEntity => ContainerSlot.ContainedEntity != null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ namespace Content.Shared.Containers.ItemSlots
|
|||||||
var item = EntityManager.SpawnEntity(slot.StartingItem, itemSlots.Owner.Transform.Coordinates);
|
var item = EntityManager.SpawnEntity(slot.StartingItem, itemSlots.Owner.Transform.Coordinates);
|
||||||
slot.ContainerSlot.Insert(item);
|
slot.ContainerSlot.Insert(item);
|
||||||
|
|
||||||
RaiseLocalEvent(uid, new ItemSlotChanged(itemSlots, slotName, slot));
|
RaiseLocalEvent(uid, new ItemSlotChangedEvent(itemSlots, slotName, slot));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -76,11 +76,9 @@ namespace Content.Shared.Containers.ItemSlots
|
|||||||
continue;
|
continue;
|
||||||
|
|
||||||
Verb verb = new();
|
Verb verb = new();
|
||||||
// TODO ITEMSLOTS give item slot names localization strings?
|
verb.Text = slot.Name;
|
||||||
// Basically: its much nicer to have "insert ID" instead of the much longer "Eject <full-in-game-username>'s ID card (assistant)"
|
|
||||||
verb.Text = slot.ContainerSlot.ContainedEntity.Name;
|
|
||||||
verb.Category = VerbCategory.Eject;
|
verb.Category = VerbCategory.Eject;
|
||||||
verb.Act = () => TryEjectContent(component, slotName, args.User);
|
verb.Act = () => TryEjectContent(uid, slotName, args.User, component);
|
||||||
|
|
||||||
args.Verbs.Add(verb);
|
args.Verbs.Add(verb);
|
||||||
}
|
}
|
||||||
@@ -100,9 +98,7 @@ namespace Content.Shared.Containers.ItemSlots
|
|||||||
continue;
|
continue;
|
||||||
|
|
||||||
Verb verb = new();
|
Verb verb = new();
|
||||||
// TODO ITEMSLOTS give item slot names localization strings?
|
verb.Text = slot.Name != string.Empty ? slot.Name : args.Using.Name;
|
||||||
// Basically: its much nicer to have "insert ID" instead of the much longer "Insert <full-in-game-username>'s ID card (assistant)"
|
|
||||||
verb.Text = args.Using.Name;
|
|
||||||
verb.Category = VerbCategory.Insert;
|
verb.Category = VerbCategory.Insert;
|
||||||
verb.Act = () => InsertContent(component, slot, slotName, args.Using);
|
verb.Act = () => InsertContent(component, slot, slotName, args.Using);
|
||||||
args.Verbs.Add(verb);
|
args.Verbs.Add(verb);
|
||||||
@@ -114,15 +110,18 @@ namespace Content.Shared.Containers.ItemSlots
|
|||||||
if (args.Handled)
|
if (args.Handled)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
args.Handled = TryInsertContent(itemSlots, args.Used, args.User);
|
args.Handled = TryInsertContent(uid, args.Used, args.User, itemSlots);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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.
|
/// 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>
|
/// </summary>
|
||||||
/// <returns>False if failed to insert item</returns>
|
/// <returns>False if failed to insert item</returns>
|
||||||
public bool TryInsertContent(SharedItemSlotsComponent itemSlots, IEntity item, IEntity user, SharedHandsComponent? hands = null)
|
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))
|
if (!Resolve(user.Uid, ref hands))
|
||||||
{
|
{
|
||||||
itemSlots.Owner.PopupMessage(user, Loc.GetString("item-slots-try-insert-no-hands"));
|
itemSlots.Owner.PopupMessage(user, Loc.GetString("item-slots-try-insert-no-hands"));
|
||||||
@@ -164,7 +163,7 @@ namespace Content.Shared.Containers.ItemSlots
|
|||||||
{
|
{
|
||||||
// insert item
|
// insert item
|
||||||
slot.ContainerSlot.Insert(item);
|
slot.ContainerSlot.Insert(item);
|
||||||
RaiseLocalEvent(itemSlots.Owner.Uid, new ItemSlotChanged(itemSlots, slotName, slot));
|
RaiseLocalEvent(itemSlots.Owner.Uid, new ItemSlotChangedEvent(itemSlots, slotName, slot));
|
||||||
|
|
||||||
// play sound
|
// play sound
|
||||||
if (slot.InsertSound != null)
|
if (slot.InsertSound != null)
|
||||||
@@ -218,8 +217,11 @@ namespace Content.Shared.Containers.ItemSlots
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Try to eject item from slot to users hands
|
/// Try to eject item from slot to users hands
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool TryEjectContent(SharedItemSlotsComponent itemSlots, string slotName, IEntity? user)
|
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))
|
if (!itemSlots.Slots.TryGetValue(slotName, out var slot))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
@@ -246,7 +248,7 @@ namespace Content.Shared.Containers.ItemSlots
|
|||||||
if (slot.EjectSound != null)
|
if (slot.EjectSound != null)
|
||||||
SoundSystem.Play(Filter.Pvs(itemSlots.Owner), slot.EjectSound.GetSound(), itemSlots.Owner);
|
SoundSystem.Play(Filter.Pvs(itemSlots.Owner), slot.EjectSound.GetSound(), itemSlots.Owner);
|
||||||
|
|
||||||
RaiseLocalEvent(itemSlots.Owner.Uid, new ItemSlotChanged(itemSlots, slotName, slot));
|
RaiseLocalEvent(itemSlots.Owner.Uid, new ItemSlotChangedEvent(itemSlots, slotName, slot));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
### Used for item cabinet (fire extinguisher cabinets)
|
|
||||||
|
|
||||||
## Displayed when the item is successfully taken out of the cabinet.
|
|
||||||
|
|
||||||
comp-item-cabinet-successfully-taken = You take { THE($item) } from { THE($cabinet) }.
|
|
||||||
@@ -35,12 +35,13 @@
|
|||||||
type: UplinkBoundUserInterface
|
type: UplinkBoundUserInterface
|
||||||
- type: ItemSlots
|
- type: ItemSlots
|
||||||
slots:
|
slots:
|
||||||
pda_pen_slot:
|
pdaPenSlot:
|
||||||
item: "Pen"
|
item: "Pen"
|
||||||
whitelist:
|
whitelist:
|
||||||
tags:
|
tags:
|
||||||
- Write
|
- Write
|
||||||
pda_id_slot:
|
pdaIdSlot:
|
||||||
|
name: ID Card
|
||||||
insertSound:
|
insertSound:
|
||||||
path: /Audio/Weapons/Guns/MagIn/batrifle_magin.ogg
|
path: /Audio/Weapons/Guns/MagIn/batrifle_magin.ogg
|
||||||
ejectSound:
|
ejectSound:
|
||||||
|
|||||||
@@ -20,14 +20,17 @@
|
|||||||
- type: ItemCabinet
|
- type: ItemCabinet
|
||||||
doorSound:
|
doorSound:
|
||||||
path: /Audio/Machines/machine_switch.ogg
|
path: /Audio/Machines/machine_switch.ogg
|
||||||
whitelist:
|
|
||||||
components:
|
|
||||||
- FireExtinguisher
|
|
||||||
- type: Appearance
|
- type: Appearance
|
||||||
visuals:
|
visuals:
|
||||||
- type: ItemCabinetVisualizer
|
- type: ItemCabinetVisualizer
|
||||||
openState: open
|
openState: open
|
||||||
closedState: closed
|
closedState: closed
|
||||||
|
- type: ItemSlots
|
||||||
|
slots:
|
||||||
|
cabinetSlot:
|
||||||
|
whitelist:
|
||||||
|
components:
|
||||||
|
- FireExtinguisher
|
||||||
placement:
|
placement:
|
||||||
mode: SnapgridCenter
|
mode: SnapgridCenter
|
||||||
|
|
||||||
@@ -44,8 +47,15 @@
|
|||||||
parent: ExtinguisherCabinet
|
parent: ExtinguisherCabinet
|
||||||
suffix: Filled
|
suffix: Filled
|
||||||
components:
|
components:
|
||||||
- type: ItemCabinet
|
- type: ItemCabinet
|
||||||
spawnPrototype: FireExtinguisher
|
spawnPrototype: FireExtinguisher
|
||||||
|
- type: ItemSlots
|
||||||
|
slots:
|
||||||
|
cabinetSlot:
|
||||||
|
item: FireExtinguisher
|
||||||
|
whitelist:
|
||||||
|
components:
|
||||||
|
- FireExtinguisher
|
||||||
|
|
||||||
- type: entity
|
- type: entity
|
||||||
id: ExtinguisherCabinetFilledOpen
|
id: ExtinguisherCabinetFilledOpen
|
||||||
|
|||||||
@@ -18,14 +18,17 @@
|
|||||||
- type: ItemCabinet
|
- type: ItemCabinet
|
||||||
doorSound:
|
doorSound:
|
||||||
path: /Audio/Machines/machine_switch.ogg
|
path: /Audio/Machines/machine_switch.ogg
|
||||||
whitelist:
|
|
||||||
tags:
|
|
||||||
- FireAxe
|
|
||||||
- type: Appearance
|
- type: Appearance
|
||||||
visuals:
|
visuals:
|
||||||
- type: ItemCabinetVisualizer
|
- type: ItemCabinetVisualizer
|
||||||
closedState: glass
|
closedState: glass
|
||||||
openState: glass-up
|
openState: glass-up
|
||||||
|
- type: ItemSlots
|
||||||
|
slots:
|
||||||
|
cabinetSlot:
|
||||||
|
whitelist:
|
||||||
|
tags:
|
||||||
|
- FireAxe
|
||||||
placement:
|
placement:
|
||||||
mode: SnapgridCenter
|
mode: SnapgridCenter
|
||||||
|
|
||||||
@@ -42,8 +45,14 @@
|
|||||||
parent: FireAxeCabinet
|
parent: FireAxeCabinet
|
||||||
suffix: Filled
|
suffix: Filled
|
||||||
components:
|
components:
|
||||||
- type: ItemCabinet
|
- type: ItemCabinet
|
||||||
spawnPrototype: FireAxe
|
- type: ItemSlots
|
||||||
|
slots:
|
||||||
|
cabinetSlot:
|
||||||
|
item: FireAxe
|
||||||
|
whitelist:
|
||||||
|
tags:
|
||||||
|
- FireAxe
|
||||||
|
|
||||||
- type: entity
|
- type: entity
|
||||||
id: FireAxeCabinetFilledOpen
|
id: FireAxeCabinetFilledOpen
|
||||||
|
|||||||
Reference in New Issue
Block a user