diff --git a/Content.Shared/Containers/ItemSlot/ItemSlotsComponent.cs b/Content.Shared/Containers/ItemSlot/ItemSlotsComponent.cs
index 522929a3c5..791d09e3ef 100644
--- a/Content.Shared/Containers/ItemSlot/ItemSlotsComponent.cs
+++ b/Content.Shared/Containers/ItemSlot/ItemSlotsComponent.cs
@@ -2,6 +2,7 @@ using Content.Shared.Sound;
using Content.Shared.Whitelist;
using Robust.Shared.Audio;
using Robust.Shared.Containers;
+using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
@@ -14,6 +15,7 @@ namespace Content.Shared.Containers.ItemSlots
///
[RegisterComponent]
[Access(typeof(ItemSlotsSystem))]
+ [NetworkedComponent]
public sealed class ItemSlotsComponent : Component
{
///
@@ -42,16 +44,11 @@ namespace Content.Shared.Containers.ItemSlots
[Serializable, NetSerializable]
public sealed class ItemSlotsComponentState : ComponentState
{
- public readonly Dictionary SlotLocked;
+ public readonly Dictionary Slots;
public ItemSlotsComponentState(Dictionary slots)
{
- SlotLocked = new(slots.Count);
-
- foreach (var (key, slot) in slots)
- {
- SlotLocked[key] = slot.Locked;
- }
+ Slots = slots;
}
}
@@ -61,8 +58,17 @@ namespace Content.Shared.Containers.ItemSlots
///
[DataDefinition]
[Access(typeof(ItemSlotsSystem))]
+ [Serializable, NetSerializable]
public sealed class ItemSlot
{
+ public ItemSlot() { }
+
+ public ItemSlot(ItemSlot other)
+ {
+ CopyFrom(other);
+ }
+
+
[DataField("whitelist")]
public EntityWhitelist? Whitelist;
@@ -76,6 +82,7 @@ namespace Content.Shared.Containers.ItemSlots
/// Options used for playing the insert/eject sounds.
///
[DataField("soundOptions")]
+ [Obsolete("Use the sound specifer parameters instead")]
public AudioParams SoundOptions = AudioParams.Default;
///
@@ -99,6 +106,7 @@ namespace Content.Shared.Containers.ItemSlots
///
[DataField("startingItem", readOnly: true, customTypeSerializer: typeof(PrototypeIdSerializer))]
[Access(typeof(ItemSlotsSystem), Other = AccessPermissions.ReadWriteExecute)] // FIXME Friends
+ [NonSerialized]
public string? StartingItem;
///
@@ -156,7 +164,7 @@ namespace Content.Shared.Containers.ItemSlots
[DataField("ejectVerbText")]
public string? EjectVerbText;
- [ViewVariables]
+ [ViewVariables, NonSerialized]
public ContainerSlot? ContainerSlot = default!;
///
@@ -167,6 +175,7 @@ namespace Content.Shared.Containers.ItemSlots
/// The actual deconstruction logic is handled by the server-side EmptyOnMachineDeconstructSystem.
///
[DataField("ejectOnDeconstruct")]
+ [NonSerialized]
public bool EjectOnDeconstruct = true;
///
@@ -174,6 +183,7 @@ namespace Content.Shared.Containers.ItemSlots
/// ejected when it is broken or destroyed?
///
[DataField("ejectOnBreak")]
+ [NonSerialized]
public bool EjectOnBreak = false;
///
@@ -204,5 +214,32 @@ namespace Content.Shared.Containers.ItemSlots
///
[DataField("priority")]
public int Priority = 0;
+
+ ///
+ /// If false, errors when adding an item slot with a duplicate key are suppressed. Local==true implies that
+ /// the slot was added via client component state handling.
+ ///
+ [NonSerialized]
+ public bool Local = true;
+
+ public void CopyFrom(ItemSlot other)
+ {
+ // These fields are mutable reference types. But they generally don't get modified, so this should be fine.
+ Whitelist = other.Whitelist;
+ InsertSound = other.InsertSound;
+ EjectSound = other.EjectSound;
+
+ SoundOptions = other.SoundOptions;
+ Name = other.Name;
+ Locked = other.Locked;
+ InsertOnInteract = other.InsertOnInteract;
+ EjectOnInteract = other.EjectOnInteract;
+ EjectOnUse = other.EjectOnUse;
+ InsertVerbText = other.InsertVerbText;
+ EjectVerbText = other.EjectVerbText;
+ WhitelistFailPopup = other.WhitelistFailPopup;
+ Swap = other.Swap;
+ Priority = other.Priority;
+ }
}
}
diff --git a/Content.Shared/Containers/ItemSlot/ItemSlotsSystem.cs b/Content.Shared/Containers/ItemSlot/ItemSlotsSystem.cs
index 49ce16f4aa..f4305e569e 100644
--- a/Content.Shared/Containers/ItemSlot/ItemSlotsSystem.cs
+++ b/Content.Shared/Containers/ItemSlot/ItemSlotsSystem.cs
@@ -13,6 +13,8 @@ using Robust.Shared.Player;
using Robust.Shared.Timing;
using System.Diagnostics.CodeAnalysis;
using Content.Shared.Destructible;
+using Robust.Shared.Network;
+using Robust.Shared.Utility;
namespace Content.Shared.Containers.ItemSlots
{
@@ -25,6 +27,7 @@ namespace Content.Shared.Containers.ItemSlots
[Dependency] private readonly SharedContainerSystem _containers = default!;
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
[Dependency] private readonly SharedHandsSystem _handsSystem = default!;
+ [Dependency] private readonly INetManager _netMan = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
public override void Initialize()
@@ -81,13 +84,23 @@ namespace Content.Shared.Containers.ItemSlots
/// Given a new item slot, store it in the and ensure the slot has an item
/// container.
///
- public void AddItemSlot(EntityUid uid, string id, ItemSlot slot)
+ public void AddItemSlot(EntityUid uid, string id, ItemSlot slot, ItemSlotsComponent? itemSlots = null)
{
- var itemSlots = EntityManager.EnsureComponent(uid);
+ itemSlots ??= EntityManager.EnsureComponent(uid);
+ DebugTools.Assert(itemSlots.Owner == uid);
+
+ if (itemSlots.Slots.TryGetValue(id, out var existing))
+ {
+ if (existing.Local)
+ Logger.Error($"Duplicate item slot key. Entity: {EntityManager.GetComponent(itemSlots.Owner).EntityName} ({uid}), key: {id}");
+ else
+ // server state takes priority
+ slot.CopyFrom(existing);
+ }
+
slot.ContainerSlot = _containers.EnsureContainer(itemSlots.Owner, id);
- if (itemSlots.Slots.ContainsKey(id))
- Logger.Error($"Duplicate item slot key. Entity: {EntityManager.GetComponent(itemSlots.Owner).EntityName} ({uid}), key: {id}");
itemSlots.Slots[id] = slot;
+ Dirty(itemSlots);
}
///
@@ -110,6 +123,8 @@ namespace Content.Shared.Containers.ItemSlots
if (itemSlots.Slots.Count == 0)
EntityManager.RemoveComponent(uid, itemSlots);
+ else
+ Dirty(itemSlots);
}
#endregion
@@ -128,7 +143,7 @@ namespace Content.Shared.Containers.ItemSlots
continue;
args.Handled = true;
- TryEjectToHands(uid, slot, args.User);
+ TryEjectToHands(uid, slot, args.User, true);
break;
}
}
@@ -147,7 +162,7 @@ namespace Content.Shared.Containers.ItemSlots
continue;
args.Handled = true;
- TryEjectToHands(uid, slot, args.User);
+ TryEjectToHands(uid, slot, args.User, true);
break;
}
}
@@ -219,10 +234,10 @@ namespace Content.Shared.Containers.ItemSlots
{
if (sound == null || !_gameTiming.IsFirstTimePredicted)
return;
+
+ var filter = Filter.Pvs(uid, entityManager: EntityManager);
- var filter = Filter.Pvs(uid);
-
- if (excluded != null)
+ if (excluded != null && _netMan.IsServer)
filter = filter.RemoveWhereAttachedEntity(entity => entity == excluded.Value);
SoundSystem.Play(sound.GetSound(), filter, uid, audioParams);
@@ -516,7 +531,7 @@ namespace Content.Shared.Containers.ItemSlots
return;
if (args.TryEject && slot.HasItem)
- TryEjectToHands(uid, slot, args.Session.AttachedEntity);
+ TryEjectToHands(uid, slot, args.Session.AttachedEntity, false);
else if (args.TryInsert && !slot.HasItem && args.Session.AttachedEntity is EntityUid user)
TryInsertFromHand(uid, slot, user);
}
@@ -583,9 +598,25 @@ namespace Content.Shared.Containers.ItemSlots
if (args.Current is not ItemSlotsComponentState state)
return;
- foreach (var (id, locked) in state.SlotLocked)
+ foreach (var (key, slot) in component.Slots)
{
- component.Slots[id].Locked = locked;
+ if (!state.Slots.ContainsKey(key))
+ RemoveItemSlot(uid, slot, component);
+ }
+
+ foreach (var (serverKey, serverSlot) in state.Slots)
+ {
+ if (component.Slots.TryGetValue(serverKey, out var itemSlot))
+ {
+ itemSlot.CopyFrom(serverSlot);
+ itemSlot.ContainerSlot = _containers.EnsureContainer(uid, serverKey);
+ }
+ else
+ {
+ var slot = new ItemSlot(serverSlot);
+ slot.Local = false;
+ AddItemSlot(uid, serverKey, slot);
+ }
}
}