diff --git a/Content.Client/Clothing/ClientClothingSystem.cs b/Content.Client/Clothing/ClientClothingSystem.cs index 46f879e815..5db7209d22 100644 --- a/Content.Client/Clothing/ClientClothingSystem.cs +++ b/Content.Client/Clothing/ClientClothingSystem.cs @@ -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) diff --git a/Content.Client/Humanoid/HumanoidAppearanceSystem.cs b/Content.Client/Humanoid/HumanoidAppearanceSystem.cs index 2d532968de..25c16ffd83 100644 --- a/Content.Client/Humanoid/HumanoidAppearanceSystem.cs +++ b/Content.Client/Humanoid/HumanoidAppearanceSystem.cs @@ -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(); - humanoid.HiddenLayers = new HashSet(); + humanoid.HiddenLayers = new Dictionary(); 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 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(uid); + var sprite = Comp(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); } } } diff --git a/Content.Server/Body/Systems/BodySystem.cs b/Content.Server/Body/Systems/BodySystem.cs index 4279f3ed2b..d41f2d5769 100644 --- a/Content.Server/Body/Systems/BodySystem.cs +++ b/Content.Server/Body/Systems/BodySystem.cs @@ -65,15 +65,11 @@ public sealed class BodySystem : SharedBodySystem // TODO: Predict this probably. base.AddPart(bodyEnt, partEnt, slotId); - if (TryComp(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 GibBody( diff --git a/Content.Server/Body/Systems/LungSystem.cs b/Content.Server/Body/Systems/LungSystem.cs index 859618ae1a..82ec490a4a 100644 --- a/Content.Server/Body/Systems/LungSystem.cs +++ b/Content.Server/Body/Systems/LungSystem.cs @@ -58,7 +58,7 @@ public sealed class LungSystem : EntitySystem private void OnMaskToggled(Entity 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); } } } diff --git a/Content.Server/Nutrition/EntitySystems/IngestionBlockerSystem.cs b/Content.Server/Nutrition/EntitySystems/IngestionBlockerSystem.cs index ede1c21680..63b39fb524 100644 --- a/Content.Server/Nutrition/EntitySystems/IngestionBlockerSystem.cs +++ b/Content.Server/Nutrition/EntitySystems/IngestionBlockerSystem.cs @@ -14,6 +14,6 @@ public sealed class IngestionBlockerSystem : EntitySystem private void OnBlockerMaskToggled(Entity ent, ref ItemMaskToggledEvent args) { - ent.Comp.Enabled = !args.IsToggled; + ent.Comp.Enabled = !args.Mask.Comp.IsToggled; } } diff --git a/Content.Shared/Clothing/ClothingEvents.cs b/Content.Shared/Clothing/ClothingEvents.cs index 83afea4597..9b0b69d186 100644 --- a/Content.Shared/Clothing/ClothingEvents.cs +++ b/Content.Shared/Clothing/ClothingEvents.cs @@ -65,13 +65,13 @@ public sealed partial class ToggleMaskEvent : InstantActionEvent { } /// Event raised on the mask entity when it is toggled. /// [ByRefEvent] -public readonly record struct ItemMaskToggledEvent(EntityUid Wearer, string? equippedPrefix, bool IsToggled, bool IsEquip); +public readonly record struct ItemMaskToggledEvent(Entity Mask, EntityUid? Wearer); /// /// Event raised on the entity wearing the mask when it is toggled. /// [ByRefEvent] -public readonly record struct WearerMaskToggledEvent(bool IsToggled); +public readonly record struct WearerMaskToggledEvent(Entity Mask); /// /// Raised on the clothing entity when it is equipped to a valid slot, diff --git a/Content.Shared/Clothing/Components/ClothingComponent.cs b/Content.Shared/Clothing/Components/ClothingComponent.cs index 4f8058dbf5..260af210e0 100644 --- a/Content.Shared/Clothing/Components/ClothingComponent.cs +++ b/Content.Shared/Clothing/Components/ClothingComponent.cs @@ -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; + /// + /// 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. + /// + /// + /// Note that this may be a combination of different slot flags, not a singular bit. + /// [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; /// - /// 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 flags. /// + [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 + + /// + /// Slot flags of the slot the clothing is currently in. See also . + /// + [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; diff --git a/Content.Shared/Clothing/Components/FoldableClothingComponent.cs b/Content.Shared/Clothing/Components/FoldableClothingComponent.cs index ffcb52b457..7b03adcc8d 100644 --- a/Content.Shared/Clothing/Components/FoldableClothingComponent.cs +++ b/Content.Shared/Clothing/Components/FoldableClothingComponent.cs @@ -19,7 +19,6 @@ public sealed partial class FoldableClothingComponent : Component [DataField] public SlotFlags? UnfoldedSlots; - /// /// What equipped prefix does this have while in folded form? /// @@ -36,11 +35,11 @@ public sealed partial class FoldableClothingComponent : Component /// Which layers does this hide when Unfolded? See and /// [DataField] - public HashSet UnfoldedHideLayers = new(); + public HashSet? UnfoldedHideLayers = new(); /// /// Which layers does this hide when folded? See and /// [DataField] - public HashSet FoldedHideLayers = new(); + public HashSet? FoldedHideLayers = new(); } diff --git a/Content.Shared/Clothing/Components/HideLayerClothingComponent.cs b/Content.Shared/Clothing/Components/HideLayerClothingComponent.cs index ac3d9b9789..b5445c28b9 100644 --- a/Content.Shared/Clothing/Components/HideLayerClothingComponent.cs +++ b/Content.Shared/Clothing/Components/HideLayerClothingComponent.cs @@ -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 { /// - /// The appearance layer to hide. + /// The appearance layer(s) to hide. Use Layers instead. /// [DataField] - public HashSet Slots = new(); + [Obsolete("This attribute is deprecated, please use Layers instead.")] + public HashSet? Slots; + + /// + /// A map of the appearance layer(s) to hide, and the equipment slot that should hide them. + /// + [DataField] + public Dictionary Layers = new(); /// /// If true, the layer will only hide when the item is in a toggled state (e.g. masks) diff --git a/Content.Shared/Clothing/Components/MaskComponent.cs b/Content.Shared/Clothing/Components/MaskComponent.cs index 47f2fd3079..985219df2e 100644 --- a/Content.Shared/Clothing/Components/MaskComponent.cs +++ b/Content.Shared/Clothing/Components/MaskComponent.cs @@ -8,15 +8,22 @@ namespace Content.Shared.Clothing.Components; [Access(typeof(MaskSystem))] public sealed partial class MaskComponent : Component { + /// + /// Action for toggling a mask (e.g., pulling the mask down or putting it back up) + /// [DataField, AutoNetworkedField] public EntProtoId ToggleAction = "ActionToggleMask"; /// - /// This mask can be toggled (pulled up/down) + /// Action for toggling a mask (e.g., pulling the mask down or putting it back up) /// [DataField, AutoNetworkedField] public EntityUid? ToggleActionEntity; + /// + /// Whether the mask is currently toggled (e.g., pulled down). + /// This generally disables some of the mask's functionality. + /// [DataField, AutoNetworkedField] public bool IsToggled; @@ -27,13 +34,13 @@ public sealed partial class MaskComponent : Component public string EquippedPrefix = "up"; /// - /// When will function normally, otherwise will not react to events + /// When , the mask will not be toggleable. /// [DataField("enabled"), AutoNetworkedField] - public bool IsEnabled = true; + public bool IsToggleable = true; /// - /// When will disable when folded + /// When will disable when folded /// [DataField, AutoNetworkedField] public bool DisableOnFolded; diff --git a/Content.Shared/Clothing/EntitySystems/ClothingSystem.cs b/Content.Shared/Clothing/EntitySystems/ClothingSystem.cs index 3b26360f10..f8ab79ec78 100644 --- a/Content.Shared/Clothing/EntitySystems/ClothingSystem.cs +++ b/Content.Shared/Clothing/EntitySystems/ClothingSystem.cs @@ -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(OnHandleState); SubscribeLocalEvent(OnGotEquipped); SubscribeLocalEvent(OnGotUnequipped); - SubscribeLocalEvent(OnMaskToggled); SubscribeLocalEvent(OnEquipDoAfter); SubscribeLocalEvent(OnUnequipDoAfter); @@ -85,59 +84,19 @@ public abstract class ClothingSystem : EntitySystem } } - private void ToggleVisualLayers(EntityUid equipee, HashSet layers, HashSet 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 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 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) diff --git a/Content.Shared/Clothing/EntitySystems/FoldableClothingSystem.cs b/Content.Shared/Clothing/EntitySystems/FoldableClothingSystem.cs index 603af4099c..a60caa454b 100644 --- a/Content.Shared/Clothing/EntitySystems/FoldableClothingSystem.cs +++ b/Content.Shared/Clothing/EntitySystems/FoldableClothingSystem.cs @@ -16,7 +16,8 @@ public sealed class FoldableClothingSystem : EntitySystem base.Initialize(); SubscribeLocalEvent(OnFoldAttempt); - SubscribeLocalEvent(OnFolded); + SubscribeLocalEvent(OnFolded, + after: [typeof(MaskSystem)]); // Mask system also modifies clothing / equipment RSI state prefixes. } private void OnFoldAttempt(Entity 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(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(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(ent.Owner, out var hideLayerComp)) + // TODO CLOTHING fix this. + if (ent.Comp.UnfoldedHideLayers != null && TryComp(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 index 0000000000..323884ab36 --- /dev/null +++ b/Content.Shared/Clothing/EntitySystems/HideLayerClothingSystem.cs @@ -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(OnHideGotUnequipped); + SubscribeLocalEvent(OnHideGotEquipped); + SubscribeLocalEvent(OnHideToggled); + } + + private void OnHideToggled(Entity ent, ref ItemMaskToggledEvent args) + { + if (args.Wearer != null) + SetLayerVisibility(ent!, args.Wearer.Value, hideLayers: true); + } + + private void OnHideGotEquipped(Entity ent, ref ClothingGotEquippedEvent args) + { + SetLayerVisibility(ent!, args.Wearer, hideLayers: true); + } + + private void OnHideGotUnequipped(Entity ent, ref ClothingGotUnequippedEvent args) + { + SetLayerVisibility(ent!, args.Wearer, hideLayers: false); + } + + private void SetLayerVisibility( + Entity clothing, + Entity 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 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; + } +} diff --git a/Content.Shared/Clothing/EntitySystems/MaskSystem.cs b/Content.Shared/Clothing/EntitySystems/MaskSystem.cs index fd0b7e782b..3e899f3dc3 100644 --- a/Content.Shared/Clothing/EntitySystems/MaskSystem.cs +++ b/Content.Shared/Clothing/EntitySystems/MaskSystem.cs @@ -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 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)) - return; + // Masks are currently only toggleable via the action while equipped. + // Its possible this might change in future? - mask.IsToggled ^= true; + // 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; + } + + 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) - return; - - mask.IsToggled = false; - ToggleMaskComponents(uid, mask, args.Equipee, mask.EquippedPrefix, true); - } - - /// - /// Called after setting IsToggled, raises events and dirties. - /// - private void ToggleMaskComponents(EntityUid uid, MaskComponent mask, EntityUid wearer, string? equippedPrefix = null, bool isEquip = false) - { - Dirty(uid, mask); - if (mask.ToggleActionEntity is {} action) - _actionSystem.SetToggled(action, mask.IsToggled); - - var maskEv = new ItemMaskToggledEvent(wearer, equippedPrefix, mask.IsToggled, isEquip); - RaiseLocalEvent(uid, ref maskEv); - - var wearerEv = new WearerMaskToggledEvent(mask.IsToggled); - RaiseLocalEvent(wearer, ref wearerEv); + // Masks are currently always un-toggled when unequipped. + SetToggled((uid, mask), false); } private void OnFolded(Entity ent, ref FoldedEvent args) { - if (ent.Comp.DisableOnFolded) - ent.Comp.IsEnabled = !args.IsFolded; - ent.Comp.IsToggled = args.IsFolded; + // See FoldableClothingComponent - ToggleMaskComponents(ent.Owner, ent.Comp, ent.Owner); + if (!ent.Comp.DisableOnFolded) + return; + + // 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); + } + + public void SetToggled(Entity mask, bool toggled, bool force = false) + { + if (_timing.ApplyingState) + return; + + if (!Resolve(mask.Owner, ref mask.Comp)) + return; + + if (!force && !mask.Comp.IsToggleable) + return; + + if (mask.Comp.IsToggled == toggled) + return; + + 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); + } + + public void SetToggleable(Entity mask, bool toggleable) + { + 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); + + mask.Comp.IsToggleable = toggleable; + Dirty(mask); } } diff --git a/Content.Shared/Foldable/FoldableSystem.cs b/Content.Shared/Foldable/FoldableSystem.cs index c251372372..3ece56720a 100644 --- a/Content.Shared/Foldable/FoldableSystem.cs +++ b/Content.Shared/Foldable/FoldableSystem.cs @@ -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 /// /// [ByRefEvent] -public record struct FoldAttemptEvent(bool Cancelled = false); +public record struct FoldAttemptEvent(FoldableComponent Comp, bool Cancelled = false); /// /// Event raised on an entity after it has been folded. diff --git a/Content.Shared/Humanoid/HumanoidAppearanceComponent.cs b/Content.Shared/Humanoid/HumanoidAppearanceComponent.cs index 0bf11f5762..4a741074c9 100644 --- a/Content.Shared/Humanoid/HumanoidAppearanceComponent.cs +++ b/Content.Shared/Humanoid/HumanoidAppearanceComponent.cs @@ -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"); /// - /// 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. /// [DataField, AutoNetworkedField] - public HashSet HiddenLayers = new(); + public Dictionary HiddenLayers = new(); [DataField, AutoNetworkedField] public Sex Sex = Sex.Male; diff --git a/Content.Shared/Humanoid/SharedHumanoidAppearanceSystem.cs b/Content.Shared/Humanoid/SharedHumanoidAppearanceSystem.cs index 8133ca4c98..a3f62fefe8 100644 --- a/Content.Shared/Humanoid/SharedHumanoidAppearanceSystem.cs +++ b/Content.Shared/Humanoid/SharedHumanoidAppearanceSystem.cs @@ -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 /// /// Toggles a humanoid's sprite layer visibility. /// - /// Humanoid mob's UID + /// Humanoid entity /// Layer to toggle visibility for - /// Humanoid component of the entity - public void SetLayerVisibility(EntityUid uid, + /// Whether to hide or show the layer. If more than once piece of clothing is hiding the layer, it may remain hidden. + /// Equipment slot that has the clothing that is (or was) hiding the layer. If not specified, the change is "permanent" (i.e., see ) + public void SetLayerVisibility(Entity 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); } /// @@ -163,49 +165,75 @@ public abstract class SharedHumanoidAppearanceSystem : EntitySystem /// /// Sets the visibility for multiple layers at once on a humanoid's sprite. /// - /// Humanoid mob's UID + /// Humanoid entity /// An enumerable of all sprite layers that are going to have their visibility set /// The visibility state of the layers given - /// If this is a permanent change, or temporary. Permanent layers are stored in their own hash set. - /// Humanoid component of the entity - public void SetLayersVisibility(EntityUid uid, IEnumerable layers, bool visible, bool permanent = false, - HumanoidAppearanceComponent? humanoid = null) + public void SetLayersVisibility(Entity ent, + IEnumerable 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, + /// + public virtual void SetLayerVisibility( + Entity 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); } } diff --git a/Content.Shared/IdentityManagement/SharedIdentitySystem.cs b/Content.Shared/IdentityManagement/SharedIdentitySystem.cs index ef1c50f63c..6d6916df32 100644 --- a/Content.Shared/IdentityManagement/SharedIdentitySystem.cs +++ b/Content.Shared/IdentityManagement/SharedIdentitySystem.cs @@ -37,7 +37,7 @@ public abstract class SharedIdentitySystem : EntitySystem private void OnMaskToggled(Entity ent, ref ItemMaskToggledEvent args) { - ent.Comp.Enabled = !args.IsToggled; + ent.Comp.Enabled = !args.Mask.Comp.IsToggled; } } /// diff --git a/Resources/Prototypes/Entities/Clothing/Masks/bandanas.yml b/Resources/Prototypes/Entities/Clothing/Masks/bandanas.yml index 0f519fdcf8..871f434579 100644 --- a/Resources/Prototypes/Entities/Clothing/Masks/bandanas.yml +++ b/Resources/Prototypes/Entities/Clothing/Masks/bandanas.yml @@ -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: diff --git a/Resources/Prototypes/Entities/Clothing/Masks/masks.yml b/Resources/Prototypes/Entities/Clothing/Masks/masks.yml index 4ba9ecdcaa..7d0441ee3d 100644 --- a/Resources/Prototypes/Entities/Clothing/Masks/masks.yml +++ b/Resources/Prototypes/Entities/Clothing/Masks/masks.yml @@ -16,8 +16,8 @@ - HamsterWearable - WhitelistChameleon - type: HideLayerClothing - slots: - - Snout + layers: + Snout: Mask hideOnToggle: true - type: entity