From: pathetic meowmeow Date: Tue, 20 Jan 2026 22:24:09 +0000 (-0500) Subject: Fix hideable humanoid layers (#42553) X-Git-Url: https://git.smokeofanarchy.ru/gitweb.cgi?a=commitdiff_plain;h=0ec9975e4fe9d9be6b83673bdbb6c041a091aa4b;p=space-station-14.git Fix hideable humanoid layers (#42553) * Fix hideable humanoid layers * test maintenance coin * clean return * voxes can no longer have human beards * voxes fixes * voxing out --------- Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com> --- diff --git a/Content.Client/Body/VisualBodySystem.cs b/Content.Client/Body/VisualBodySystem.cs index 724dd22017..fba936ee58 100644 --- a/Content.Client/Body/VisualBodySystem.cs +++ b/Content.Client/Body/VisualBodySystem.cs @@ -232,6 +232,9 @@ public sealed class VisualBodySystem : SharedVisualBodySystem private void OnMarkingsChangedVisibility(Entity ent, ref BodyRelayedEvent args) { + if (!ent.Comp.HideableLayers.Contains(args.Args.Layer)) + return; + foreach (var markings in ent.Comp.Markings.Values) { foreach (var marking in markings) @@ -239,7 +242,7 @@ public sealed class VisualBodySystem : SharedVisualBodySystem 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) diff --git a/Content.Client/Humanoid/HideableHumanoidLayersSystem.cs b/Content.Client/Humanoid/HideableHumanoidLayersSystem.cs index 4feb48cbda..9d034a7a63 100644 --- a/Content.Client/Humanoid/HideableHumanoidLayersSystem.cs +++ b/Content.Client/Humanoid/HideableHumanoidLayersSystem.cs @@ -26,13 +26,13 @@ public sealed class HideableHumanoidLayersSystem : SharedHideableHumanoidLayersS UpdateSprite(ent); } - public override void SetLayerVisibility( + public override void SetLayerOcclusion( Entity 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)); diff --git a/Content.IntegrationTests/Tests/Humanoid/HideableHumanoidLayersTest.cs b/Content.IntegrationTests/Tests/Humanoid/HideableHumanoidLayersTest.cs new file mode 100644 index 0000000000..24d8da479c --- /dev/null +++ b/Content.IntegrationTests/Tests/Humanoid/HideableHumanoidLayersTest.cs @@ -0,0 +1,88 @@ +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(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(SPlayer); + Assert.That(hideableHumanoidLayers.HiddenLayers, Does.Not.ContainKey(HumanoidVisualLayers.Snout)); + }); + } + + [Test] + public async Task DependentHiding() + { + await Server.WaitAssertion(() => + { + var visualBody = SEntMan.System(); + visualBody.ApplyMarkings(SPlayer, new() + { + ["Head"] = new() + { + [HumanoidVisualLayers.SnoutCover] = new List() { new("VulpSnoutNose", 1) }, + }, + }); + }); + + await SpawnTarget("ClothingMaskGas"); + await Pickup(); // equip mask + await UseInHand(); + + await RunTicks(20); + + await Client.WaitAssertion(() => + { + var spriteSystem = CEntMan.System(); + var snoutIndex = spriteSystem.LayerMapGet(CPlayer, "VulpSnout-snout"); + var snoutCoverIndex = spriteSystem.LayerMapGet(CPlayer, "VulpSnoutNose-snout-nose"); + var spriteComp = CEntMan.GetComponent(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(); + var snoutIndex = spriteSystem.LayerMapGet(CPlayer, "VulpSnout-snout"); + var snoutCoverIndex = spriteSystem.LayerMapGet(CPlayer, "VulpSnoutNose-snout-nose"); + var spriteComp = CEntMan.GetComponent(CPlayer); + + Assert.That(spriteComp[snoutIndex].Visible, Is.True); + Assert.That(spriteComp[snoutCoverIndex].Visible, Is.True); + }); + } +} diff --git a/Content.IntegrationTests/Tests/Humanoid/HideablePrototypeValidation.cs b/Content.IntegrationTests/Tests/Humanoid/HideablePrototypeValidation.cs new file mode 100644 index 0000000000..d95992bda2 --- /dev/null +++ b/Content.IntegrationTests/Tests/Humanoid/HideablePrototypeValidation.cs @@ -0,0 +1,80 @@ +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>(); + foreach (var (proto, component) in pair.GetPrototypesWithComponent()) + { + foreach (var layer in component.HideableLayers) + { + requirements[layer] = requirements.GetValueOrDefault(layer) ?? []; + requirements[layer].Add(proto.ID); + } + } + + var provided = new HashSet(); + foreach (var (_, component) in pair.GetPrototypesWithComponent()) + { +#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>(); + foreach (var (proto, component) in pair.GetPrototypesWithComponent()) + { +#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(); + foreach (var (_, component) in pair.GetPrototypesWithComponent()) + { + 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(); + } +} diff --git a/Content.Shared/Body/VisualOrganMarkingsComponent.cs b/Content.Shared/Body/VisualOrganMarkingsComponent.cs index e0ec567cf4..a0af5a6a16 100644 --- a/Content.Shared/Body/VisualOrganMarkingsComponent.cs +++ b/Content.Shared/Body/VisualOrganMarkingsComponent.cs @@ -22,6 +22,18 @@ public sealed partial class VisualOrganMarkingsComponent : Component [DataField, AutoNetworkedField] public Dictionary> Markings = new(); + /// + /// Layers that are eligible for hiding based on e.g. clothing + /// + [DataField, AutoNetworkedField] + public HashSet HideableLayers = new(); + + /// + /// A dictionary of layers to other layers that visually depend on them for hiding, e.g. SnoutCover depends on Snout + /// + [DataField, AutoNetworkedField] + public Dictionary> DependentHidingLayers = new(); + /// /// Client only - the last markings applied by this component /// diff --git a/Content.Shared/Clothing/EntitySystems/HideLayerClothingSystem.cs b/Content.Shared/Clothing/EntitySystems/HideLayerClothingSystem.cs index 1d58e7071c..ee0f3932d6 100644 --- a/Content.Shared/Clothing/EntitySystems/HideLayerClothingSystem.cs +++ b/Content.Shared/Clothing/EntitySystems/HideLayerClothingSystem.cs @@ -51,7 +51,6 @@ public sealed class HideLayerClothingSystem : EntitySystem 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 @@ -64,12 +63,9 @@ public sealed class HideLayerClothingSystem : EntitySystem // 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 @@ -80,8 +76,7 @@ public sealed class HideLayerClothingSystem : EntitySystem { foreach (var layer in slots) { - if (hideable.Contains(layer)) - _hideableHumanoidLayers.SetLayerVisibility(user, layer, !hideLayers, inSlot); + _hideableHumanoidLayers.SetLayerOcclusion(user, layer, hideLayers, inSlot); } } } diff --git a/Content.Shared/Humanoid/HideableHumanoidLayersComponent.cs b/Content.Shared/Humanoid/HideableHumanoidLayersComponent.cs index 8fa0998ab5..75abeb4316 100644 --- a/Content.Shared/Humanoid/HideableHumanoidLayersComponent.cs +++ b/Content.Shared/Humanoid/HideableHumanoidLayersComponent.cs @@ -15,12 +15,6 @@ public sealed partial class HideableHumanoidLayersComponent : Component [DataField, AutoNetworkedField] public Dictionary HiddenLayers = new(); - /// - /// Which layers of this humanoid that should be hidden on equipping a corresponding item.. - /// - [DataField] - public HashSet HideLayersOnEquip = [HumanoidVisualLayers.Hair]; - /// /// Client only - which layers were last hidden /// diff --git a/Content.Shared/Humanoid/SharedHideableHumanoidLayersSystem.cs b/Content.Shared/Humanoid/SharedHideableHumanoidLayersSystem.cs index 6e308cc4bb..0baea47892 100644 --- a/Content.Shared/Humanoid/SharedHideableHumanoidLayersSystem.cs +++ b/Content.Shared/Humanoid/SharedHideableHumanoidLayersSystem.cs @@ -11,12 +11,12 @@ public abstract partial class SharedHideableHumanoidLayersSystem : EntitySystem /// /// Humanoid entity /// Layer to toggle visibility for - /// Whether to hide or show the layer. If more than once piece of clothing is hiding the layer, it may remain 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. /// Equipment slot that has the clothing that is (or was) hiding the layer. - public virtual void SetLayerVisibility( + public virtual void SetLayerOcclusion( Entity ent, HumanoidVisualLayers layer, - bool visible, + bool hidden, SlotFlags slot) { if (!Resolve(ent, ref ent.Comp)) @@ -30,7 +30,7 @@ public abstract partial class SharedHideableHumanoidLayersSystem : EntitySystem #endif var dirty = false; - if (visible) + if (hidden) { var oldSlots = ent.Comp.HiddenLayers.GetValueOrDefault(layer); ent.Comp.HiddenLayers[layer] = slot | oldSlots; @@ -52,7 +52,7 @@ public abstract partial class SharedHideableHumanoidLayersSystem : EntitySystem Dirty(ent); - var evt = new HumanoidLayerVisibilityChangedEvent(layer, visible); + var evt = new HumanoidLayerVisibilityChangedEvent(layer, ent.Comp.HiddenLayers.ContainsKey(layer)); RaiseLocalEvent(ent, ref evt); } } diff --git a/Resources/Prototypes/Body/Species/human.yml b/Resources/Prototypes/Body/Species/human.yml index 7c0ee31f32..fd9dd1fbbe 100644 --- a/Resources/Prototypes/Body/Species/human.yml +++ b/Resources/Prototypes/Body/Species/human.yml @@ -130,6 +130,11 @@ - type: entity parent: [ OrganBaseHeadSexed, OrganBaseHead, OrganHumanExternal ] id: OrganHumanHead + components: + - type: VisualOrganMarkings + hideableLayers: + - enum.HumanoidVisualLayers.Hair + - enum.HumanoidVisualLayers.Snout - type: entity parent: [ OrganBaseArmLeft, OrganHumanExternal ] diff --git a/Resources/Prototypes/Body/Species/moth.yml b/Resources/Prototypes/Body/Species/moth.yml index a5a1874169..a1a10ff8d9 100644 --- a/Resources/Prototypes/Body/Species/moth.yml +++ b/Resources/Prototypes/Body/Species/moth.yml @@ -239,6 +239,10 @@ - type: entity parent: [ OrganBaseHead, OrganMothExternal ] id: OrganMothHead + components: + - type: VisualOrganMarkings + hideableLayers: + - enum.HumanoidVisualLayers.HeadTop - type: entity parent: [ OrganBaseArmLeft, OrganMothExternal ] diff --git a/Resources/Prototypes/Body/Species/reptilian.yml b/Resources/Prototypes/Body/Species/reptilian.yml index 3dd25cf468..8083d8593c 100644 --- a/Resources/Prototypes/Body/Species/reptilian.yml +++ b/Resources/Prototypes/Body/Species/reptilian.yml @@ -205,10 +205,20 @@ - 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 ] diff --git a/Resources/Prototypes/Body/Species/vox.yml b/Resources/Prototypes/Body/Species/vox.yml index 15190165e0..08a1b88dc1 100644 --- a/Resources/Prototypes/Body/Species/vox.yml +++ b/Resources/Prototypes/Body/Species/vox.yml @@ -4,13 +4,15 @@ 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 @@ -19,12 +21,12 @@ 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 @@ -36,11 +38,11 @@ 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 @@ -290,22 +292,19 @@ - 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 ] diff --git a/Resources/Prototypes/Body/Species/vulpkanin.yml b/Resources/Prototypes/Body/Species/vulpkanin.yml index 9209e4c8b7..d2d094e31b 100644 --- a/Resources/Prototypes/Body/Species/vulpkanin.yml +++ b/Resources/Prototypes/Body/Species/vulpkanin.yml @@ -240,6 +240,17 @@ - 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 ] diff --git a/Resources/Prototypes/Body/base_organs.yml b/Resources/Prototypes/Body/base_organs.yml index 0a5607acef..5c16184a78 100644 --- a/Resources/Prototypes/Body/base_organs.yml +++ b/Resources/Prototypes/Body/base_organs.yml @@ -60,6 +60,8 @@ data: state: head - type: VisualOrganMarkings + hideableLayers: + - enum.HumanoidVisualLayers.Hair markingData: layers: - Head diff --git a/Resources/Prototypes/Body/species_appearance.yml b/Resources/Prototypes/Body/species_appearance.yml index ab5f83dd7c..aa584a5e0c 100644 --- a/Resources/Prototypes/Body/species_appearance.yml +++ b/Resources/Prototypes/Body/species_appearance.yml @@ -84,13 +84,6 @@ - type: ContainerContainer - type: Appearance - type: HideableHumanoidLayers - hideLayersOnEquip: - - Snout - - SnoutCover - - HeadTop - - HeadSide - - FacialHair - - Hair - type: UserInterface interfaces: enum.HumanoidMarkingModifierKey.Key: diff --git a/Resources/Prototypes/Entities/Mobs/Customization/Markings/vox_tattoos.yml b/Resources/Prototypes/Entities/Mobs/Customization/Markings/vox_tattoos.yml index c10880d281..c1112ebaea 100644 --- a/Resources/Prototypes/Entities/Mobs/Customization/Markings/vox_tattoos.yml +++ b/Resources/Prototypes/Entities/Mobs/Customization/Markings/vox_tattoos.yml @@ -162,7 +162,7 @@ sprites: - sprite: Mobs/Customization/vox_tattoos.rsi state: eyeshadow_large - + - type: marking id: VoxTattooEyeliner bodyPart: Eyes @@ -173,7 +173,7 @@ - type: marking id: VoxBeakCoverStripe - bodyPart: Snout + bodyPart: SnoutCover coloring: default: type: @@ -186,7 +186,7 @@ - type: marking id: VoxBeakCoverTip - bodyPart: Snout + bodyPart: SnoutCover coloring: default: type: