From 470e4f8bdcab48b16e95a36eb1bba73b70240055 Mon Sep 17 00:00:00 2001 From: Leon Friedrich <60421075+ElectroJr@users.noreply.github.com> Date: Mon, 7 Feb 2022 16:37:57 +1300 Subject: [PATCH] Add support for multi-layer in-hand and clothing sprites (#6252) --- Content.Client/Clothing/ClothingSystem.cs | 221 ++++++++++++------ Content.Client/Hands/HandsComponent.cs | 7 +- Content.Client/Hands/HandsVisualizer.cs | 62 ----- Content.Client/Hands/Systems/HandsSystem.cs | 100 +++++++- .../Inventory/ClientInventoryComponent.cs | 6 + Content.Client/Items/Systems/ItemSystem.cs | 110 +++++++++ Content.Server/Items/ItemSystem.cs | 8 + Content.Shared/Clothing/ClothingEvents.cs | 60 +++++ .../Components/HandVirtualItemComponent.cs | 3 +- .../Hands/Components/SharedHandsComponent.cs | 44 +--- Content.Shared/Hands/HandEvents.cs | 54 +++++ Content.Shared/Hands/SharedHandsSystem.cs | 46 +--- .../Inventory/InventorySystem.Equip.cs | 3 + Content.Shared/Item/SharedItemComponent.cs | 52 ++--- .../{ItemSystem.cs => SharedItemSystem.cs} | 13 +- .../Prototypes/Entities/Mobs/NPCs/animals.yml | 1 - .../Entities/Mobs/Species/human.yml | 1 - 17 files changed, 529 insertions(+), 262 deletions(-) delete mode 100644 Content.Client/Hands/HandsVisualizer.cs create mode 100644 Content.Client/Items/Systems/ItemSystem.cs create mode 100644 Content.Server/Items/ItemSystem.cs create mode 100644 Content.Shared/Clothing/ClothingEvents.cs rename Content.Shared/Item/{ItemSystem.cs => SharedItemSystem.cs} (88%) diff --git a/Content.Client/Clothing/ClothingSystem.cs b/Content.Client/Clothing/ClothingSystem.cs index e630bae637..6c57c255b3 100644 --- a/Content.Client/Clothing/ClothingSystem.cs +++ b/Content.Client/Clothing/ClothingSystem.cs @@ -1,4 +1,6 @@ -using System.Collections.Generic; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; using Content.Client.Inventory; using Content.Shared.CharacterAppearance; using Content.Shared.Clothing; @@ -9,8 +11,9 @@ using Robust.Client.GameObjects; using Robust.Client.Graphics; using Robust.Client.ResourceManagement; using Robust.Shared.GameObjects; -using Robust.Shared.GameStates; using Robust.Shared.IoC; +using Robust.Shared.Log; +using static Robust.Shared.GameObjects.SharedSpriteComponent; namespace Content.Client.Clothing; @@ -48,11 +51,94 @@ public class ClothingSystem : EntitySystem SubscribeLocalEvent(OnGotEquipped); SubscribeLocalEvent(OnGotUnequipped); - SubscribeLocalEvent(OnPrefixChanged); + + SubscribeLocalEvent(OnGetVisuals); + + SubscribeLocalEvent(OnVisualsChanged); SubscribeLocalEvent(OnDidUnequip); } - private void OnPrefixChanged(EntityUid uid, ClientInventoryComponent component, ItemPrefixChangeEvent args) + private void OnGetVisuals(EntityUid uid, SharedItemComponent item, GetEquipmentVisualsEvent args) + { + if (!TryComp(args.Equipee, out ClientInventoryComponent? inventory)) + return; + + List? layers = null; + + // first attempt to get species specific data. + if (inventory.SpeciesId != null) + item.ClothingVisuals.TryGetValue($"{args.Slot}-{inventory.SpeciesId}", out layers); + + // if that returned nothing, attempt to find generic data + if (layers == null && !item.ClothingVisuals.TryGetValue(args.Slot, out layers)) + { + // No generic data either. Attempt to generate defaults from the item's RSI & item-prefixes + if (!TryGetDefaultVisuals(uid, item, args.Slot, inventory.SpeciesId, out layers)) + return; + } + + // add each layer to the visuals + var i = 0; + foreach (var layer in layers) + { + var key = layer.MapKeys?.FirstOrDefault(); + if (key == null) + { + key = i == 0 ? args.Slot : $"{args.Slot}-{i}"; + i++; + } + + args.Layers.Add((key, layer)); + } + } + + /// + /// If no explicit clothing visuals were specified, this attempts to populate with default values. + /// + /// + /// Useful for lazily adding clothing sprites without modifying yaml. And for backwards compatibility. + /// + private bool TryGetDefaultVisuals(EntityUid uid, SharedItemComponent item, string slot, string? speciesId, + [NotNullWhen(true)] out List? layers) + { + layers = null; + + RSI? rsi = null; + + if (item.RsiPath != null) + rsi = _cache.GetResource(TextureRoot / item.RsiPath).RSI; + else if (TryComp(uid, out SpriteComponent? sprite)) + rsi = sprite.BaseRSI; + + if (rsi == null || rsi.Path == null) + return false; + + var correctedSlot = slot; + TemporarySlotMap.TryGetValue(correctedSlot, out correctedSlot); + + var state = (item.EquippedPrefix == null) + ? $"equipped-{correctedSlot}" + : $"{item.EquippedPrefix}-equipped-{correctedSlot}"; + + // species specific + if (speciesId != null && rsi.TryGetState($"{state}-{speciesId}", out _)) + { + state = $"{state}-{speciesId}"; + } + else if (!rsi.TryGetState(state, out _)) + { + return false; + } + + var layer = PrototypeLayerData.New(); + layer.RsiPath = rsi.Path.ToString(); + layer.State = state; + layers = new() { layer }; + + return true; + } + + private void OnVisualsChanged(EntityUid uid, ClientInventoryComponent component, VisualsChangedEvent args) { if (!TryComp(args.Item, out ClothingComponent? clothing) || clothing.InSlot == null) return; @@ -67,7 +153,19 @@ public class ClothingSystem : EntitySystem private void OnDidUnequip(EntityUid uid, SpriteComponent component, DidUnequipEvent args) { - component.LayerSetVisible(args.Slot, false); + if (!TryComp(uid, out ClientInventoryComponent? inventory) || !TryComp(uid, out SpriteComponent? sprite)) + return; + + if (!inventory.VisualLayerKeys.TryGetValue(args.Slot, out var revealedLayers)) + return; + + // Remove old layers. We could also just set them to invisible, but as items may add arbitrary layers, this + // may eventually bloat the player with lots of invisible layers. + foreach (var layer in revealedLayers) + { + sprite.RemoveLayer(layer); + } + revealedLayers.Clear(); } public void InitClothing(EntityUid uid, ClientInventoryComponent? component = null, SpriteComponent? sprite = null) @@ -76,8 +174,6 @@ public class ClothingSystem : EntitySystem foreach (var slot in slots) { - sprite.LayerMapReserveBlank(slot.Name); - if (!_inventorySystem.TryGetSlotContainer(uid, slot.Name, out var containerSlot, out _, component) || !containerSlot.ContainedEntity.HasValue) continue; @@ -89,55 +185,15 @@ public class ClothingSystem : EntitySystem { component.InSlot = args.Slot; - if (!TryComp(args.Equipee, out var sprite) || !TryComp(args.Equipee, out var invComp)) - { - return; - } - - var data = GetEquippedStateInfo(args.Equipment, args.Slot, invComp.SpeciesId, component); - if (data != null) - { - var (rsi, state) = data.Value; - sprite.LayerSetVisible(args.Slot, true); - sprite.LayerSetState(args.Slot, state, rsi); - sprite.LayerSetAutoAnimated(args.Slot, true); - - if (args.Slot == "jumpsuit" && sprite.LayerMapTryGet(HumanoidVisualLayers.StencilMask, out _)) - { - sprite.LayerSetState(HumanoidVisualLayers.StencilMask, component.FemaleMask switch - { - FemaleClothingMask.NoMask => "female_none", - FemaleClothingMask.UniformTop => "female_top", - _ => "female_full", - }); - } - - return; - } - - - sprite.LayerSetVisible(args.Slot, false); + RenderEquipment(args.Equipee, uid, args.Slot, clothingComponent: component); } - private void RenderEquipment(EntityUid uid, EntityUid equipment, string slot, - ClientInventoryComponent? inventoryComponent = null, SpriteComponent? sprite = null, ClothingComponent? clothingComponent = null) + private void RenderEquipment(EntityUid equipee, EntityUid equipment, string slot, + ClientInventoryComponent? inventory = null, SpriteComponent? sprite = null, ClothingComponent? clothingComponent = null) { - if(!Resolve(uid, ref inventoryComponent, ref sprite)) + if(!Resolve(equipee, ref inventory, ref sprite) || !Resolve(equipment, ref clothingComponent, false)) return; - if (!Resolve(equipment, ref clothingComponent, false)) - { - sprite.LayerSetVisible(slot, false); - return; - } - - var data = GetEquippedStateInfo(equipment, slot, inventoryComponent.SpeciesId, clothingComponent); - if (data == null) return; - var (rsi, state) = data.Value; - sprite.LayerSetVisible(slot, true); - sprite.LayerSetState(slot, state, rsi); - sprite.LayerSetAutoAnimated(slot, true); - if (slot == "jumpsuit" && sprite.LayerMapTryGet(HumanoidVisualLayers.StencilMask, out _)) { sprite.LayerSetState(HumanoidVisualLayers.StencilMask, clothingComponent.FemaleMask switch @@ -147,34 +203,55 @@ public class ClothingSystem : EntitySystem _ => "female_full", }); } - } - public (RSI rsi, RSI.StateId stateId)? GetEquippedStateInfo(EntityUid uid, string slot, string? speciesId=null, ClothingComponent? component = null) - { - if (!Resolve(uid, ref component)) - return null; - - if (component.RsiPath == null) - return null; - - var rsi = _cache.GetResource(SharedSpriteComponent.TextureRoot / component.RsiPath).RSI; - var correctedSlot = slot; - TemporarySlotMap.TryGetValue(correctedSlot, out correctedSlot); - var stateId = component.EquippedPrefix != null ? $"{component.EquippedPrefix}-equipped-{correctedSlot}" : $"equipped-{correctedSlot}"; - if (speciesId != null) + // Remove old layers. We could also just set them to invisible, but as items may add arbitrary layers, this + // may eventually bloat the player with lots of invisible layers. + if (inventory.VisualLayerKeys.TryGetValue(slot, out var revealedLayers)) { - var speciesState = $"{stateId}-{speciesId}"; - if (rsi.TryGetState(speciesState, out _)) + foreach (var key in revealedLayers) { - return (rsi, speciesState); + sprite.RemoveLayer(key); } + revealedLayers.Clear(); } - - if (rsi.TryGetState(stateId, out _)) + else { - return (rsi, stateId); + revealedLayers = new(); + inventory.VisualLayerKeys[slot] = revealedLayers; } - return null; + var ev = new GetEquipmentVisualsEvent(equipee, slot); + RaiseLocalEvent(equipment, ev, false); + + if (ev.Layers.Count == 0) + { + RaiseLocalEvent(equipment, new EquipmentVisualsUpdatedEvent(equipee, slot, revealedLayers)); + return; + } + + // add the new layers + foreach (var (key, layerData) in ev.Layers) + { + if (!revealedLayers.Add(key)) + { + Logger.Warning($"Duplicate key for clothing visuals: {key}. Are multiple components attempting to modify the same layer? Equipment: {ToPrettyString(equipment)}"); + continue; + } + + var index = sprite.LayerMapReserveBlank(key); + + // In case no RSI is given, use the item's base RSI as a default. This cuts down on a lot of unnecessary yaml entries. + if (layerData.RsiPath == null + && layerData.TexturePath == null + && sprite[index].Rsi == null + && TryComp(equipment, out SpriteComponent? clothingSprite)) + { + sprite.LayerSetRSI(index, clothingSprite.BaseRSI); + } + + sprite.LayerSetData(index, layerData); + } + + RaiseLocalEvent(equipment, new EquipmentVisualsUpdatedEvent(equipee, slot, revealedLayers)); } } diff --git a/Content.Client/Hands/HandsComponent.cs b/Content.Client/Hands/HandsComponent.cs index 2927ba97bc..c2c8cde42b 100644 --- a/Content.Client/Hands/HandsComponent.cs +++ b/Content.Client/Hands/HandsComponent.cs @@ -1,7 +1,7 @@ using Content.Shared.Hands.Components; using Robust.Shared.Analyzers; using Robust.Shared.GameObjects; -using Robust.Shared.IoC; +using System.Collections.Generic; namespace Content.Client.Hands { @@ -11,5 +11,10 @@ namespace Content.Client.Hands public class HandsComponent : SharedHandsComponent { public HandsGui? Gui { get; set; } + + /// + /// Data about the current sprite layers that the hand is contributing to the owner entity. Used for sprite in-hands. + /// + public readonly Dictionary> RevealedLayers = new(); } } diff --git a/Content.Client/Hands/HandsVisualizer.cs b/Content.Client/Hands/HandsVisualizer.cs deleted file mode 100644 index 38704a05d1..0000000000 --- a/Content.Client/Hands/HandsVisualizer.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System; -using Content.Shared.Hands.Components; -using JetBrains.Annotations; -using Robust.Client.GameObjects; -using Robust.Client.ResourceManagement; -using Robust.Shared.GameObjects; -using Robust.Shared.IoC; - -namespace Content.Client.Hands -{ - [UsedImplicitly] - public class HandsVisualizer : AppearanceVisualizer - { - public override void OnChangeData(AppearanceComponent component) - { - base.OnChangeData(component); - - var entities = IoCManager.Resolve(); - if (!entities.TryGetComponent(component.Owner, out var sprite)) return; - if (!component.TryGetData(HandsVisuals.VisualState, out HandsVisualState visualState)) return; - - foreach (HandLocation location in Enum.GetValues(typeof(HandLocation))) - { - var layerKey = LocationToLayerKey(location); - if (sprite.LayerMapTryGet(layerKey, out var layer)) - { - sprite.RemoveLayer(layer); - sprite.LayerMapRemove(layer); - } - } - - var resourceCache = IoCManager.Resolve(); - var hands = visualState.Hands; - - foreach (var hand in hands) - { - var rsi = resourceCache.GetResource(SharedSpriteComponent.TextureRoot / hand.RsiPath).RSI; - - var state = $"inhand-{hand.Location.ToString().ToLowerInvariant()}"; - if (hand.EquippedPrefix != null) - state = $"{hand.EquippedPrefix}-" + state; - - if (rsi.TryGetState(state, out var _)) - { - var layerKey = LocationToLayerKey(hand.Location); - sprite.LayerMapReserveBlank(layerKey); - - var layer = sprite.LayerMapGet(layerKey); - sprite.LayerSetVisible(layer, true); - sprite.LayerSetRSI(layer, rsi); - sprite.LayerSetColor(layer, hand.Color); - sprite.LayerSetState(layer, state); - } - } - } - - private string LocationToLayerKey(HandLocation location) - { - return location.ToString(); - } - } -} diff --git a/Content.Client/Hands/Systems/HandsSystem.cs b/Content.Client/Hands/Systems/HandsSystem.cs index fcc7fe3722..7f3e236629 100644 --- a/Content.Client/Hands/Systems/HandsSystem.cs +++ b/Content.Client/Hands/Systems/HandsSystem.cs @@ -4,6 +4,7 @@ using Content.Client.Animations; using Content.Client.HUD; using Content.Shared.Hands; using Content.Shared.Hands.Components; +using Content.Shared.Item; using JetBrains.Annotations; using Robust.Client.GameObjects; using Robust.Client.Player; @@ -11,6 +12,7 @@ using Robust.Shared.Containers; using Robust.Shared.GameObjects; using Robust.Shared.GameStates; using Robust.Shared.IoC; +using Robust.Shared.Log; using Robust.Shared.Map; using Robust.Shared.Maths; using Robust.Shared.Timing; @@ -32,6 +34,7 @@ namespace Content.Client.Hands SubscribeLocalEvent(HandlePlayerDetached); SubscribeLocalEvent(HandleCompRemove); SubscribeLocalEvent(HandleComponentState); + SubscribeLocalEvent(OnVisualsChanged); SubscribeNetworkEvent(HandlePickupAnimation); } @@ -180,6 +183,95 @@ namespace Content.Client.Hands RaiseNetworkEvent(new ActivateInHandMsg(handName)); } + #region visuals + protected override void HandleContainerModified(EntityUid uid, SharedHandsComponent handComp, ContainerModifiedMessage args) + { + if (handComp.TryGetHand(args.Container.ID, out var hand)) + { + UpdateHandVisuals(uid, args.Entity, hand); + } + } + + /// + /// Update the players sprite with new in-hand visuals. + /// + private void UpdateHandVisuals(EntityUid uid, EntityUid held, Hand hand, HandsComponent? handComp = null, SpriteComponent? sprite = null) + { + if (!Resolve(uid, ref handComp, ref sprite, false)) + return; + + if (uid == _playerManager.LocalPlayer?.ControlledEntity) + UpdateGui(); + + // Remove old layers. We could also just set them to invisible, but as items may add arbitrary layers, this + // may eventually bloat the player with lots of layers. + if (handComp.RevealedLayers.TryGetValue(hand.Location, out var revealedLayers)) + { + foreach (var key in revealedLayers) + { + sprite.RemoveLayer(key); + } + revealedLayers.Clear(); + } + else + { + revealedLayers = new(); + handComp.RevealedLayers[hand.Location] = revealedLayers; + } + + if (hand.HeldEntity == null) + { + // the held item was removed. + RaiseLocalEvent(held, new HeldVisualsUpdatedEvent(uid, revealedLayers)); + return; + } + + var ev = new GetInhandVisualsEvent(uid, hand.Location); + RaiseLocalEvent(held, ev, false); + + if (ev.Layers.Count == 0) + { + RaiseLocalEvent(held, new HeldVisualsUpdatedEvent(uid, revealedLayers)); + return; + } + + // add the new layers + foreach (var (key, layerData) in ev.Layers) + { + if (!revealedLayers.Add(key)) + { + Logger.Warning($"Duplicate key for in-hand visuals: {key}. Are multiple components attempting to modify the same layer? Entity: {ToPrettyString(held)}"); + continue; + } + + var index = sprite.LayerMapReserveBlank(key); + + // In case no RSI is given, use the item's base RSI as a default. This cuts down on a lot of unnecessary yaml entries. + if (layerData.RsiPath == null + && layerData.TexturePath == null + && sprite[index].Rsi == null + && TryComp(held, out SpriteComponent? clothingSprite)) + { + sprite.LayerSetRSI(index, clothingSprite.BaseRSI); + } + + sprite.LayerSetData(index, layerData); + } + + RaiseLocalEvent(held, new HeldVisualsUpdatedEvent(uid, revealedLayers)); + } + + private void OnVisualsChanged(EntityUid uid, HandsComponent component, VisualsChangedEvent args) + { + // update hands visuals if this item is in a hand (rather then inventory or other container). + if (component.TryGetHand(args.ContainerId, out var hand)) + { + UpdateHandVisuals(uid, args.Item, hand, component); + } + } + #endregion + + #region Gui public void UpdateGui(HandsComponent? hands = null) { @@ -193,14 +285,6 @@ namespace Content.Client.Hands hands.Gui.Update(new HandsGuiState(states, hands.ActiveHand)); } - public override void UpdateHandVisuals(EntityUid uid, SharedHandsComponent? handComp = null, AppearanceComponent? appearance = null) - { - base.UpdateHandVisuals(uid, handComp, appearance); - - if (uid == _playerManager.LocalPlayer?.ControlledEntity) - UpdateGui(); - } - public override bool TrySetActiveHand(EntityUid uid, string? value, SharedHandsComponent? handComp = null) { if (!base.TrySetActiveHand(uid, value, handComp)) diff --git a/Content.Client/Inventory/ClientInventoryComponent.cs b/Content.Client/Inventory/ClientInventoryComponent.cs index 010ac12ff4..3bfd6221dc 100644 --- a/Content.Client/Inventory/ClientInventoryComponent.cs +++ b/Content.Client/Inventory/ClientInventoryComponent.cs @@ -30,6 +30,12 @@ namespace Content.Client.Inventory [ViewVariables] [DataField("speciesId")] public string? SpeciesId { get; set; } + /// + /// Data about the current layers that have been added to the players sprite due to the items in each equipment slot. + /// + [ViewVariables] + public readonly Dictionary> VisualLayerKeys = new(); + public bool AttachedToGameHud; } } diff --git a/Content.Client/Items/Systems/ItemSystem.cs b/Content.Client/Items/Systems/ItemSystem.cs new file mode 100644 index 0000000000..32328ea664 --- /dev/null +++ b/Content.Client/Items/Systems/ItemSystem.cs @@ -0,0 +1,110 @@ +using Content.Shared.Hands; +using Content.Shared.Hands.Components; +using Content.Shared.Item; +using Robust.Client.GameObjects; +using Robust.Client.Graphics; +using Robust.Client.ResourceManagement; +using Robust.Shared.Containers; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Log; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using static Robust.Shared.GameObjects.SharedSpriteComponent; + +namespace Content.Client.Items.Systems; + +public sealed class ItemSystem : SharedItemSystem +{ + [Dependency] private readonly SharedContainerSystem _containerSystem = default!; + [Dependency] private readonly IResourceCache _resCache = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnGetVisuals); + } + + #region InhandVisuals + /// + /// When an items visual state changes, notify and entities that are holding this item that their sprite may need updating. + /// + public override void VisualsChanged(EntityUid uid, SharedItemComponent? item = null) + { + if (!Resolve(uid, ref item)) + return; + + // if the item is in a container, it might be equipped to hands or inventory slots --> update visuals. + if (_containerSystem.TryGetContainingContainer(uid, out var container)) + RaiseLocalEvent(container.Owner, new VisualsChangedEvent(uid, container.ID)); + } + + /// + /// An entity holding this item is requesting visual information for in-hand sprites. + /// + private void OnGetVisuals(EntityUid uid, SharedItemComponent item, GetInhandVisualsEvent args) + { + var defaultKey = $"inhand-{args.Location.ToString().ToLowerInvariant()}"; + + // try get explicit visuals + if (item.InhandVisuals == null || !item.InhandVisuals.TryGetValue(args.Location, out var layers)) + { + // get defaults + if (!TryGetDefaultVisuals(uid, item, defaultKey, out layers)) + return; + } + + var i = 0; + foreach (var layer in layers) + { + var key = layer.MapKeys?.FirstOrDefault(); + if (key == null) + { + key = i == 0 ? defaultKey : $"{defaultKey}-{i}"; + i++; + } + + args.Layers.Add((key, layer)); + } + } + + /// + /// If no explicit in-hand visuals were specified, this attempts to populate with default values. + /// + /// + /// Useful for lazily adding in-hand sprites without modifying yaml. And backwards compatibility. + /// + private bool TryGetDefaultVisuals(EntityUid uid, SharedItemComponent item, string defaultKey, [NotNullWhen(true)] out List? result) + { + result = null; + + RSI? rsi = null; + + if (item.RsiPath != null) + rsi = _resCache.GetResource(TextureRoot / item.RsiPath).RSI; + else if (TryComp(uid, out SpriteComponent? sprite)) + rsi = sprite.BaseRSI; + + if (rsi == null || rsi.Path == null) + return false; + + var state = (item.EquippedPrefix == null) + ? defaultKey + : $"{item.EquippedPrefix}-{defaultKey}"; + + if (!rsi.TryGetState(state, out var _)) + return false; + + var layer = PrototypeLayerData.New(); + layer.RsiPath = rsi.Path.ToString(); + layer.State = state; + layer.MapKeys = new() { state }; + + result = new() { layer }; + return true; + } + #endregion +} diff --git a/Content.Server/Items/ItemSystem.cs b/Content.Server/Items/ItemSystem.cs new file mode 100644 index 0000000000..51e62044db --- /dev/null +++ b/Content.Server/Items/ItemSystem.cs @@ -0,0 +1,8 @@ +using Content.Shared.Item; + +namespace Content.Server.Item; + +public sealed class ItemSystem : SharedItemSystem +{ + // Ello Guvna +} diff --git a/Content.Shared/Clothing/ClothingEvents.cs b/Content.Shared/Clothing/ClothingEvents.cs new file mode 100644 index 0000000000..3b38719b18 --- /dev/null +++ b/Content.Shared/Clothing/ClothingEvents.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using Robust.Shared.GameObjects; +using static Robust.Shared.GameObjects.SharedSpriteComponent; + +namespace Content.Shared.Clothing; + +/// +/// Raised directed at a piece of clothing to get the set of layers to show on the wearer's sprite +/// +public class GetEquipmentVisualsEvent : EntityEventArgs +{ + /// + /// Entity that is wearing the item. + /// + public readonly EntityUid Equipee; + + public readonly string Slot; + + /// + /// The layers that will be added to the entity that is wearing this item. + /// + /// + /// Note that the actual ordering of the layers depends on the order in which they are added to this list; + /// + public List<(string, PrototypeLayerData)> Layers = new(); + + public GetEquipmentVisualsEvent(EntityUid equipee, string slot) + { + Equipee = equipee; + Slot = slot; + } +} + +/// +/// Raised directed at a piece of clothing after its visuals have been updated. +/// +/// +/// Useful for systems/components that modify the visual layers that an item adds to a player. (e.g. RGB memes) +/// +public class EquipmentVisualsUpdatedEvent : EntityEventArgs +{ + /// + /// Entity that is wearing the item. + /// + public readonly EntityUid Equipee; + + public readonly string Slot; + + /// + /// The layers that this item is now revealing. + /// + public HashSet RevealedLayers; + + public EquipmentVisualsUpdatedEvent(EntityUid equipee, string slot, HashSet revealedLayers) + { + Equipee = equipee; + Slot = slot; + RevealedLayers = revealedLayers; + } +} diff --git a/Content.Shared/Hands/Components/HandVirtualItemComponent.cs b/Content.Shared/Hands/Components/HandVirtualItemComponent.cs index 2574137739..7603fd623e 100644 --- a/Content.Shared/Hands/Components/HandVirtualItemComponent.cs +++ b/Content.Shared/Hands/Components/HandVirtualItemComponent.cs @@ -1,4 +1,5 @@ using System; +using Content.Shared.Item; using Robust.Shared.Containers; using Robust.Shared.GameObjects; using Robust.Shared.GameStates; @@ -40,7 +41,7 @@ namespace Content.Shared.Hands.Components // update hands GUI with new entity. if (Owner.TryGetContainer(out var containter)) - EntitySystem.Get().UpdateHandVisuals(containter.Owner); + EntitySystem.Get().VisualsChanged(Owner); } [Serializable, NetSerializable] diff --git a/Content.Shared/Hands/Components/SharedHandsComponent.cs b/Content.Shared/Hands/Components/SharedHandsComponent.cs index 448ad6d4bb..3472889bca 100644 --- a/Content.Shared/Hands/Components/SharedHandsComponent.cs +++ b/Content.Shared/Hands/Components/SharedHandsComponent.cs @@ -3,8 +3,6 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using Content.Shared.ActionBlocker; -using Content.Shared.Administration.Logs; -using Content.Shared.Database; using Content.Shared.Interaction; using Content.Shared.Item; using Robust.Shared.Containers; @@ -18,6 +16,7 @@ using Robust.Shared.Physics; using Robust.Shared.Serialization; using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.ViewVariables; +using static Robust.Shared.GameObjects.SharedSpriteComponent; namespace Content.Shared.Hands.Components { @@ -708,47 +707,6 @@ namespace Content.Shared.Hands.Components } } - #region visualizerData - [Serializable, NetSerializable] - public enum HandsVisuals : byte - { - VisualState - } - - [Serializable, NetSerializable] - public class HandsVisualState : ICloneable - { - public List Hands { get; } = new(); - - public HandsVisualState(List hands) - { - Hands = hands; - } - - public object Clone() - { - return new HandsVisualState(new List(Hands)); - } - } - - [Serializable, NetSerializable] - public struct HandVisualState - { - public string RsiPath { get; } - public string? EquippedPrefix { get; } - public HandLocation Location { get; } - public Color Color { get; } - - public HandVisualState(string rsiPath, string? equippedPrefix, HandLocation location, Color color) - { - RsiPath = rsiPath; - EquippedPrefix = equippedPrefix; - Location = location; - Color = color; - } - } - #endregion - [Serializable, NetSerializable] public class Hand { diff --git a/Content.Shared/Hands/HandEvents.cs b/Content.Shared/Hands/HandEvents.cs index a2c727cf47..17655cac17 100644 --- a/Content.Shared/Hands/HandEvents.cs +++ b/Content.Shared/Hands/HandEvents.cs @@ -1,13 +1,67 @@ using System; +using System.Collections.Generic; using Content.Shared.Hands.Components; using JetBrains.Annotations; using Robust.Shared.GameObjects; using Robust.Shared.Map; using Robust.Shared.Maths; using Robust.Shared.Serialization; +using static Robust.Shared.GameObjects.SharedSpriteComponent; namespace Content.Shared.Hands { + /// + /// Raised directed at an item that needs to update its in-hand sprites/layers. + /// + public class GetInhandVisualsEvent : EntityEventArgs + { + /// + /// Entity that owns the hand holding the item. + /// + public readonly EntityUid User; + + public readonly HandLocation Location; + + /// + /// The layers that will be added to the entity that is holding this item. + /// + /// + /// Note that the actual ordering of the layers depends on the order in which they are added to this list; + /// + public List<(string, PrototypeLayerData)> Layers = new(); + + public GetInhandVisualsEvent(EntityUid user, HandLocation location) + { + User = user; + Location = location; + } + } + + /// + /// Raised directed at an item after its visuals have been updated. + /// + /// + /// Useful for systems/components that modify the visual layers that an item adds to a player. (e.g. RGB memes) + /// + public class HeldVisualsUpdatedEvent : EntityEventArgs + { + /// + /// Entity that is holding the item. + /// + public readonly EntityUid User; + + /// + /// The layers that this item is now revealing. + /// + public HashSet RevealedLayers; + + public HeldVisualsUpdatedEvent(EntityUid user, HashSet revealedLayers) + { + User = user; + RevealedLayers = revealedLayers; + } + } + /// /// Raised when an entity item in a hand is deselected. /// diff --git a/Content.Shared/Hands/SharedHandsSystem.cs b/Content.Shared/Hands/SharedHandsSystem.cs index 83804fb8e0..f642f9da1e 100644 --- a/Content.Shared/Hands/SharedHandsSystem.cs +++ b/Content.Shared/Hands/SharedHandsSystem.cs @@ -2,7 +2,6 @@ using Content.Shared.Administration.Logs; using Content.Shared.Database; using Content.Shared.Hands.Components; using Content.Shared.Input; -using Content.Shared.Item; using Robust.Shared.Containers; using Robust.Shared.GameObjects; using Robust.Shared.Input.Binding; @@ -11,7 +10,6 @@ using Robust.Shared.Log; using Robust.Shared.Map; using Robust.Shared.Maths; using Robust.Shared.Players; -using System.Collections.Generic; namespace Content.Shared.Hands { @@ -25,8 +23,7 @@ namespace Content.Shared.Hands SubscribeAllEvent(HandleSetHand); SubscribeLocalEvent(HandleContainerRemoved); - SubscribeLocalEvent(HandleContainerModified); - SubscribeLocalEvent(OnPrefixChanged); + SubscribeLocalEvent(HandleContainerInserted); CommandBinds.Builder .Bind(ContentKeyFunctions.Drop, new PointerInputCmdHandler(DropPressed)) @@ -34,15 +31,6 @@ namespace Content.Shared.Hands .Register(); } - private void OnPrefixChanged(EntityUid uid, SharedHandsComponent component, ItemPrefixChangeEvent args) - { - // update hands visuals if this item is in a hand (rather then inventory or other container). - if (component.HasHand(args.ContainerId)) - { - UpdateHandVisuals(uid, component); - } - } - public override void Shutdown() { base.Shutdown(); @@ -136,43 +124,25 @@ namespace Content.Shared.Hands public abstract void PickupAnimation(EntityUid item, EntityCoordinates initialPosition, Vector2 finalPosition, EntityUid? exclude); + #endregion protected virtual void HandleContainerRemoved(EntityUid uid, SharedHandsComponent component, ContainerModifiedMessage args) { HandleContainerModified(uid, component, args); } - #endregion - #region visuals - private void HandleContainerModified(EntityUid uid, SharedHandsComponent hands, ContainerModifiedMessage args) + protected virtual void HandleContainerModified(EntityUid uid, SharedHandsComponent hands, ContainerModifiedMessage args) { - UpdateHandVisuals(uid, hands); + // client updates hand visuals here. } - /// - /// Update the In-Hand sprites - /// - public virtual void UpdateHandVisuals(EntityUid uid, SharedHandsComponent? handComp = null, AppearanceComponent? appearance = null) + private void HandleContainerInserted(EntityUid uid, SharedHandsComponent component, EntInsertedIntoContainerMessage args) { - if (!Resolve(uid, ref handComp, ref appearance, false)) - return; + // un-rotate entities. needed for things like directional flashlights + Transform(args.Entity).LocalRotation = 0; - var handsVisuals = new List(); - foreach (var hand in handComp.Hands) - { - if (hand.HeldEntity == null) - continue; - - if (!TryComp(hand.HeldEntity.Value, out SharedItemComponent? item) || item.RsiPath == null) - continue; - - var handState = new HandVisualState(item.RsiPath, item.EquippedPrefix, hand.Location, item.Color); - handsVisuals.Add(handState); - } - - appearance.SetData(HandsVisuals.VisualState, new HandsVisualState(handsVisuals)); + HandleContainerModified(uid, component, args); } - #endregion private void HandleSetHand(RequestSetHandEvent msg, EntitySessionEventArgs eventArgs) { diff --git a/Content.Shared/Inventory/InventorySystem.Equip.cs b/Content.Shared/Inventory/InventorySystem.Equip.cs index 2b25abda1e..a2aa080df7 100644 --- a/Content.Shared/Inventory/InventorySystem.Equip.cs +++ b/Content.Shared/Inventory/InventorySystem.Equip.cs @@ -53,6 +53,9 @@ public abstract partial class InventorySystem if(!TryGetSlot(uid, args.Container.ID, out var slotDef, inventory: component)) return; + // un-rotate entities. needed for things like directional flashlights on hardsuit helmets + Transform(args.Entity).LocalRotation = 0; + var equippedEvent = new DidEquipEvent(uid, args.Entity, slotDef); RaiseLocalEvent(uid, equippedEvent); diff --git a/Content.Shared/Item/SharedItemComponent.cs b/Content.Shared/Item/SharedItemComponent.cs index b3196a86c9..d22efaa67f 100644 --- a/Content.Shared/Item/SharedItemComponent.cs +++ b/Content.Shared/Item/SharedItemComponent.cs @@ -1,11 +1,10 @@ using System; -using Content.Shared.Hands; +using System.Collections.Generic; using Content.Shared.Hands.Components; using Content.Shared.Interaction; using Content.Shared.Interaction.Helpers; using Content.Shared.Inventory; using Content.Shared.Sound; -using Robust.Shared.Containers; using Robust.Shared.GameObjects; using Robust.Shared.GameStates; using Robust.Shared.IoC; @@ -13,6 +12,7 @@ using Robust.Shared.Maths; using Robust.Shared.Serialization; using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.ViewVariables; +using static Robust.Shared.GameObjects.SharedSpriteComponent; namespace Content.Shared.Item { @@ -40,10 +40,18 @@ namespace Content.Shared.Item [DataField("size")] private int _size; + [DataField("inhandVisuals")] + public Dictionary> InhandVisuals = new(); + + [DataField("clothingVisuals")] + public Dictionary> ClothingVisuals = new(); + /// - /// Part of the state of the sprite shown on the player when this item is in their hands. + /// Part of the state of the sprite shown on the player when this item is in their hands or inventory. /// - // todo paul make this update slotvisuals on client on change + /// + /// Only used if or are unspecified. + /// [ViewVariables(VVAccess.ReadWrite)] public string? EquippedPrefix { @@ -51,7 +59,7 @@ namespace Content.Shared.Item set { _equippedPrefix = value; - OnEquippedPrefixChange(); + EntitySystem.Get().VisualsChanged(Owner, this); Dirty(); } } @@ -65,6 +73,7 @@ namespace Content.Shared.Item [DataField("EquipSound")] public SoundSpecifier? EquipSound { get; set; } = default!; + // TODO REMOVE. Currently nonfunctional and only used by RGB system. #6253 Fixes this but requires #6252 /// /// Color of the sprite shown on the player when this item is in their hands. /// @@ -82,20 +91,11 @@ namespace Content.Shared.Item private Color _color = Color.White; /// - /// Rsi of the sprite shown on the player when this item is in their hands. + /// Rsi of the sprite shown on the player when this item is in their hands. Used to generate a default entry for /// [ViewVariables(VVAccess.ReadWrite)] - public string? RsiPath - { - get => _rsiPath; - set - { - _rsiPath = value; - Dirty(); - } - } [DataField("sprite")] - private string? _rsiPath; + public readonly string? RsiPath; bool IInteractHand.InteractHand(InteractHandEventArgs eventArgs) { @@ -116,12 +116,6 @@ namespace Content.Shared.Item return hands.TryPickupEntityToActiveHand(Owner, animateUser: true); } - private void OnEquippedPrefixChange() - { - if (Owner.TryGetContainer(out var container)) - _entMan.EventBus.RaiseLocalEvent(container.Owner, new ItemPrefixChangeEvent(Owner, container.ID)); - } - public void RemovedFromSlot() { if (_entMan.TryGetComponent(Owner, out SharedSpriteComponent component)) @@ -140,29 +134,25 @@ namespace Content.Shared.Item { public int Size { get; } public string? EquippedPrefix { get; } - public Color Color { get; } - public string? RsiPath { get; } - public ItemComponentState(int size, string? equippedPrefix, Color color, string? rsiPath) + public ItemComponentState(int size, string? equippedPrefix) { Size = size; EquippedPrefix = equippedPrefix; - Color = color; - RsiPath = rsiPath; } } /// - /// Raised when an item's EquippedPrefix is changed. The event is directed at the entity that contains this item, so - /// that it can properly update its sprite/GUI. + /// Raised when an item's visual state is changed. The event is directed at the entity that contains this item, so + /// that it can properly update its hands or inventory sprites and GUI. /// [Serializable, NetSerializable] - public class ItemPrefixChangeEvent : EntityEventArgs + public class VisualsChangedEvent : EntityEventArgs { public readonly EntityUid Item; public readonly string ContainerId; - public ItemPrefixChangeEvent(EntityUid item, string containerId) + public VisualsChangedEvent(EntityUid item, string containerId) { Item = item; ContainerId = containerId; diff --git a/Content.Shared/Item/ItemSystem.cs b/Content.Shared/Item/SharedItemSystem.cs similarity index 88% rename from Content.Shared/Item/ItemSystem.cs rename to Content.Shared/Item/SharedItemSystem.cs index 1f05a229b0..f31cec387e 100644 --- a/Content.Shared/Item/ItemSystem.cs +++ b/Content.Shared/Item/SharedItemSystem.cs @@ -4,10 +4,11 @@ using Robust.Shared.Containers; using Robust.Shared.GameObjects; using Robust.Shared.GameStates; using Robust.Shared.Localization; +using System; namespace Content.Shared.Item { - public class ItemSystem : EntitySystem + public abstract class SharedItemSystem : EntitySystem { public override void Initialize() { @@ -28,13 +29,11 @@ namespace Content.Shared.Item component.Size = state.Size; component.EquippedPrefix = state.EquippedPrefix; - component.Color = state.Color; - component.RsiPath = state.RsiPath; } private void OnGetState(EntityUid uid, SharedItemComponent component, ref ComponentGetState args) { - args.State = new ItemComponentState(component.Size, component.EquippedPrefix, component.Color, component.RsiPath); + args.State = new ItemComponentState(component.Size, component.EquippedPrefix); } private void OnUnequipped(EntityUid uid, SharedSpriteComponent component, GotUnequippedEvent args) @@ -70,5 +69,11 @@ namespace Content.Shared.Item args.Verbs.Add(verb); } + + /// + /// Notifies any entity that is holding or wearing this item that they may need to update their sprite. + /// + public virtual void VisualsChanged(EntityUid owner, SharedItemComponent? item = null) + { } } } diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml index 9ff26f50f4..692c1152c8 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml @@ -502,7 +502,6 @@ normal: monkey crit: dead dead: dead - - type: HandsVisualizer - type: FireVisualizer sprite: Mobs/Effects/onfire.rsi normalState: Monkey_burning diff --git a/Resources/Prototypes/Entities/Mobs/Species/human.yml b/Resources/Prototypes/Entities/Mobs/Species/human.yml index a95472b3be..193dae8395 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/human.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/human.yml @@ -243,7 +243,6 @@ fireStackAlternateState: 3 - type: CreamPiedVisualizer state: creampie_human - - type: HandsVisualizer - type: DamageVisualizer thresholds: [20, 40, 100] targetLayers: