var state = $"equipped-{correctedSlot}";
- if (clothing.EquippedPrefix != null)
+ if (!string.IsNullOrEmpty(clothing.EquippedPrefix))
state = $"{clothing.EquippedPrefix}-equipped-{correctedSlot}";
if (clothing.EquippedState != null)
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;
}
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)
{
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;
}
}
- 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];
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);
}
}
}
// 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);
}
}
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(
private void OnMaskToggled(Entity<BreathToolComponent> ent, ref ItemMaskToggledEvent args)
{
- if (args.IsToggled || args.IsEquip)
+ if (args.Mask.Comp.IsToggled)
{
_atmos.DisconnectInternals(ent);
}
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);
}
}
}
private void OnBlockerMaskToggled(Entity<IngestionBlockerComponent> ent, ref ItemMaskToggledEvent args)
{
- ent.Comp.Enabled = !args.IsToggled;
+ ent.Comp.Enabled = !args.Mask.Comp.IsToggled;
}
}
/// 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,
+using System.Diagnostics.CodeAnalysis;
using Content.Shared.Clothing.EntitySystems;
using Content.Shared.DoAfter;
using Content.Shared.Inventory;
[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;
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;
[DataField]
public SlotFlags? UnfoldedSlots;
-
/// <summary>
/// What equipped prefix does this have while in folded form?
/// </summary>
/// 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();
}
using Content.Shared.Humanoid;
+using Content.Shared.Inventory;
using Robust.Shared.GameStates;
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)
[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;
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;
{
[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()
{
SubscribeLocalEvent<ClothingComponent, ComponentHandleState>(OnHandleState);
SubscribeLocalEvent<ClothingComponent, GotEquippedEvent>(OnGotEquipped);
SubscribeLocalEvent<ClothingComponent, GotUnequippedEvent>(OnGotUnequipped);
- SubscribeLocalEvent<ClothingComponent, ItemMaskToggledEvent>(OnMaskToggled);
SubscribeLocalEvent<ClothingComponent, ClothingEquipDoAfterEvent>(OnEquipDoAfter);
SubscribeLocalEvent<ClothingComponent, ClothingUnequipDoAfterEvent>(OnUnequipDoAfter);
}
}
- 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)
}
component.InSlot = null;
- CheckEquipmentForLayerHide(args.Equipment, args.Equipee);
+ component.InSlotFlag = null;
}
private void OnGetState(EntityUid uid, ClothingComponent component, ref ComponentGetState args)
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)
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)
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)
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;
}
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;
}
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;
}
--- /dev/null
+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;
+ }
+}
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;
[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()
{
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);
}
}
if (_container.IsEntityInContainer(uid) && !fold.CanFoldInsideContainer)
return false;
- var ev = new FoldAttemptEvent();
+ var ev = new FoldAttemptEvent(fold);
RaiseLocalEvent(uid, ref ev);
return !ev.Cancelled;
}
/// </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.
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;
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;
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;
/// <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>
/// <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);
}
}
private void OnMaskToggled(Entity<IdentityBlockerComponent> ent, ref ItemMaskToggledEvent args)
{
- ent.Comp.Enabled = !args.IsToggled;
+ ent.Comp.Enabled = !args.Mask.Comp.IsToggled;
}
}
/// <summary>
- 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:
- HamsterWearable
- WhitelistChameleon
- type: HideLayerClothing
- slots:
- - Snout
+ layers:
+ Snout: Mask
hideOnToggle: true
- type: entity