mirror of
https://github.com/space-wizards/space-station-14.git
synced 2026-02-15 03:31:30 +01:00
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>
This commit is contained in:
committed by
GitHub
parent
acb685f3f9
commit
0ec9975e4f
@@ -232,6 +232,9 @@ public sealed class VisualBodySystem : SharedVisualBodySystem
|
||||
|
||||
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)
|
||||
@@ -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)
|
||||
|
||||
@@ -26,13 +26,13 @@ public sealed class HideableHumanoidLayersSystem : SharedHideableHumanoidLayersS
|
||||
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));
|
||||
|
||||
@@ -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<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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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<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();
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,18 @@ public sealed partial class VisualOrganMarkingsComponent : Component
|
||||
[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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,12 +15,6 @@ public sealed partial class HideableHumanoidLayersComponent : Component
|
||||
[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>
|
||||
|
||||
@@ -11,12 +11,12 @@ public abstract partial class SharedHideableHumanoidLayersSystem : EntitySystem
|
||||
/// </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))
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 ]
|
||||
|
||||
@@ -239,6 +239,10 @@
|
||||
- type: entity
|
||||
parent: [ OrganBaseHead, OrganMothExternal ]
|
||||
id: OrganMothHead
|
||||
components:
|
||||
- type: VisualOrganMarkings
|
||||
hideableLayers:
|
||||
- enum.HumanoidVisualLayers.HeadTop
|
||||
|
||||
- type: entity
|
||||
parent: [ OrganBaseArmLeft, OrganMothExternal ]
|
||||
|
||||
@@ -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 ]
|
||||
|
||||
@@ -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 ]
|
||||
|
||||
@@ -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 ]
|
||||
|
||||
@@ -60,6 +60,8 @@
|
||||
data:
|
||||
state: head
|
||||
- type: VisualOrganMarkings
|
||||
hideableLayers:
|
||||
- enum.HumanoidVisualLayers.Hair
|
||||
markingData:
|
||||
layers:
|
||||
- Head
|
||||
|
||||
@@ -84,13 +84,6 @@
|
||||
- type: ContainerContainer
|
||||
- type: Appearance
|
||||
- type: HideableHumanoidLayers
|
||||
hideLayersOnEquip:
|
||||
- Snout
|
||||
- SnoutCover
|
||||
- HeadTop
|
||||
- HeadSide
|
||||
- FacialHair
|
||||
- Hair
|
||||
- type: UserInterface
|
||||
interfaces:
|
||||
enum.HumanoidMarkingModifierKey.Key:
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user