diff --git a/Content.Client/Clothing/ClientClothingSystem.cs b/Content.Client/Clothing/ClientClothingSystem.cs index 065179c714..8d53e90e34 100644 --- a/Content.Client/Clothing/ClientClothingSystem.cs +++ b/Content.Client/Clothing/ClientClothingSystem.cs @@ -59,7 +59,7 @@ public sealed class ClientClothingSystem : ClothingSystem base.Initialize(); SubscribeLocalEvent(OnGetVisuals); - SubscribeLocalEvent(OnInventoryTemplateUpdated); + SubscribeLocalEvent(OnInventoryTemplateUpdated); SubscribeLocalEvent(OnVisualsChanged); SubscribeLocalEvent(OnDidUnequip); @@ -82,20 +82,19 @@ public sealed class ClientClothingSystem : ClothingSystem } } - private void OnInventoryTemplateUpdated(Entity ent, ref InventoryTemplateUpdated args) + private void OnInventoryTemplateUpdated(Entity ent, ref InventoryTemplateUpdated args) { - UpdateAllSlots(ent.Owner, clothing: ent.Comp); + UpdateAllSlots(ent.Owner, ent.Comp); } private void UpdateAllSlots( EntityUid uid, - InventoryComponent? inventoryComponent = null, - ClothingComponent? clothing = null) + InventoryComponent? inventoryComponent = null) { var enumerator = _inventorySystem.GetSlotEnumerator((uid, inventoryComponent)); while (enumerator.NextItem(out var item, out var slot)) { - RenderEquipment(uid, item, slot.Name, inventoryComponent, clothingComponent: clothing); + RenderEquipment(uid, item, slot.Name, inventoryComponent); } } diff --git a/Content.Client/Inventory/ClientInventorySystem.cs b/Content.Client/Inventory/ClientInventorySystem.cs index de58077e44..1f926b42a1 100644 --- a/Content.Client/Inventory/ClientInventorySystem.cs +++ b/Content.Client/Inventory/ClientInventorySystem.cs @@ -1,3 +1,4 @@ +using System.Linq; using Content.Client.Clothing; using Content.Client.Examine; using Content.Client.Verbs.UI; @@ -11,6 +12,7 @@ using Robust.Client.UserInterface; using Robust.Shared.Containers; using Robust.Shared.Input.Binding; using Robust.Shared.Player; +using Robust.Shared.Timing; namespace Content.Client.Inventory { @@ -19,7 +21,7 @@ namespace Content.Client.Inventory { [Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IUserInterfaceManager _ui = default!; - + [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly ClientClothingSystem _clothingVisualsSystem = default!; [Dependency] private readonly ExamineSystem _examine = default!; @@ -91,6 +93,14 @@ namespace Content.Client.Inventory private void OnShutdown(EntityUid uid, InventoryComponent component, ComponentShutdown args) { + if (TryComp(uid, out InventorySlotsComponent? inventorySlots)) + { + foreach (var slot in component.Slots) + { + TryRemoveSlotData((uid, inventorySlots), (SlotData)slot); + } + } + if (uid == _playerManager.LocalEntity) OnUnlinkInventory?.Invoke(); } @@ -102,23 +112,6 @@ namespace Content.Client.Inventory private void OnPlayerAttached(EntityUid uid, InventorySlotsComponent component, LocalPlayerAttachedEvent args) { - if (TryGetSlots(uid, out var definitions)) - { - foreach (var definition in definitions) - { - if (!TryGetSlotContainer(uid, definition.Name, out var container, out _)) - continue; - - if (!component.SlotData.TryGetValue(definition.Name, out var data)) - { - data = new SlotData(definition); - component.SlotData[definition.Name] = data; - } - - data.Container = container; - } - } - OnLinkInventorySlots?.Invoke(uid, component); } @@ -128,20 +121,6 @@ namespace Content.Client.Inventory base.Shutdown(); } - protected override void OnInit(EntityUid uid, InventoryComponent component, ComponentInit args) - { - base.OnInit(uid, component, args); - _clothingVisualsSystem.InitClothing(uid, component); - - if (!TryComp(uid, out InventorySlotsComponent? inventorySlots)) - return; - - foreach (var slot in component.Slots) - { - TryAddSlotDef(uid, inventorySlots, slot); - } - } - public void ReloadInventory(InventorySlotsComponent? component = null) { var player = _playerManager.LocalEntity; @@ -165,7 +144,10 @@ namespace Content.Client.Inventory public void UpdateSlot(EntityUid owner, InventorySlotsComponent component, string slotName, bool? blocked = null, bool? highlight = null) { - var oldData = component.SlotData[slotName]; + // The slot might have been removed when changing templates, which can cause items to be dropped. + if (!component.SlotData.TryGetValue(slotName, out var oldData)) + return; + var newHighlight = oldData.Highlighted; var newBlocked = oldData.Blocked; @@ -181,14 +163,28 @@ namespace Content.Client.Inventory EntitySlotUpdate?.Invoke(newData); } - public bool TryAddSlotDef(EntityUid owner, InventorySlotsComponent component, SlotDefinition newSlotDef) + public bool TryAddSlotData(Entity ent, SlotData newSlotData) { - SlotData newSlotData = newSlotDef; //convert to slotData - if (!component.SlotData.TryAdd(newSlotDef.Name, newSlotData)) + if (!ent.Comp.SlotData.TryAdd(newSlotData.SlotName, newSlotData)) return false; - if (owner == _playerManager.LocalEntity) + if (TryGetSlotContainer(ent.Owner, newSlotData.SlotName, out var newContainer, out _)) + ent.Comp.SlotData[newSlotData.SlotName].Container = newContainer; + + if (ent.Owner == _playerManager.LocalEntity) OnSlotAdded?.Invoke(newSlotData); + + return true; + } + + public bool TryRemoveSlotData(Entity ent, SlotData removedSlotData) + { + if (!ent.Comp.SlotData.Remove(removedSlotData.SlotName)) + return false; + + if (ent.Owner == _playerManager.LocalEntity) + OnSlotRemoved?.Invoke(removedSlotData); + return true; } @@ -239,33 +235,52 @@ namespace Content.Client.Inventory { base.UpdateInventoryTemplate(ent); - if (TryComp(ent, out InventorySlotsComponent? inventorySlots)) + if (!TryComp(ent, out var inventorySlots)) + return; + + List slotDataToRemove = new(); // don't modify dict while iterating + + foreach (var slotData in inventorySlots.SlotData.Values) { - foreach (var slot in ent.Comp.Slots) - { - if (inventorySlots.SlotData.TryGetValue(slot.Name, out var slotData)) - slotData.SlotDef = slot; - } + if (!ent.Comp.Slots.Any(s => s.Name == slotData.SlotName)) + slotDataToRemove.Add(slotData); } + + // remove slots that are no longer in the new template + foreach (var slotData in slotDataToRemove) + { + TryRemoveSlotData((ent.Owner, inventorySlots), slotData); + } + + // update existing slots or add them if they don't exist yet + foreach (var slot in ent.Comp.Slots) + { + if (inventorySlots.SlotData.TryGetValue(slot.Name, out var slotData)) + slotData.SlotDef = slot; + else + TryAddSlotData((ent.Owner, inventorySlots), (SlotData)slot); + } + + _clothingVisualsSystem.InitClothing(ent, ent.Comp); + if (ent.Owner == _playerManager.LocalEntity) + ReloadInventory(inventorySlots); } public sealed class SlotData { - public SlotDefinition SlotDef; - public EntityUid? HeldEntity => Container?.ContainedEntity; - public bool Blocked; - public bool Highlighted; - - [ViewVariables] - public ContainerSlot? Container; - public bool HasSlotGroup => SlotDef.SlotGroup != "Default"; - public Vector2i ButtonOffset => SlotDef.UIWindowPosition; - public string SlotName => SlotDef.Name; - public bool ShowInWindow => SlotDef.ShowInWindow; - public string SlotGroup => SlotDef.SlotGroup; - public string SlotDisplayName => SlotDef.DisplayName; - public string TextureName => "Slots/" + SlotDef.TextureName; - public string FullTextureName => SlotDef.FullTextureName; + [ViewVariables] public SlotDefinition SlotDef; + [ViewVariables] public EntityUid? HeldEntity => Container?.ContainedEntity; + [ViewVariables] public bool Blocked; + [ViewVariables] public bool Highlighted; + [ViewVariables] public ContainerSlot? Container; + [ViewVariables] public bool HasSlotGroup => SlotDef.SlotGroup != "Default"; + [ViewVariables] public Vector2i ButtonOffset => SlotDef.UIWindowPosition; + [ViewVariables] public string SlotName => SlotDef.Name; + [ViewVariables] public bool ShowInWindow => SlotDef.ShowInWindow; + [ViewVariables] public string SlotGroup => SlotDef.SlotGroup; + [ViewVariables] public string SlotDisplayName => SlotDef.DisplayName; + [ViewVariables] public string TextureName => "Slots/" + SlotDef.TextureName; + [ViewVariables] public string FullTextureName => SlotDef.FullTextureName; public SlotData(SlotDefinition slotDef, ContainerSlot? container = null, bool highlighted = false, bool blocked = false) diff --git a/Content.Shared/DisplacementMap/DisplacementData.cs b/Content.Shared/DisplacementMap/DisplacementData.cs index 6564f720a8..79f89a1d25 100644 --- a/Content.Shared/DisplacementMap/DisplacementData.cs +++ b/Content.Shared/DisplacementMap/DisplacementData.cs @@ -1,6 +1,8 @@ +using Robust.Shared.Serialization; + namespace Content.Shared.DisplacementMap; -[DataDefinition] +[DataDefinition, Serializable, NetSerializable] public sealed partial class DisplacementData { /// diff --git a/Content.Shared/Inventory/InventoryComponent.cs b/Content.Shared/Inventory/InventoryComponent.cs index 61e0114ff2..664fdd6d56 100644 --- a/Content.Shared/Inventory/InventoryComponent.cs +++ b/Content.Shared/Inventory/InventoryComponent.cs @@ -1,7 +1,7 @@ using Content.Shared.DisplacementMap; using Robust.Shared.Containers; using Robust.Shared.GameStates; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; +using Robust.Shared.Prototypes; namespace Content.Shared.Inventory; @@ -10,28 +10,46 @@ namespace Content.Shared.Inventory; [AutoGenerateComponentState(true)] public sealed partial class InventoryComponent : Component { - [DataField("templateId", customTypeSerializer: typeof(PrototypeIdSerializer))] - [AutoNetworkedField] - public string TemplateId { get; set; } = "human"; + /// + /// The template defining how the inventory layout will look like. + /// + [DataField, AutoNetworkedField] + [ViewVariables] // use the API method + public ProtoId TemplateId = "human"; - [DataField("speciesId")] public string? SpeciesId { get; set; } + /// + /// For setting the TemplateId. + /// + [ViewVariables(VVAccess.ReadWrite)] + public ProtoId TemplateIdVV + { + get => TemplateId; + set => IoCManager.Resolve().System().SetTemplateId((Owner, this), value); + } + [DataField, AutoNetworkedField] + public string? SpeciesId; + + + [ViewVariables] public SlotDefinition[] Slots = Array.Empty(); + + [ViewVariables] public ContainerSlot[] Containers = Array.Empty(); - [DataField] + [DataField, AutoNetworkedField] public Dictionary Displacements = new(); /// /// Alternate displacement maps, which if available, will be selected for the player of the appropriate gender. /// - [DataField] + [DataField, AutoNetworkedField] public Dictionary FemaleDisplacements = new(); /// /// Alternate displacement maps, which if available, will be selected for the player of the appropriate gender. /// - [DataField] + [DataField, AutoNetworkedField] public Dictionary MaleDisplacements = new(); } diff --git a/Content.Shared/Inventory/InventorySystem.Slots.cs b/Content.Shared/Inventory/InventorySystem.Slots.cs index 04d58c1cd5..e9fb62f2ad 100644 --- a/Content.Shared/Inventory/InventorySystem.Slots.cs +++ b/Content.Shared/Inventory/InventorySystem.Slots.cs @@ -43,7 +43,7 @@ public partial class InventorySystem : EntitySystem if (!TryComp(item, out var required)) continue; - if ((((IClothingSlots) required).Slots & slot.SlotFlags) == 0x0) + if ((((IClothingSlots)required).Slots & slot.SlotFlags) == 0x0) continue; target = (item, required); @@ -55,20 +55,9 @@ public partial class InventorySystem : EntitySystem return false; } - protected virtual void OnInit(EntityUid uid, InventoryComponent component, ComponentInit args) + private void OnInit(Entity ent, ref ComponentInit args) { - if (!_prototypeManager.TryIndex(component.TemplateId, out InventoryTemplatePrototype? invTemplate)) - return; - - component.Slots = invTemplate.Slots; - component.Containers = new ContainerSlot[component.Slots.Length]; - for (var i = 0; i < component.Containers.Length; i++) - { - var slot = component.Slots[i]; - var container = _containerSystem.EnsureContainer(uid, slot.Name); - container.OccludesLight = false; - component.Containers[i] = container; - } + UpdateInventoryTemplate(ent); } private void AfterAutoState(Entity ent, ref AfterAutoHandleStateEvent args) @@ -78,18 +67,31 @@ public partial class InventorySystem : EntitySystem protected virtual void UpdateInventoryTemplate(Entity ent) { - if (ent.Comp.LifeStage < ComponentLifeStage.Initialized) + if (!_prototypeManager.Resolve(ent.Comp.TemplateId, out var invTemplate)) return; - if (!_prototypeManager.TryIndex(ent.Comp.TemplateId, out InventoryTemplatePrototype? invTemplate)) - return; + // Remove any containers that aren't in the new template. + foreach (var container in ent.Comp.Containers) + { + if (invTemplate.Slots.Any(s => s.Name == container.ID)) + continue; - DebugTools.Assert(ent.Comp.Slots.Length == invTemplate.Slots.Length); + // Empty container before deletion so the contents don't get deleted. + // For cases when we update the template while items are already worn. + _containerSystem.EmptyContainer(container); + _containerSystem.ShutdownContainer(container); + } + // Ensure the containers from the template. ent.Comp.Slots = invTemplate.Slots; - - var ev = new InventoryTemplateUpdated(); - RaiseLocalEvent(ent, ref ev); + ent.Comp.Containers = new ContainerSlot[ent.Comp.Slots.Length]; + for (var i = 0; i < ent.Comp.Containers.Length; i++) + { + var slot = ent.Comp.Slots[i]; + var container = _containerSystem.EnsureContainer(ent.Owner, slot.Name); + container.OccludesLight = false; + ent.Comp.Containers[i] = container; + } } private void OnOpenSlotStorage(OpenSlotStorageNetworkMessage ev, EntitySessionEventArgs args) @@ -195,27 +197,21 @@ public partial class InventorySystem : EntitySystem } /// - /// Change the inventory template ID an entity is using. The new template must be compatible. + /// Change the inventory template ID an entity is using + /// and drop any item that does not have a slot according to the new template. + /// This will update the client-side UI accordingly. /// /// - /// - /// For an inventory template to be compatible with another, it must have exactly the same slot names. - /// All other changes are rejected. - /// /// /// The entity to update. /// The ID of the new inventory template prototype. - /// - /// Thrown if the new template is not compatible with the existing one. - /// public void SetTemplateId(Entity ent, ProtoId newTemplate) { - var newPrototype = _prototypeManager.Index(newTemplate); - - if (!newPrototype.Slots.Select(x => x.Name).SequenceEqual(ent.Comp.Slots.Select(x => x.Name))) - throw new ArgumentException("Incompatible inventory template!"); + if (ent.Comp.TemplateId == newTemplate) + return; ent.Comp.TemplateId = newTemplate; + UpdateInventoryTemplate(ent); Dirty(ent); } @@ -231,12 +227,12 @@ public partial class InventorySystem : EntitySystem private int _nextIdx = 0; public static InventorySlotEnumerator Empty = new(Array.Empty(), Array.Empty()); - public InventorySlotEnumerator(InventoryComponent inventory, SlotFlags flags = SlotFlags.All) + public InventorySlotEnumerator(InventoryComponent inventory, SlotFlags flags = SlotFlags.All) : this(inventory.Slots, inventory.Containers, flags) { } - public InventorySlotEnumerator(SlotDefinition[] slots, ContainerSlot[] containers, SlotFlags flags = SlotFlags.All) + public InventorySlotEnumerator(SlotDefinition[] slots, ContainerSlot[] containers, SlotFlags flags = SlotFlags.All) { DebugTools.Assert(flags != SlotFlags.NONE); DebugTools.AssertEqual(slots.Length, containers.Length);