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); + } } }