Fix broken layer hiding on clothes with multiple equipment slots (#34080)

* 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>
This commit is contained in:
paige404
2025-03-20 09:30:47 -04:00
committed by GitHub
parent 65f393bb14
commit 2e7f01b99e
20 changed files with 370 additions and 184 deletions

View 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)

View 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);
}
}
}

View 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(

View 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);
}
}
}

View 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;
}
}

View 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,

View 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;

View 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();
}

View 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)

View 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;

View 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)

View 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;
}

View File

@@ -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;
}
}

View 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))
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);
}
/// <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)
{
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<MaskComponent> 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<MaskComponent?> 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<MaskComponent?> 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);
}
}

View 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.

View 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;

View 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);
}
}

View 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>

View 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:

View File

@@ -16,8 +16,8 @@
- HamsterWearable
- WhitelistChameleon
- type: HideLayerClothing
slots:
- Snout
layers:
Snout: Mask
hideOnToggle: true
- type: entity