private void OnMarkingsChangedVisibility(Entity<VisualOrganMarkingsComponent> ent, ref BodyRelayedEvent<HumanoidLayerVisibilityChangedEvent> args)
{
+ if (!ent.Comp.HideableLayers.Contains(args.Args.Layer))
+ return;
+
foreach (var markings in ent.Comp.Markings.Values)
{
foreach (var marking in markings)
if (!_marking.TryGetMarking(marking, out var proto))
continue;
- if (proto.BodyPart != args.Args.Layer)
+ if (proto.BodyPart != args.Args.Layer && !(ent.Comp.DependentHidingLayers.TryGetValue(args.Args.Layer, out var dependent) && dependent.Contains(proto.BodyPart)))
continue;
foreach (var sprite in proto.Sprites)
UpdateSprite(ent);
}
- public override void SetLayerVisibility(
+ public override void SetLayerOcclusion(
Entity<HideableHumanoidLayersComponent?> ent,
HumanoidVisualLayers layer,
bool visible,
SlotFlags source)
{
- base.SetLayerVisibility(ent, layer, visible, source);
+ base.SetLayerOcclusion(ent, layer, visible, source);
if (Resolve(ent, ref ent.Comp))
UpdateSprite((ent, ent.Comp));
--- /dev/null
+using System.Collections.Generic;
+using Content.IntegrationTests.Tests.Interaction;
+using Content.Shared.Body;
+using Content.Shared.Humanoid;
+using Content.Shared.Humanoid.Markings;
+using Content.Shared.Inventory;
+using Robust.Client.GameObjects;
+
+namespace Content.IntegrationTests.Tests.Humanoid;
+
+[TestOf(typeof(SharedHideableHumanoidLayersSystem))]
+public sealed class HideableHumanoidLayersTest : InteractionTest
+{
+ protected override string PlayerPrototype => "MobVulpkanin";
+
+ [Test]
+ public async Task BasicHiding()
+ {
+ await SpawnTarget("ClothingMaskGas");
+ await Pickup(); // equip mask
+ await UseInHand();
+
+ await Server.WaitAssertion(() =>
+ {
+ var hideableHumanoidLayers = SEntMan.GetComponent<HideableHumanoidLayersComponent>(SPlayer);
+ Assert.That(hideableHumanoidLayers.HiddenLayers, Does.ContainKey(HumanoidVisualLayers.Snout).WithValue(SlotFlags.MASK));
+ });
+
+ await Server.WaitAssertion(() =>
+ {
+ SEntMan.DeleteEntity(STarget); // de-equip mask
+
+ var hideableHumanoidLayers = SEntMan.GetComponent<HideableHumanoidLayersComponent>(SPlayer);
+ Assert.That(hideableHumanoidLayers.HiddenLayers, Does.Not.ContainKey(HumanoidVisualLayers.Snout));
+ });
+ }
+
+ [Test]
+ public async Task DependentHiding()
+ {
+ await Server.WaitAssertion(() =>
+ {
+ var visualBody = SEntMan.System<SharedVisualBodySystem>();
+ visualBody.ApplyMarkings(SPlayer, new()
+ {
+ ["Head"] = new()
+ {
+ [HumanoidVisualLayers.SnoutCover] = new List<Marking>() { new("VulpSnoutNose", 1) },
+ },
+ });
+ });
+
+ await SpawnTarget("ClothingMaskGas");
+ await Pickup(); // equip mask
+ await UseInHand();
+
+ await RunTicks(20);
+
+ await Client.WaitAssertion(() =>
+ {
+ var spriteSystem = CEntMan.System<SpriteSystem>();
+ var snoutIndex = spriteSystem.LayerMapGet(CPlayer, "VulpSnout-snout");
+ var snoutCoverIndex = spriteSystem.LayerMapGet(CPlayer, "VulpSnoutNose-snout-nose");
+ var spriteComp = CEntMan.GetComponent<SpriteComponent>(CPlayer);
+
+ Assert.That(spriteComp[snoutIndex].Visible, Is.False);
+ Assert.That(spriteComp[snoutCoverIndex].Visible, Is.False);
+ });
+
+ await Server.WaitAssertion(() =>
+ {
+ SEntMan.DeleteEntity(STarget); // de-equip mask
+ });
+
+ await RunTicks(20);
+
+ await Client.WaitAssertion(() =>
+ {
+ var spriteSystem = CEntMan.System<SpriteSystem>();
+ var snoutIndex = spriteSystem.LayerMapGet(CPlayer, "VulpSnout-snout");
+ var snoutCoverIndex = spriteSystem.LayerMapGet(CPlayer, "VulpSnoutNose-snout-nose");
+ var spriteComp = CEntMan.GetComponent<SpriteComponent>(CPlayer);
+
+ Assert.That(spriteComp[snoutIndex].Visible, Is.True);
+ Assert.That(spriteComp[snoutCoverIndex].Visible, Is.True);
+ });
+ }
+}
--- /dev/null
+using System.Collections.Generic;
+using System.Linq;
+using Content.Shared.Body;
+using Content.Shared.Clothing.Components;
+using Content.Shared.Humanoid;
+using Robust.Shared.Prototypes;
+
+namespace Content.IntegrationTests.Tests.Humanoid;
+
+[TestFixture]
+public sealed class HideablePrototypeValidation
+{
+ [Test]
+ public async Task NoOrgansWithoutClothing()
+ {
+ await using var pair = await PoolManager.GetServerClient();
+
+ var requirements = new Dictionary<Enum, HashSet<EntProtoId>>();
+ foreach (var (proto, component) in pair.GetPrototypesWithComponent<VisualOrganMarkingsComponent>())
+ {
+ foreach (var layer in component.HideableLayers)
+ {
+ requirements[layer] = requirements.GetValueOrDefault(layer) ?? [];
+ requirements[layer].Add(proto.ID);
+ }
+ }
+
+ var provided = new HashSet<HumanoidVisualLayers>();
+ foreach (var (_, component) in pair.GetPrototypesWithComponent<HideLayerClothingComponent>())
+ {
+#pragma warning disable CS0618 // Type or member is obsolete
+ if (component.Slots is { } slots)
+ {
+ provided.UnionWith(slots);
+ }
+ provided.UnionWith(component.Layers.Keys);
+#pragma warning restore CS0618 // Type or member is obsolete
+ }
+
+ using var scope = Assert.EnterMultipleScope();
+ foreach (var (key, requirement) in requirements)
+ {
+ Assert.That(provided, Does.Contain(key), $"No clothing will hide {key} that can be hidden on {string.Join(", ", requirement.Select(it => it.Id))}");
+ }
+
+ await pair.CleanReturnAsync();
+ }
+
+ [Test]
+ public async Task NoClothingWithoutOrgans()
+ {
+ await using var pair = await PoolManager.GetServerClient();
+
+ var requirements = new Dictionary<Enum, HashSet<EntProtoId>>();
+ foreach (var (proto, component) in pair.GetPrototypesWithComponent<HideLayerClothingComponent>())
+ {
+#pragma warning disable CS0618 // Type or member is obsolete
+ foreach (var layer in component.Layers.Keys.Concat(component.Slots ?? []))
+#pragma warning restore CS0618 // Type or member is obsolete
+ {
+ requirements[layer] = requirements.GetValueOrDefault(layer) ?? [];
+ requirements[layer].Add(proto.ID);
+ }
+ }
+
+ var provided = new HashSet<Enum>();
+ foreach (var (_, component) in pair.GetPrototypesWithComponent<VisualOrganMarkingsComponent>())
+ {
+ provided.UnionWith(component.HideableLayers);
+ }
+
+ using var scope = Assert.EnterMultipleScope();
+ foreach (var (key, requirement) in requirements)
+ {
+ Assert.That(provided, Does.Contain(key), $"No organ will hide {key} that can be hidden by {string.Join(", ", requirement.Select(it => it.Id))}");
+ }
+
+ await pair.CleanReturnAsync();
+ }
+}
[DataField, AutoNetworkedField]
public Dictionary<HumanoidVisualLayers, List<Marking>> Markings = new();
+ /// <summary>
+ /// Layers that are eligible for hiding based on e.g. clothing
+ /// </summary>
+ [DataField, AutoNetworkedField]
+ public HashSet<Enum> HideableLayers = new();
+
+ /// <summary>
+ /// A dictionary of layers to other layers that visually depend on them for hiding, e.g. SnoutCover depends on Snout
+ /// </summary>
+ [DataField, AutoNetworkedField]
+ public Dictionary<Enum, HashSet<Enum>> DependentHidingLayers = new();
+
/// <summary>
/// Client only - the last markings applied by this component
/// </summary>
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 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))
- _hideableHumanoidLayers.SetLayerVisibility(user, layer, !hideLayers, inSlot);
+ _hideableHumanoidLayers.SetLayerOcclusion(user, layer, hideLayers, inSlot);
}
// Fallback for obsolete field: assume we want to hide **all** layers, as long as we are equipped to any
{
foreach (var layer in slots)
{
- if (hideable.Contains(layer))
- _hideableHumanoidLayers.SetLayerVisibility(user, layer, !hideLayers, inSlot);
+ _hideableHumanoidLayers.SetLayerOcclusion(user, layer, hideLayers, inSlot);
}
}
}
[DataField, AutoNetworkedField]
public Dictionary<HumanoidVisualLayers, SlotFlags> HiddenLayers = new();
- /// <summary>
- /// Which layers of this humanoid that should be hidden on equipping a corresponding item..
- /// </summary>
- [DataField]
- public HashSet<HumanoidVisualLayers> HideLayersOnEquip = [HumanoidVisualLayers.Hair];
-
/// <summary>
/// Client only - which layers were last hidden
/// </summary>
/// </summary>
/// <param name="ent">Humanoid entity</param>
/// <param name="layer">Layer to toggle visibility for</param>
- /// <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="hidden">Whether to hide (true) or show (false) the layer. If more than once piece of clothing is hiding the layer, it may remain hidden.</param>
/// <param name="slot">Equipment slot that has the clothing that is (or was) hiding the layer.</param>
- public virtual void SetLayerVisibility(
+ public virtual void SetLayerOcclusion(
Entity<HideableHumanoidLayersComponent?> ent,
HumanoidVisualLayers layer,
- bool visible,
+ bool hidden,
SlotFlags slot)
{
if (!Resolve(ent, ref ent.Comp))
#endif
var dirty = false;
- if (visible)
+ if (hidden)
{
var oldSlots = ent.Comp.HiddenLayers.GetValueOrDefault(layer);
ent.Comp.HiddenLayers[layer] = slot | oldSlots;
Dirty(ent);
- var evt = new HumanoidLayerVisibilityChangedEvent(layer, visible);
+ var evt = new HumanoidLayerVisibilityChangedEvent(layer, ent.Comp.HiddenLayers.ContainsKey(layer));
RaiseLocalEvent(ent, ref evt);
}
}
- type: entity
parent: [ OrganBaseHeadSexed, OrganBaseHead, OrganHumanExternal ]
id: OrganHumanHead
+ components:
+ - type: VisualOrganMarkings
+ hideableLayers:
+ - enum.HumanoidVisualLayers.Hair
+ - enum.HumanoidVisualLayers.Snout
- type: entity
parent: [ OrganBaseArmLeft, OrganHumanExternal ]
- type: entity
parent: [ OrganBaseHead, OrganMothExternal ]
id: OrganMothHead
+ components:
+ - type: VisualOrganMarkings
+ hideableLayers:
+ - enum.HumanoidVisualLayers.HeadTop
- type: entity
parent: [ OrganBaseArmLeft, OrganMothExternal ]
- type: entity
parent: [ OrganBaseTorsoSexed, OrganBaseTorso, OrganReptilianExternal ]
id: OrganReptilianTorso
+ components:
+ - type: VisualOrganMarkings
+ hideableLayers:
+ - enum.HumanoidVisualLayers.Tail
- type: entity
parent: [ OrganBaseHeadSexed, OrganBaseHead, OrganReptilianExternal ]
id: OrganReptilianHead
+ components:
+ - type: VisualOrganMarkings
+ hideableLayers:
+ - enum.HumanoidVisualLayers.Snout
+ - enum.HumanoidVisualLayers.HeadTop
+ - enum.HumanoidVisualLayers.HeadSide
- type: entity
parent: [ OrganBaseArmLeft, OrganReptilianExternal ]
limits:
enum.HumanoidVisualLayers.Hair:
limit: 1
+ onlyGroupWhitelisted: true
required: false
enum.HumanoidVisualLayers.FacialHair:
limit: 1
+ onlyGroupWhitelisted: true
required: false
enum.HumanoidVisualLayers.Head:
limit: 4
- required: true
+ required: false
enum.HumanoidVisualLayers.Snout:
limit: 1
required: true
limit: 1
required: false
enum.HumanoidVisualLayers.LArm:
- limit: 1
- required: true
+ limit: 2
+ required: false
default: [ VoxLArmScales ]
enum.HumanoidVisualLayers.RArm:
- limit: 1
- required: true
+ limit: 2
+ required: false
default: [ VoxRArmScales ]
enum.HumanoidVisualLayers.LHand:
limit: 1
default: [ VoxRHandScales ]
enum.HumanoidVisualLayers.LLeg:
limit: 1
- required: true
+ required: false
default: [ VoxLLegScales ]
enum.HumanoidVisualLayers.RLeg:
limit: 1
- required: true
+ required: false
default: [ VoxRLegScales ]
enum.HumanoidVisualLayers.LFoot:
limit: 1
- type: entity
parent: [ OrganBaseTorso, OrganVoxExternal ]
id: OrganVoxTorso
- components:
- - type: Sprite
- state: torso
- - type: VisualOrgan
- data:
- state: torso
- type: entity
parent: [ OrganBaseHead, OrganVoxExternal ]
id: OrganVoxHead
components:
- - type: Sprite
- state: head
- - type: VisualOrgan
- data:
- state: head
+ - type: VisualOrganMarkings
+ hideableLayers:
+ - enum.HumanoidVisualLayers.Snout
+ - enum.HumanoidVisualLayers.Hair
+ - enum.HumanoidVisualLayers.FacialHair
+ dependentHidingLayers:
+ enum.HumanoidVisualLayers.Snout:
+ - enum.HumanoidVisualLayers.SnoutCover
- type: entity
parent: [ OrganBaseArmLeft, OrganVoxExternal ]
- type: entity
parent: [ OrganBaseHead, OrganVulpkaninExternal ]
id: OrganVulpkaninHead
+ components:
+ - type: VisualOrganMarkings
+ hideableLayers:
+ - enum.HumanoidVisualLayers.Snout
+ - enum.HumanoidVisualLayers.HeadTop
+ - enum.HumanoidVisualLayers.HeadSide
+ - enum.HumanoidVisualLayers.Hair
+ - enum.HumanoidVisualLayers.FacialHair
+ dependentHidingLayers:
+ enum.HumanoidVisualLayers.Snout:
+ - enum.HumanoidVisualLayers.SnoutCover
- type: entity
parent: [ OrganBaseArmLeft, OrganVulpkaninExternal ]
data:
state: head
- type: VisualOrganMarkings
+ hideableLayers:
+ - enum.HumanoidVisualLayers.Hair
markingData:
layers:
- Head
- type: ContainerContainer
- type: Appearance
- type: HideableHumanoidLayers
- hideLayersOnEquip:
- - Snout
- - SnoutCover
- - HeadTop
- - HeadSide
- - FacialHair
- - Hair
- type: UserInterface
interfaces:
enum.HumanoidMarkingModifierKey.Key:
sprites:
- sprite: Mobs/Customization/vox_tattoos.rsi
state: eyeshadow_large
-
+
- type: marking
id: VoxTattooEyeliner
bodyPart: Eyes
- type: marking
id: VoxBeakCoverStripe
- bodyPart: Snout
+ bodyPart: SnoutCover
coloring:
default:
type:
- type: marking
id: VoxBeakCoverTip
- bodyPart: Snout
+ bodyPart: SnoutCover
coloring:
default:
type: