]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Fix broken layer hiding on clothes with multiple equipment slots (#34080)
authorpaige404 <59348003+paige404@users.noreply.github.com>
Thu, 20 Mar 2025 13:30:47 +0000 (09:30 -0400)
committerGitHub <noreply@github.com>
Thu, 20 Mar 2025 13:30:47 +0000 (00:30 +1100)
* Fix broken layer hiding on clothes with multiple equipment slots

* Refactor ToggleVisualLayers, HideLayerClothingComponent, and ClothingComponent to allow more
precise layer hide behavior and more CPU efficient layer toggling.

* Adjust HumanoidAppearaceSystem to track which slots are hiding a given layer (e.g. gas mask and welding mask)
Add documentation
Change gas masks to use the new HideLayerClothingComponent structure as an example of its usage

* Fix the delayed snout bug

* Misc cleanup

* Make `bool permanent` implicit from SlotFlags

any non-permanent visibility toggle with `SlotFlags.None` isn't supported with how its set up. And similarly, the slot flags argument does nothing if permanent = true. So IMO it makes more sense to infer it from a nullable arg.

* Split into separate system

Too much pasta

* Remove (hopefully unnecessary) refresh

* Fisk mask networking

AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

* Keep old behaviour, use clearer names?

I'm just guessing at what this was meant to do

* english

* Separate slot name & flag

* dirty = true

* fix comment

* Improved SetLayerVisibility with dirtying logic suggested by @ElectroJr

* Only set mask toggled if DisableOnFold is true

* FoldableClothingSystem fixes

* fix bandana state

* Better comment

---------

Co-authored-by: ElectroJr <leonsfriedrich@gmail.com>
20 files changed:
Content.Client/Clothing/ClientClothingSystem.cs
Content.Client/Humanoid/HumanoidAppearanceSystem.cs
Content.Server/Body/Systems/BodySystem.cs
Content.Server/Body/Systems/LungSystem.cs
Content.Server/Nutrition/EntitySystems/IngestionBlockerSystem.cs
Content.Shared/Clothing/ClothingEvents.cs
Content.Shared/Clothing/Components/ClothingComponent.cs
Content.Shared/Clothing/Components/FoldableClothingComponent.cs
Content.Shared/Clothing/Components/HideLayerClothingComponent.cs
Content.Shared/Clothing/Components/MaskComponent.cs
Content.Shared/Clothing/EntitySystems/ClothingSystem.cs
Content.Shared/Clothing/EntitySystems/FoldableClothingSystem.cs
Content.Shared/Clothing/EntitySystems/HideLayerClothingSystem.cs [new file with mode: 0644]
Content.Shared/Clothing/EntitySystems/MaskSystem.cs
Content.Shared/Foldable/FoldableSystem.cs
Content.Shared/Humanoid/HumanoidAppearanceComponent.cs
Content.Shared/Humanoid/SharedHumanoidAppearanceSystem.cs
Content.Shared/IdentityManagement/SharedIdentitySystem.cs
Resources/Prototypes/Entities/Clothing/Masks/bandanas.yml
Resources/Prototypes/Entities/Clothing/Masks/masks.yml

index 46f879e815608147c7f55b8d083184d7bc4102f3..5db7209d2228e60054dad6958078d46662b196a0 100644 (file)
@@ -162,7 +162,7 @@ public sealed class ClientClothingSystem : ClothingSystem
 
         var state = $"equipped-{correctedSlot}";
 
-        if (clothing.EquippedPrefix != null)
+        if (!string.IsNullOrEmpty(clothing.EquippedPrefix))
             state = $"{clothing.EquippedPrefix}-equipped-{correctedSlot}";
 
         if (clothing.EquippedState != null)
index 2d532968de0f1df05fea2564ce861b07d7f7e410..25c16ffd83384519270fe687fd618b696f537d65 100644 (file)
@@ -2,6 +2,7 @@ using Content.Shared.CCVar;
 using Content.Shared.Humanoid;
 using Content.Shared.Humanoid.Markings;
 using Content.Shared.Humanoid.Prototypes;
+using Content.Shared.Inventory;
 using Content.Shared.Preferences;
 using Robust.Client.GameObjects;
 using Robust.Shared.Configuration;
@@ -48,7 +49,7 @@ public sealed class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem
     }
 
     private static bool IsHidden(HumanoidAppearanceComponent humanoid, HumanoidVisualLayers layer)
-        => humanoid.HiddenLayers.Contains(layer) || humanoid.PermanentlyHidden.Contains(layer);
+        => humanoid.HiddenLayers.ContainsKey(layer) || humanoid.PermanentlyHidden.Contains(layer);
 
     private void UpdateLayers(HumanoidAppearanceComponent component, SpriteComponent sprite)
     {
@@ -203,7 +204,7 @@ public sealed class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem
 
         humanoid.MarkingSet = markings;
         humanoid.PermanentlyHidden = new HashSet<HumanoidVisualLayers>();
-        humanoid.HiddenLayers = new HashSet<HumanoidVisualLayers>();
+        humanoid.HiddenLayers = new Dictionary<HumanoidVisualLayers, SlotFlags>();
         humanoid.CustomBaseLayers = customBaseLayers;
         humanoid.Sex = profile.Sex;
         humanoid.Gender = profile.Gender;
@@ -391,23 +392,21 @@ public sealed class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem
         }
     }
 
-    protected override void SetLayerVisibility(
-        EntityUid uid,
-        HumanoidAppearanceComponent humanoid,
+    public override void SetLayerVisibility(
+        Entity<HumanoidAppearanceComponent> ent,
         HumanoidVisualLayers layer,
         bool visible,
-        bool permanent,
+        SlotFlags? slot,
         ref bool dirty)
     {
-        base.SetLayerVisibility(uid, humanoid, layer, visible, permanent, ref dirty);
+        base.SetLayerVisibility(ent, layer, visible, slot, ref dirty);
 
-        var sprite = Comp<SpriteComponent>(uid);
+        var sprite = Comp<SpriteComponent>(ent);
         if (!sprite.LayerMapTryGet(layer, out var index))
         {
             if (!visible)
                 return;
-            else
-                index = sprite.LayerMapReserveBlank(layer);
+            index = sprite.LayerMapReserveBlank(layer);
         }
 
         var spriteLayer = sprite[index];
@@ -417,13 +416,14 @@ public sealed class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem
         spriteLayer.Visible = visible;
 
         // I fucking hate this. I'll get around to refactoring sprite layers eventually I swear
+        // Just a week away...
 
-        foreach (var markingList in humanoid.MarkingSet.Markings.Values)
+        foreach (var markingList in ent.Comp.MarkingSet.Markings.Values)
         {
             foreach (var marking in markingList)
             {
                 if (_markingManager.TryGetMarking(marking, out var markingPrototype) && markingPrototype.BodyPart == layer)
-                    ApplyMarking(markingPrototype, marking.MarkingColors, marking.Visible, humanoid, sprite);
+                    ApplyMarking(markingPrototype, marking.MarkingColors, marking.Visible, ent, sprite);
             }
         }
     }
index 4279f3ed2b89de5bd1bcd3ef143e29ca997eebff..d41f2d57695ce281900718b10f49ad9b4af5c3bc 100644 (file)
@@ -65,15 +65,11 @@ public sealed class BodySystem : SharedBodySystem
         // TODO: Predict this probably.
         base.AddPart(bodyEnt, partEnt, slotId);
 
-        if (TryComp<HumanoidAppearanceComponent>(bodyEnt, out var humanoid))
+        var layer = partEnt.Comp.ToHumanoidLayers();
+        if (layer != null)
         {
-            var layer = partEnt.Comp.ToHumanoidLayers();
-            if (layer != null)
-            {
-                var layers = HumanoidVisualLayersExtension.Sublayers(layer.Value);
-                _humanoidSystem.SetLayersVisibility(
-                    bodyEnt, layers, visible: true, permanent: true, humanoid);
-            }
+            var layers = HumanoidVisualLayersExtension.Sublayers(layer.Value);
+            _humanoidSystem.SetLayersVisibility(bodyEnt.Owner, layers, visible: true);
         }
     }
 
@@ -93,8 +89,7 @@ public sealed class BodySystem : SharedBodySystem
             return;
 
         var layers = HumanoidVisualLayersExtension.Sublayers(layer.Value);
-        _humanoidSystem.SetLayersVisibility(
-            bodyEnt, layers, visible: false, permanent: true, humanoid);
+        _humanoidSystem.SetLayersVisibility((bodyEnt, humanoid), layers, visible: false);
     }
 
     public override HashSet<EntityUid> GibBody(
index 859618ae1a2a0b70ccf9a40d78a5ff2949dc4375..82ec490a4ae60cdee230872c244d520d0c7f91b0 100644 (file)
@@ -58,7 +58,7 @@ public sealed class LungSystem : EntitySystem
 
     private void OnMaskToggled(Entity<BreathToolComponent> ent, ref ItemMaskToggledEvent args)
     {
-        if (args.IsToggled || args.IsEquip)
+        if (args.Mask.Comp.IsToggled)
         {
             _atmos.DisconnectInternals(ent);
         }
@@ -69,7 +69,7 @@ public sealed class LungSystem : EntitySystem
             if (TryComp(args.Wearer, out InternalsComponent? internals))
             {
                 ent.Comp.ConnectedInternalsEntity = args.Wearer;
-                _internals.ConnectBreathTool((args.Wearer, internals), ent);
+                _internals.ConnectBreathTool((args.Wearer.Value, internals), ent);
             }
         }
     }
index ede1c21680c4a8446f1286bc332d534f790fc57e..63b39fb524a2149f56736cf168ebd02e956fc29c 100644 (file)
@@ -14,6 +14,6 @@ public sealed class IngestionBlockerSystem : EntitySystem
 
     private void OnBlockerMaskToggled(Entity<IngestionBlockerComponent> ent, ref ItemMaskToggledEvent args)
     {
-        ent.Comp.Enabled = !args.IsToggled;
+        ent.Comp.Enabled = !args.Mask.Comp.IsToggled;
     }
 }
index 83afea459738e6c7040ad207e1c838d1f125ba6a..9b0b69d186e158ed3dea77e144f7e33cb632be93 100644 (file)
@@ -65,13 +65,13 @@ public sealed partial class ToggleMaskEvent : InstantActionEvent { }
 ///     Event raised on the mask entity when it is toggled.
 /// </summary>
 [ByRefEvent]
-public readonly record struct ItemMaskToggledEvent(EntityUid Wearer, string? equippedPrefix, bool IsToggled, bool IsEquip);
+public readonly record struct ItemMaskToggledEvent(Entity<MaskComponent> Mask, EntityUid? Wearer);
 
 /// <summary>
 ///     Event raised on the entity wearing the mask when it is toggled.
 /// </summary>
 [ByRefEvent]
-public readonly record struct WearerMaskToggledEvent(bool IsToggled);
+public readonly record struct WearerMaskToggledEvent(Entity<MaskComponent> Mask);
 
 /// <summary>
 /// Raised on the clothing entity when it is equipped to a valid slot,
index 4f8058dbf5a03e77e8f449e018de3e9295fc4b2e..260af210e0dd8c5a5b65827934da9fb716c054c4 100644 (file)
@@ -1,3 +1,4 @@
+using System.Diagnostics.CodeAnalysis;
 using Content.Shared.Clothing.EntitySystems;
 using Content.Shared.DoAfter;
 using Content.Shared.Inventory;
@@ -28,8 +29,15 @@ public sealed partial class ClothingComponent : Component
     [DataField("quickEquip")]
     public bool QuickEquip = true;
 
+    /// <summary>
+    /// The slots in which the clothing is considered "worn" or "equipped". E.g., putting shoes in your pockets does not
+    /// equip them as far as clothing related events are concerned.
+    /// </summary>
+    /// <remarks>
+    /// Note that this may be a combination of different slot flags, not a singular bit.
+    /// </remarks>
     [ViewVariables(VVAccess.ReadWrite)]
-    [DataField("slots", required: true)]
+    [DataField(required: true)]
     [Access(typeof(ClothingSystem), typeof(InventorySystem), Other = AccessPermissions.ReadExecute)]
     public SlotFlags Slots = SlotFlags.NONE;
 
@@ -60,9 +68,25 @@ public sealed partial class ClothingComponent : Component
     public string? RsiPath;
 
     /// <summary>
-    /// Name of the inventory slot the clothing is in.
+    /// Name of the inventory slot the clothing is currently in.
+    /// Note that this being non-null does not mean the clothing is considered "worn" or "equipped" unless the slot
+    /// satisfies the <see cref="Slots"/> flags.
     /// </summary>
+    [DataField]
     public string? InSlot;
+    // TODO CLOTHING
+    // Maybe keep this null unless its in a valid slot?
+    // To lazy to figure out ATM if that would break anything.
+    // And when doing this, combine InSlot and InSlotFlag, as it'd be a breaking change for downstreams anyway
+
+    /// <summary>
+    /// Slot flags of the slot the clothing is currently in. See also <see cref="InSlot"/>.
+    /// </summary>
+    [DataField]
+    public SlotFlags? InSlotFlag;
+    // TODO CLOTHING
+    // Maybe keep this null unless its in a valid slot?
+    // And when doing this, combine InSlot and InSlotFlag, as it'd be a breaking change for downstreams anyway
 
     [DataField, ViewVariables(VVAccess.ReadWrite)]
     public TimeSpan EquipDelay = TimeSpan.Zero;
index ffcb52b457666e70ce326b5d3b741041d76cfc46..7b03adcc8d6b493726fe227520cd03b8c0cf4026 100644 (file)
@@ -19,7 +19,6 @@ public sealed partial class FoldableClothingComponent : Component
     [DataField]
     public SlotFlags? UnfoldedSlots;
 
-
     /// <summary>
     /// What equipped prefix does this have while in folded form?
     /// </summary>
@@ -36,11 +35,11 @@ public sealed partial class FoldableClothingComponent : Component
     /// Which layers does this hide when Unfolded? See <see cref="HumanoidVisualLayers"/> and <see cref="HideLayerClothingComponent"/>
     /// </summary>
     [DataField]
-    public HashSet<HumanoidVisualLayers> UnfoldedHideLayers = new();
+    public HashSet<HumanoidVisualLayers>? UnfoldedHideLayers = new();
 
     /// <summary>
     /// Which layers does this hide when folded? See <see cref="HumanoidVisualLayers"/> and <see cref="HideLayerClothingComponent"/>
     /// </summary>
     [DataField]
-    public HashSet<HumanoidVisualLayers> FoldedHideLayers = new();
+    public HashSet<HumanoidVisualLayers>? FoldedHideLayers = new();
 }
index ac3d9b978965f6287772b145cd6daed77d6cee12..b5445c28b98330c1d80e5c70596a5da990d7b260 100644 (file)
@@ -1,4 +1,5 @@
 using Content.Shared.Humanoid;
+using Content.Shared.Inventory;
 using Robust.Shared.GameStates;
 
 namespace Content.Shared.Clothing.Components;
@@ -11,10 +12,17 @@ namespace Content.Shared.Clothing.Components;
 public sealed partial class HideLayerClothingComponent : Component
 {
     /// <summary>
-    /// The appearance layer to hide.
+    /// The appearance layer(s) to hide. Use <see cref='Layers'>Layers</see> instead.
     /// </summary>
     [DataField]
-    public HashSet<HumanoidVisualLayers> Slots = new();
+    [Obsolete("This attribute is deprecated, please use Layers instead.")]
+    public HashSet<HumanoidVisualLayers>? Slots;
+
+    /// <summary>
+    /// A map of the appearance layer(s) to hide, and the equipment slot that should hide them.
+    /// </summary>
+    [DataField]
+    public Dictionary<HumanoidVisualLayers, SlotFlags> Layers = new();
 
     /// <summary>
     /// If true, the layer will only hide when the item is in a toggled state (e.g. masks)
index 47f2fd3079c22aeed2ec896d75cb62157f92ac7e..985219df2ec5759958248008b6c9b3a6e298c470 100644 (file)
@@ -8,15 +8,22 @@ namespace Content.Shared.Clothing.Components;
 [Access(typeof(MaskSystem))]
 public sealed partial class MaskComponent : Component
 {
+    /// <summary>
+    /// Action for toggling a mask (e.g., pulling the mask down or putting it back up)
+    /// </summary>
     [DataField, AutoNetworkedField]
     public EntProtoId ToggleAction = "ActionToggleMask";
 
     /// <summary>
-    /// This mask can be toggled (pulled up/down)
+    /// Action for toggling a mask (e.g., pulling the mask down or putting it back up)
     /// </summary>
     [DataField, AutoNetworkedField]
     public EntityUid? ToggleActionEntity;
 
+    /// <summary>
+    /// Whether the mask is currently toggled (e.g., pulled down).
+    /// This generally disables some of the mask's functionality.
+    /// </summary>
     [DataField, AutoNetworkedField]
     public bool IsToggled;
 
@@ -27,13 +34,13 @@ public sealed partial class MaskComponent : Component
     public string EquippedPrefix = "up";
 
     /// <summary>
-    /// When <see langword="true"/> will function normally, otherwise will not react to events
+    /// When <see langword="false"/>, the mask will not be toggleable.
     /// </summary>
     [DataField("enabled"), AutoNetworkedField]
-    public bool IsEnabled = true;
+    public bool IsToggleable = true;
 
     /// <summary>
-    /// When <see langword="true"/> will disable <see cref="IsEnabled"/> when folded
+    /// When <see langword="true"/> will disable <see cref="IsToggleable"/> when folded
     /// </summary>
     [DataField, AutoNetworkedField]
     public bool DisableOnFolded;
index 3b26360f10795065ea4e3e9a620fdb53f5a2616b..f8ab79ec78c8bd7ccdf5de78946c243b4efe7481 100644 (file)
@@ -16,9 +16,9 @@ public abstract class ClothingSystem : EntitySystem
 {
     [Dependency] private readonly SharedItemSystem _itemSys = default!;
     [Dependency] private readonly SharedContainerSystem _containerSys = default!;
-    [Dependency] private readonly SharedHumanoidAppearanceSystem _humanoidSystem = default!;
     [Dependency] private readonly InventorySystem _invSystem = default!;
     [Dependency] private readonly SharedHandsSystem _handsSystem = default!;
+    [Dependency] private readonly HideLayerClothingSystem _hideLayer = default!;
 
     public override void Initialize()
     {
@@ -29,7 +29,6 @@ public abstract class ClothingSystem : EntitySystem
         SubscribeLocalEvent<ClothingComponent, ComponentHandleState>(OnHandleState);
         SubscribeLocalEvent<ClothingComponent, GotEquippedEvent>(OnGotEquipped);
         SubscribeLocalEvent<ClothingComponent, GotUnequippedEvent>(OnGotUnequipped);
-        SubscribeLocalEvent<ClothingComponent, ItemMaskToggledEvent>(OnMaskToggled);
 
         SubscribeLocalEvent<ClothingComponent, ClothingEquipDoAfterEvent>(OnEquipDoAfter);
         SubscribeLocalEvent<ClothingComponent, ClothingUnequipDoAfterEvent>(OnUnequipDoAfter);
@@ -85,59 +84,19 @@ public abstract class ClothingSystem : EntitySystem
         }
     }
 
-    private void ToggleVisualLayers(EntityUid equipee, HashSet<HumanoidVisualLayers> layers, HashSet<HumanoidVisualLayers> appearanceLayers)
-    {
-        foreach (HumanoidVisualLayers layer in layers)
-        {
-            if (!appearanceLayers.Contains(layer))
-                continue;
-
-            InventorySystem.InventorySlotEnumerator enumerator = _invSystem.GetSlotEnumerator(equipee);
-
-            bool shouldLayerShow = true;
-            while (enumerator.NextItem(out EntityUid item, out SlotDefinition? slot))
-            {
-                if (TryComp(item, out HideLayerClothingComponent? comp))
-                {
-                    if (comp.Slots.Contains(layer))
-                    {
-                        if (TryComp(item, out ClothingComponent? clothing) && clothing.Slots == slot.SlotFlags)
-                        {
-                            //Checks for mask toggling. TODO: Make a generic system for this
-                            if (comp.HideOnToggle && TryComp(item, out MaskComponent? mask))
-                            {
-                                if (clothing.EquippedPrefix != mask.EquippedPrefix)
-                                {
-                                    shouldLayerShow = false;
-                                    break;
-                                }
-                            }
-                            else
-                            {
-                                shouldLayerShow = false;
-                                break;
-                            }
-                        }
-                    }
-                }
-            }
-            _humanoidSystem.SetLayerVisibility(equipee, layer, shouldLayerShow);
-        }
-    }
-
     protected virtual void OnGotEquipped(EntityUid uid, ClothingComponent component, GotEquippedEvent args)
     {
         component.InSlot = args.Slot;
-        CheckEquipmentForLayerHide(args.Equipment, args.Equipee);
+        component.InSlotFlag = args.SlotFlags;
 
-        if ((component.Slots & args.SlotFlags) != SlotFlags.NONE)
-        {
-            var gotEquippedEvent = new ClothingGotEquippedEvent(args.Equipee, component);
-            RaiseLocalEvent(uid, ref gotEquippedEvent);
+        if ((component.Slots & args.SlotFlags) == SlotFlags.NONE)
+            return;
 
-            var didEquippedEvent = new ClothingDidEquippedEvent((uid, component));
-            RaiseLocalEvent(args.Equipee, ref didEquippedEvent);
-        }
+        var gotEquippedEvent = new ClothingGotEquippedEvent(args.Equipee, component);
+        RaiseLocalEvent(uid, ref gotEquippedEvent);
+
+        var didEquippedEvent = new ClothingDidEquippedEvent((uid, component));
+        RaiseLocalEvent(args.Equipee, ref didEquippedEvent);
     }
 
     protected virtual void OnGotUnequipped(EntityUid uid, ClothingComponent component, GotUnequippedEvent args)
@@ -152,7 +111,7 @@ public abstract class ClothingSystem : EntitySystem
         }
 
         component.InSlot = null;
-        CheckEquipmentForLayerHide(args.Equipment, args.Equipee);
+        component.InSlotFlag = null;
     }
 
     private void OnGetState(EntityUid uid, ClothingComponent component, ref ComponentGetState args)
@@ -162,21 +121,10 @@ public abstract class ClothingSystem : EntitySystem
 
     private void OnHandleState(EntityUid uid, ClothingComponent component, ref ComponentHandleState args)
     {
-        if (args.Current is ClothingComponentState state)
-        {
-            SetEquippedPrefix(uid, state.EquippedPrefix, component);
-            if (component.InSlot != null && _containerSys.TryGetContainingContainer((uid, null, null), out var container))
-            {
-                CheckEquipmentForLayerHide(uid, container.Owner);
-            }
-        }
-    }
+        if (args.Current is not ClothingComponentState state)
+            return;
 
-    private void OnMaskToggled(Entity<ClothingComponent> ent, ref ItemMaskToggledEvent args)
-    {
-        //TODO: sprites for 'pulled down' state. defaults to invisible due to no sprite with this prefix
-        SetEquippedPrefix(ent, args.IsToggled ? args.equippedPrefix : null, ent);
-        CheckEquipmentForLayerHide(ent.Owner, args.Wearer);
+        SetEquippedPrefix(uid, state.EquippedPrefix, component);
     }
 
     private void OnEquipDoAfter(Entity<ClothingComponent> ent, ref ClothingEquipDoAfterEvent args)
@@ -200,12 +148,6 @@ public abstract class ClothingSystem : EntitySystem
         args.Additive += ent.Comp.StripDelay;
     }
 
-    private void CheckEquipmentForLayerHide(EntityUid equipment, EntityUid equipee)
-    {
-        if (TryComp(equipment, out HideLayerClothingComponent? clothesComp) && TryComp(equipee, out HumanoidAppearanceComponent? appearanceComp))
-            ToggleVisualLayers(equipee, clothesComp.Slots, appearanceComp.HideLayersOnEquip);
-    }
-
     #region Public API
 
     public void SetEquippedPrefix(EntityUid uid, string? prefix, ClothingComponent? clothing = null)
index 603af4099c8fcbeadf69567fa30bb075f1a1c8c8..a60caa454b1e6bb3d5bfbd515e6f5f6ac34e96c7 100644 (file)
@@ -16,7 +16,8 @@ public sealed class FoldableClothingSystem : EntitySystem
         base.Initialize();
 
         SubscribeLocalEvent<FoldableClothingComponent, FoldAttemptEvent>(OnFoldAttempt);
-        SubscribeLocalEvent<FoldableClothingComponent, FoldedEvent>(OnFolded);
+        SubscribeLocalEvent<FoldableClothingComponent, FoldedEvent>(OnFolded,
+            after: [typeof(MaskSystem)]); // Mask system also modifies clothing / equipment RSI state prefixes.
     }
 
     private void OnFoldAttempt(Entity<FoldableClothingComponent> ent, ref FoldAttemptEvent args)
@@ -24,10 +25,19 @@ public sealed class FoldableClothingSystem : EntitySystem
         if (args.Cancelled)
             return;
 
-        // allow folding while equipped if allowed slots are the same:
-        // e.g. flip a hat backwards while on your head
-        if (_inventorySystem.TryGetContainingSlot(ent.Owner, out var slot) &&
-            !ent.Comp.FoldedSlots.Equals(ent.Comp.UnfoldedSlots))
+        if (!_inventorySystem.TryGetContainingSlot(ent.Owner, out var slot))
+            return;
+
+        // Cannot fold clothing equipped to a slot if the slot becomes disallowed
+        var newSlots = args.Comp.IsFolded ? ent.Comp.UnfoldedSlots : ent.Comp.FoldedSlots;
+        if (newSlots != null && (newSlots.Value & slot.SlotFlags) != slot.SlotFlags)
+        {
+            args.Cancelled = true;
+            return;
+        }
+
+        // Setting hidden layers while equipped is not currently supported.
+        if (ent.Comp.FoldedHideLayers != null || ent.Comp.UnfoldedHideLayers != null)
             args.Cancelled = true;
     }
 
@@ -48,7 +58,14 @@ public sealed class FoldableClothingSystem : EntitySystem
             if (ent.Comp.FoldedHeldPrefix != null)
                 _itemSystem.SetHeldPrefix(ent.Owner, ent.Comp.FoldedHeldPrefix, false, itemComp);
 
-            if (TryComp<HideLayerClothingComponent>(ent.Owner, out var hideLayerComp))
+            // This is janky and likely to lead to bugs.
+            // I.e., overriding this and resetting it again later will lead to bugs if someone tries to modify clothing
+            // in yaml, but doesn't realise theres actually two other fields on an unrelated component that they also need
+            // to modify.
+            // This should instead work via an event or something that gets raised to optionally modify the currently hidden layers.
+            // Or at the very least it should stash the old layers and restore them when unfolded.
+            // TODO CLOTHING fix this.
+            if (ent.Comp.FoldedHideLayers != null && TryComp<HideLayerClothingComponent>(ent.Owner, out var hideLayerComp))
                 hideLayerComp.Slots = ent.Comp.FoldedHideLayers;
 
         }
@@ -63,7 +80,8 @@ public sealed class FoldableClothingSystem : EntitySystem
             if (ent.Comp.FoldedHeldPrefix != null)
                 _itemSystem.SetHeldPrefix(ent.Owner, null, false, itemComp);
 
-            if (TryComp<HideLayerClothingComponent>(ent.Owner, out var hideLayerComp))
+            // TODO CLOTHING fix this.
+            if (ent.Comp.UnfoldedHideLayers != null && TryComp<HideLayerClothingComponent>(ent.Owner, out var hideLayerComp))
                 hideLayerComp.Slots = ent.Comp.UnfoldedHideLayers;
 
         }
diff --git a/Content.Shared/Clothing/EntitySystems/HideLayerClothingSystem.cs b/Content.Shared/Clothing/EntitySystems/HideLayerClothingSystem.cs
new file mode 100644 (file)
index 0000000..323884a
--- /dev/null
@@ -0,0 +1,106 @@
+using Content.Shared.Clothing.Components;
+using Content.Shared.Humanoid;
+using Content.Shared.Inventory;
+using Robust.Shared.Timing;
+using Robust.Shared.Utility;
+
+namespace Content.Shared.Clothing.EntitySystems;
+
+public sealed class HideLayerClothingSystem : EntitySystem
+{
+    [Dependency] private readonly SharedHumanoidAppearanceSystem _humanoid = default!;
+    [Dependency] private readonly IGameTiming _timing = default!;
+
+    public override void Initialize()
+    {
+        SubscribeLocalEvent<HideLayerClothingComponent, ClothingGotUnequippedEvent>(OnHideGotUnequipped);
+        SubscribeLocalEvent<HideLayerClothingComponent, ClothingGotEquippedEvent>(OnHideGotEquipped);
+        SubscribeLocalEvent<HideLayerClothingComponent, ItemMaskToggledEvent>(OnHideToggled);
+    }
+
+    private void OnHideToggled(Entity<HideLayerClothingComponent> ent, ref ItemMaskToggledEvent args)
+    {
+        if (args.Wearer != null)
+            SetLayerVisibility(ent!, args.Wearer.Value, hideLayers: true);
+    }
+
+    private void OnHideGotEquipped(Entity<HideLayerClothingComponent> ent, ref ClothingGotEquippedEvent args)
+    {
+        SetLayerVisibility(ent!, args.Wearer, hideLayers: true);
+    }
+
+    private void OnHideGotUnequipped(Entity<HideLayerClothingComponent> ent, ref ClothingGotUnequippedEvent args)
+    {
+        SetLayerVisibility(ent!, args.Wearer, hideLayers: false);
+    }
+
+    private void SetLayerVisibility(
+        Entity<HideLayerClothingComponent?, ClothingComponent?> clothing,
+        Entity<HumanoidAppearanceComponent?> user,
+        bool hideLayers)
+    {
+        if (_timing.ApplyingState)
+            return;
+
+        if (!Resolve(clothing.Owner, ref clothing.Comp1, ref clothing.Comp2))
+            return;
+
+        if (!Resolve(user.Owner, ref user.Comp))
+            return;
+
+        hideLayers &= IsEnabled(clothing!);
+
+        var hideable = user.Comp.HideLayersOnEquip;
+        var inSlot = clothing.Comp2.InSlotFlag ?? SlotFlags.NONE;
+
+        // This method should only be getting called while the clothing is equipped (though possibly currently in
+        // the process of getting unequipped).
+        DebugTools.AssertNotNull(clothing.Comp2.InSlot);
+        DebugTools.AssertNotNull(clothing.Comp2.InSlotFlag);
+        DebugTools.AssertNotEqual(inSlot, SlotFlags.NONE);
+
+        var dirty = false;
+
+        // iterate the HideLayerClothingComponent's layers map and check that
+        // the clothing is (or was)equipped in a matching slot.
+        foreach (var (layer, validSlots) in clothing.Comp1.Layers)
+        {
+            if (!hideable.Contains(layer))
+                continue;
+
+            // Only update this layer if we are currently equipped to the relevant slot.
+            if (validSlots.HasFlag(inSlot))
+                _humanoid.SetLayerVisibility(user!, layer, !hideLayers, inSlot, ref dirty);
+        }
+
+        // Fallback for obsolete field: assume we want to hide **all** layers, as long as we are equipped to any
+        // relevant clothing slot
+#pragma warning disable CS0618 // Type or member is obsolete
+        if (clothing.Comp1.Slots is { } slots && clothing.Comp2.Slots.HasFlag(inSlot))
+#pragma warning restore CS0618 // Type or member is obsolete
+        {
+            foreach (var layer in slots)
+            {
+                if (hideable.Contains(layer))
+                    _humanoid.SetLayerVisibility(user!, layer, !hideLayers, inSlot, ref dirty);
+            }
+        }
+
+        if (dirty)
+            Dirty(user!);
+    }
+
+    private bool IsEnabled(Entity<HideLayerClothingComponent, ClothingComponent> clothing)
+    {
+        // TODO Generalize this
+        // I.e., make this and mask component use some generic toggleable.
+
+        if (!clothing.Comp1.HideOnToggle)
+            return true;
+
+        if (!TryComp(clothing, out MaskComponent? mask))
+            return true;
+
+        return !mask.IsToggled;
+    }
+}
index fd0b7e782bfffdcc5bbef4553ca6c6c48a072ed9..3e899f3dc33b6c5b88ea7cb72092aaa98d6f6b62 100644 (file)
@@ -3,7 +3,6 @@ using Content.Shared.Clothing.Components;
 using Content.Shared.Foldable;
 using Content.Shared.Inventory;
 using Content.Shared.Inventory.Events;
-using Content.Shared.Item;
 using Content.Shared.Popups;
 using Robust.Shared.Timing;
 
@@ -15,6 +14,7 @@ public sealed class MaskSystem : EntitySystem
     [Dependency] private readonly InventorySystem _inventorySystem = default!;
     [Dependency] private readonly SharedPopupSystem _popupSystem = default!;
     [Dependency] private readonly IGameTiming _timing = default!;
+    [Dependency] private readonly ClothingSystem _clothing = default!;
 
     public override void Initialize()
     {
@@ -35,53 +35,109 @@ public sealed class MaskSystem : EntitySystem
     private void OnToggleMask(Entity<MaskComponent> ent, ref ToggleMaskEvent args)
     {
         var (uid, mask) = ent;
-        if (mask.ToggleActionEntity == null || !_timing.IsFirstTimePredicted || !mask.IsEnabled)
+        if (mask.ToggleActionEntity == null || !mask.IsToggleable)
             return;
 
-        if (!_inventorySystem.TryGetSlotEntity(args.Performer, "mask", out var existing) || !uid.Equals(existing))
+        // Masks are currently only toggleable via the action while equipped.
+        // Its possible this might change in future?
+
+        // TODO Inventory / Clothing
+        // Add an easier way to check if clothing is equipped to a valid slot.
+        if (!TryComp(ent, out ClothingComponent? clothing)
+            || clothing.InSlotFlag is not { } slotFlag
+            || !clothing.Slots.HasFlag(slotFlag))
+        {
             return;
+        }
 
-        mask.IsToggled ^= true;
+        SetToggled((uid, mask), !mask.IsToggled);
 
         var dir = mask.IsToggled ? "down" : "up";
         var msg = $"action-mask-pull-{dir}-popup-message";
         _popupSystem.PopupClient(Loc.GetString(msg, ("mask", uid)), args.Performer, args.Performer);
-
-        ToggleMaskComponents(uid, mask, args.Performer, mask.EquippedPrefix);
     }
 
-    // set to untoggled when unequipped, so it isn't left in a 'pulled down' state
     private void OnGotUnequipped(EntityUid uid, MaskComponent mask, GotUnequippedEvent args)
     {
-        if (!mask.IsToggled || !mask.IsEnabled)
+        // Masks are currently always un-toggled when unequipped.
+        SetToggled((uid, mask), false);
+    }
+
+    private void OnFolded(Entity<MaskComponent> ent, ref FoldedEvent args)
+    {
+        // See FoldableClothingComponent
+
+        if (!ent.Comp.DisableOnFolded)
             return;
 
-        mask.IsToggled = false;
-        ToggleMaskComponents(uid, mask, args.Equipee, mask.EquippedPrefix, true);
+        // While folded, we force the mask to be toggled / pulled down, so that its functionality as a mask is disabled,
+        // and we also prevent it from being un-toggled. We also automatically untoggle it when it gets unfolded, so it
+        // fully returns to its previous state when folded & unfolded.
+
+        SetToggled(ent!, args.IsFolded, force: true);
+        SetToggleable(ent!, !args.IsFolded);
     }
 
-    /// <summary>
-    /// Called after setting IsToggled, raises events and dirties.
-    /// <summary>
-    private void ToggleMaskComponents(EntityUid uid, MaskComponent mask, EntityUid wearer, string? equippedPrefix = null, bool isEquip = false)
+    public void SetToggled(Entity<MaskComponent?> mask, bool toggled, bool force = false)
     {
-        Dirty(uid, mask);
-        if (mask.ToggleActionEntity is {} action)
-            _actionSystem.SetToggled(action, mask.IsToggled);
+        if (_timing.ApplyingState)
+            return;
+
+        if (!Resolve(mask.Owner, ref mask.Comp))
+            return;
 
-        var maskEv = new ItemMaskToggledEvent(wearer, equippedPrefix, mask.IsToggled, isEquip);
-        RaiseLocalEvent(uid, ref maskEv);
+        if (!force && !mask.Comp.IsToggleable)
+            return;
+
+        if (mask.Comp.IsToggled == toggled)
+            return;
 
-        var wearerEv = new WearerMaskToggledEvent(mask.IsToggled);
-        RaiseLocalEvent(wearer, ref wearerEv);
+        mask.Comp.IsToggled = toggled;
+
+        if (mask.Comp.ToggleActionEntity is { } action)
+            _actionSystem.SetToggled(action, mask.Comp.IsToggled);
+
+        // TODO Generalize toggling & clothing prefixes. See also FoldableClothingComponent
+        var prefix = mask.Comp.IsToggled ? mask.Comp.EquippedPrefix : null;
+        _clothing.SetEquippedPrefix(mask, prefix);
+
+        // TODO Inventory / Clothing
+        // Add an easier way to get the entity that is wearing clothing in a valid slot.
+        EntityUid? wearer = null;
+        if (TryComp(mask, out ClothingComponent? clothing)
+            && clothing.InSlotFlag is {} slotFlag
+            && clothing.Slots.HasFlag(slotFlag))
+        {
+            wearer = Transform(mask).ParentUid;
+        }
+
+        var maskEv = new ItemMaskToggledEvent(mask!, wearer);
+        RaiseLocalEvent(mask, ref maskEv);
+
+        if (wearer != null)
+        {
+            var wearerEv = new WearerMaskToggledEvent(mask!);
+            RaiseLocalEvent(wearer.Value, ref wearerEv);
+        }
+
+        Dirty(mask);
     }
 
-    private void OnFolded(Entity<MaskComponent> ent, ref FoldedEvent args)
+    public void SetToggleable(Entity<MaskComponent?> mask, bool toggleable)
     {
-        if (ent.Comp.DisableOnFolded)
-            ent.Comp.IsEnabled = !args.IsFolded;
-        ent.Comp.IsToggled = args.IsFolded;
+        if (_timing.ApplyingState)
+            return;
+
+        if (!Resolve(mask.Owner, ref mask.Comp))
+            return;
+
+        if (mask.Comp.IsToggleable == toggleable)
+            return;
+
+        if (mask.Comp.ToggleActionEntity is { } action)
+            _actionSystem.SetEnabled(action, mask.Comp.IsToggleable);
 
-        ToggleMaskComponents(ent.Owner, ent.Comp, ent.Owner);
+        mask.Comp.IsToggleable = toggleable;
+        Dirty(mask);
     }
 }
index c25137237252962066fdae7c5ad000974b9da2c6..3ece56720a80f167c915c4d0d15c7bcaa829a74d 100644 (file)
@@ -103,7 +103,7 @@ public sealed class FoldableSystem : EntitySystem
         if (_container.IsEntityInContainer(uid) && !fold.CanFoldInsideContainer)
             return false;
 
-        var ev = new FoldAttemptEvent();
+        var ev = new FoldAttemptEvent(fold);
         RaiseLocalEvent(uid, ref ev);
         return !ev.Cancelled;
     }
@@ -157,7 +157,7 @@ public sealed class FoldableSystem : EntitySystem
 /// </summary>
 /// <param name="Cancelled"></param>
 [ByRefEvent]
-public record struct FoldAttemptEvent(bool Cancelled = false);
+public record struct FoldAttemptEvent(FoldableComponent Comp, bool Cancelled = false);
 
 /// <summary>
 /// Event raised on an entity after it has been folded.
index 0bf11f5762a8edf54433d5fdfc1058712bdf93d9..4a741074c9e82e8abd02ae8e967655a3daa2add9 100644 (file)
@@ -1,5 +1,6 @@
 using Content.Shared.Humanoid.Markings;
 using Content.Shared.Humanoid.Prototypes;
+using Content.Shared.Inventory;
 using Robust.Shared.Enums;
 using Robust.Shared.GameStates;
 using Robust.Shared.Prototypes;
@@ -59,11 +60,12 @@ public sealed partial class HumanoidAppearanceComponent : Component
     public Color SkinColor { get; set; } = Color.FromHex("#C0967F");
 
     /// <summary>
-    ///     Visual layers currently hidden. This will affect the base sprite
-    ///     on this humanoid layer, and any markings that sit above it.
+    ///     A map of the visual layers currently hidden to the equipment
+    ///     slots that are currently hiding them. This will affect the base
+    ///     sprite on this humanoid layer, and any markings that sit above it.
     /// </summary>
     [DataField, AutoNetworkedField]
-    public HashSet<HumanoidVisualLayers> HiddenLayers = new();
+    public Dictionary<HumanoidVisualLayers, SlotFlags> HiddenLayers = new();
 
     [DataField, AutoNetworkedField]
     public Sex Sex = Sex.Male;
index 8133ca4c98adee5b57c966d04aec09d005989ca9..a3f62fefe8394b4c9001f879639ad9781417b594 100644 (file)
@@ -1,11 +1,13 @@
 using System.IO;
 using System.Linq;
+using System.Numerics;
 using Content.Shared.CCVar;
 using Content.Shared.Decals;
 using Content.Shared.Examine;
 using Content.Shared.Humanoid.Markings;
 using Content.Shared.Humanoid.Prototypes;
 using Content.Shared.IdentityManagement;
+using Content.Shared.Inventory;
 using Content.Shared.Preferences;
 using Robust.Shared;
 using Robust.Shared.Configuration;
@@ -114,22 +116,22 @@ public abstract class SharedHumanoidAppearanceSystem : EntitySystem
     /// <summary>
     ///     Toggles a humanoid's sprite layer visibility.
     /// </summary>
-    /// <param name="uid">Humanoid mob's UID</param>
+    /// <param name="ent">Humanoid entity</param>
     /// <param name="layer">Layer to toggle visibility for</param>
-    /// <param name="humanoid">Humanoid component of the entity</param>
-    public void SetLayerVisibility(EntityUid uid,
+    /// <param name="visible">Whether to hide or show the layer. If more than once piece of clothing is hiding the layer, it may remain hidden.</param>
+    /// <param name="source">Equipment slot that has the clothing that is (or was) hiding the layer. If not specified, the change is "permanent" (i.e., see <see cref="HumanoidAppearanceComponent.PermanentlyHidden"/>)</param>
+    public void SetLayerVisibility(Entity<HumanoidAppearanceComponent?> ent,
         HumanoidVisualLayers layer,
         bool visible,
-        bool permanent = false,
-        HumanoidAppearanceComponent? humanoid = null)
+        SlotFlags? source = null)
     {
-        if (!Resolve(uid, ref humanoid, false))
+        if (!Resolve(ent.Owner, ref ent.Comp, false))
             return;
 
         var dirty = false;
-        SetLayerVisibility(uid, humanoid, layer, visible, permanent, ref dirty);
+        SetLayerVisibility(ent!, layer, visible, source, ref dirty);
         if (dirty)
-            Dirty(uid, humanoid);
+            Dirty(ent);
     }
 
     /// <summary>
@@ -163,49 +165,75 @@ public abstract class SharedHumanoidAppearanceSystem : EntitySystem
     /// <summary>
     ///     Sets the visibility for multiple layers at once on a humanoid's sprite.
     /// </summary>
-    /// <param name="uid">Humanoid mob's UID</param>
+    /// <param name="ent">Humanoid entity</param>
     /// <param name="layers">An enumerable of all sprite layers that are going to have their visibility set</param>
     /// <param name="visible">The visibility state of the layers given</param>
-    /// <param name="permanent">If this is a permanent change, or temporary. Permanent layers are stored in their own hash set.</param>
-    /// <param name="humanoid">Humanoid component of the entity</param>
-    public void SetLayersVisibility(EntityUid uid, IEnumerable<HumanoidVisualLayers> layers, bool visible, bool permanent = false,
-        HumanoidAppearanceComponent? humanoid = null)
+    public void SetLayersVisibility(Entity<HumanoidAppearanceComponent?> ent,
+        IEnumerable<HumanoidVisualLayers> layers,
+        bool visible)
     {
-        if (!Resolve(uid, ref humanoid))
+        if (!Resolve(ent.Owner, ref ent.Comp, false))
             return;
 
         var dirty = false;
 
         foreach (var layer in layers)
         {
-            SetLayerVisibility(uid, humanoid, layer, visible, permanent, ref dirty);
+            SetLayerVisibility(ent!, layer, visible, null, ref dirty);
         }
 
         if (dirty)
-            Dirty(uid, humanoid);
+            Dirty(ent);
     }
 
-    protected virtual void SetLayerVisibility(
-        EntityUid uid,
-        HumanoidAppearanceComponent humanoid,
+    /// <inheritdoc cref="SetLayerVisibility(Entity{HumanoidAppearanceComponent?},HumanoidVisualLayers,bool,Nullable{SlotFlags})"/>
+    public virtual void SetLayerVisibility(
+        Entity<HumanoidAppearanceComponent> ent,
         HumanoidVisualLayers layer,
         bool visible,
-        bool permanent,
+        SlotFlags? source,
         ref bool dirty)
     {
+#if DEBUG
+        if (source is {} s)
+        {
+            DebugTools.AssertNotEqual(s, SlotFlags.NONE);
+            // Check that only a single bit in the bitflag is set
+            var powerOfTwo = BitOperations.RoundUpToPowerOf2((uint)s);
+            DebugTools.AssertEqual((uint)s, powerOfTwo);
+        }
+#endif
+
         if (visible)
         {
-            if (permanent)
-                dirty |= humanoid.PermanentlyHidden.Remove(layer);
+            if (source is not {} slot)
+            {
+                dirty |= ent.Comp.PermanentlyHidden.Remove(layer);
+            }
+            else if (ent.Comp.HiddenLayers.TryGetValue(layer, out var oldSlots))
+            {
+                // This layer might be getting hidden by more than one piece of equipped clothing.
+                // remove slot flag from the set of slots hiding this layer, then check if there are any left.
+                ent.Comp.HiddenLayers[layer] = ~slot & oldSlots;
+                if (ent.Comp.HiddenLayers[layer] == SlotFlags.NONE)
+                    ent.Comp.HiddenLayers.Remove(layer);
 
-            dirty |= humanoid.HiddenLayers.Remove(layer);
+                dirty |= (oldSlots & slot) != 0;
+            }
         }
         else
         {
-            if (permanent)
-                dirty |= humanoid.PermanentlyHidden.Add(layer);
+            if (source is not { } slot)
+            {
+                dirty |= ent.Comp.PermanentlyHidden.Add(layer);
+            }
+            else
+            {
+                var oldSlots = ent.Comp.HiddenLayers.GetValueOrDefault(layer);
+                ent.Comp.HiddenLayers[layer] = slot | oldSlots;
+                dirty |= (oldSlots & slot) != slot;
+            }
 
-            dirty |= humanoid.HiddenLayers.Add(layer);
         }
     }
 
index ef1c50f63ce183c3ccf5dbc61ab41cc386bca372..6d6916df32f2a84b3ca34172d6c4f2230a89ef40 100644 (file)
@@ -37,7 +37,7 @@ public abstract class SharedIdentitySystem : EntitySystem
 
     private void OnMaskToggled(Entity<IdentityBlockerComponent> ent, ref ItemMaskToggledEvent args)
     {
-        ent.Comp.Enabled = !args.IsToggled;
+        ent.Comp.Enabled = !args.Mask.Comp.IsToggled;
     }
 }
 /// <summary>
index 0f519fdcf8a53430c75938174bc02909767d3cd5..871f43457962e5229bde60d1ae209eed8bc19b57 100644 (file)
@@ -7,6 +7,7 @@
   - type: Foldable
     canFoldInsideContainer: true
   - type: FoldableClothing
+    foldedEquippedPrefix: "" # folding the bandana will toggles the mask, which adds the toggled prefix. This overrides that prefix.
     foldedSlots:
     - HEAD
     unfoldedSlots:
index 4ba9ecdcaa2682a54f47e46db549651572d2a10c..7d0441ee3d113fd15e5b57c17df403d3aea1f26f 100644 (file)
@@ -16,8 +16,8 @@
     - HamsterWearable
     - WhitelistChameleon
   - type: HideLayerClothing
-    slots:
-    - Snout
+    layers:
+      Snout: Mask
     hideOnToggle: true
 
 - type: entity