diff --git a/Content.Client/Clothing/ClientClothingSystem.cs b/Content.Client/Clothing/ClientClothingSystem.cs index 261eae2abb..91ed73c89a 100644 --- a/Content.Client/Clothing/ClientClothingSystem.cs +++ b/Content.Client/Clothing/ClientClothingSystem.cs @@ -5,12 +5,14 @@ using Content.Shared.Clothing; using Content.Shared.Clothing.Components; using Content.Shared.Clothing.EntitySystems; using Content.Shared.Humanoid; +using Content.Shared.Humanoid.Prototypes; using Content.Shared.Inventory; using Content.Shared.Inventory.Events; using Content.Shared.Item; using Robust.Client.GameObjects; using Robust.Client.Graphics; using Robust.Client.ResourceManagement; +using Robust.Shared.Prototypes; using Robust.Shared.Serialization.TypeSerializers.Implementations; using Robust.Shared.Utility; using static Robust.Client.GameObjects.SpriteComponent; @@ -28,30 +30,31 @@ public sealed class ClientClothingSystem : ClothingSystem /// private static readonly Dictionary TemporarySlotMap = new() { - {"head", "HELMET"}, - {"eyes", "EYES"}, - {"ears", "EARS"}, - {"mask", "MASK"}, - {"outerClothing", "OUTERCLOTHING"}, - {Jumpsuit, "INNERCLOTHING"}, - {"neck", "NECK"}, - {"back", "BACKPACK"}, - {"belt", "BELT"}, - {"gloves", "HAND"}, - {"shoes", "FEET"}, - {"id", "IDCARD"}, - {"pocket1", "POCKET1"}, - {"pocket2", "POCKET2"}, - {"suitstorage", "SUITSTORAGE"}, + { "head", "HELMET" }, + { "eyes", "EYES" }, + { "ears", "EARS" }, + { "mask", "MASK" }, + { "outerClothing", "OUTERCLOTHING" }, + { Jumpsuit, "INNERCLOTHING" }, + { "neck", "NECK" }, + { "back", "BACKPACK" }, + { "belt", "BELT" }, + { "gloves", "HAND" }, + { "shoes", "FEET" }, + { "id", "IDCARD" }, + { "pocket1", "POCKET1" }, + { "pocket2", "POCKET2" }, + { "suitstorage", "SUITSTORAGE" }, //WHITE EDIT - {"socks", "SOCKS"}, - {"underweart", "UNDERWEART"}, - {"underwearb", "UNDERWEARB"}, + { "socks", "SOCKS" }, + { "underweart", "UNDERWEART" }, + { "underwearb", "UNDERWEARB" }, // WHITE EDIT }; [Dependency] private readonly IResourceCache _cache = default!; [Dependency] private readonly InventorySystem _inventorySystem = default!; + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; public override void Initialize() { @@ -99,8 +102,13 @@ public sealed class ClientClothingSystem : ClothingSystem // if that returned nothing, attempt to find generic data if (layers == null && !item.ClothingVisuals.TryGetValue(args.Slot, out layers)) { + if (!TryComp(args.Equipee, out HumanoidAppearanceComponent? humanoid)) + { + return; + } + // No generic data either. Attempt to generate defaults from the item's RSI & item-prefixes - if (!TryGetDefaultVisuals(uid, item, args.Slot, inventory.SpeciesId, out layers)) + if (!TryGetDefaultVisuals(uid, item, args.Slot, inventory.SpeciesId, humanoid, out layers)) return; } @@ -126,7 +134,12 @@ public sealed class ClientClothingSystem : ClothingSystem /// /// Useful for lazily adding clothing sprites without modifying yaml. And for backwards compatibility. /// - private bool TryGetDefaultVisuals(EntityUid uid, ClothingComponent clothing, string slot, string? speciesId, + private bool TryGetDefaultVisuals( + EntityUid uid, + ClothingComponent clothing, + string slot, + string? speciesId, + HumanoidAppearanceComponent humanoid, [NotNullWhen(true)] out List? layers) { layers = null; @@ -138,14 +151,12 @@ public sealed class ClientClothingSystem : ClothingSystem else if (TryComp(uid, out SpriteComponent? sprite)) rsi = sprite.BaseRSI; - if (rsi == null || rsi.Path == null) + if (rsi?.Path == null) return false; var correctedSlot = slot; TemporarySlotMap.TryGetValue(correctedSlot, out correctedSlot); - - var state = $"equipped-{correctedSlot}"; if (clothing.EquippedPrefix != null) @@ -154,6 +165,13 @@ public sealed class ClientClothingSystem : ClothingSystem if (clothing.EquippedState != null) state = $"{clothing.EquippedState}"; + // body type specific + var bodyTypeProto = _prototypeManager.Index(humanoid.BodyType); + if (rsi.TryGetState($"{state}-{bodyTypeProto.Name}", out _)) + { + state = $"{state}-{bodyTypeProto.Name}"; + } + // species specific if (speciesId != null && rsi.TryGetState($"{state}-{speciesId}", out _)) { @@ -164,10 +182,13 @@ public sealed class ClientClothingSystem : ClothingSystem return false; } - var layer = new PrototypeLayerData(); - layer.RsiPath = rsi.Path.ToString(); - layer.State = state; - layers = new() { layer }; + var layer = new PrototypeLayerData + { + RsiPath = rsi.Path.ToString(), + State = state + }; + + layers = new List { layer }; return true; } @@ -189,7 +210,7 @@ public sealed class ClientClothingSystem : ClothingSystem && TryComp(uid, out SpriteComponent? sprite) && sprite.LayerMapTryGet(HumanoidVisualLayers.StencilMask, out var maskLayer)) { - sprite.LayerSetVisible(maskLayer, false); + sprite.LayerSetVisible(maskLayer, false); } if (!TryComp(uid, out InventorySlotsComponent? inventorySlots)) @@ -204,6 +225,7 @@ public sealed class ClientClothingSystem : ClothingSystem { component.RemoveLayer(layer); } + revealedLayers.Clear(); } @@ -226,12 +248,17 @@ public sealed class ClientClothingSystem : ClothingSystem RenderEquipment(args.Equipee, uid, args.Slot, clothingComponent: component); } - private void RenderEquipment(EntityUid equipee, EntityUid equipment, string slot, - InventoryComponent? inventory = null, SpriteComponent? sprite = null, ClothingComponent? clothingComponent = null, + private void RenderEquipment( + EntityUid equipee, + EntityUid equipment, + string slot, + InventoryComponent? inventory = null, + SpriteComponent? sprite = null, + ClothingComponent? clothingComponent = null, InventorySlotsComponent? inventorySlots = null) { if (!Resolve(equipee, ref inventory, ref sprite, ref inventorySlots) || - !Resolve(equipment, ref clothingComponent, false)) + !Resolve(equipment, ref clothingComponent, false)) { return; } @@ -250,11 +277,12 @@ public sealed class ClientClothingSystem : ClothingSystem { sprite.RemoveLayer(key); } + revealedLayers.Clear(); } else { - revealedLayers = new(); + revealedLayers = new HashSet(); inventorySlots.VisualLayerKeys[slot] = revealedLayers; } @@ -276,7 +304,9 @@ public sealed class ClientClothingSystem : ClothingSystem { if (!revealedLayers.Add(key)) { - Logger.Warning($"Duplicate key for clothing visuals: {key}. Are multiple components attempting to modify the same layer? Equipment: {ToPrettyString(equipment)}"); + Logger.Warning( + $"Duplicate key for clothing visuals: {key}. Are multiple components attempting to modify the same layer? Equipment: {ToPrettyString(equipment)}"); + continue; } @@ -315,12 +345,10 @@ public sealed class ClientClothingSystem : ClothingSystem RaiseLocalEvent(equipment, new EquipmentVisualsUpdatedEvent(equipee, slot, revealedLayers), true); } - /// /// Sets a sprite's gendered mask based on gender (obviously). /// /// Sprite to modify - /// Humanoid, to get gender from /// Clothing component, to get mask sprite type private void SetGenderedMask(EntityUid uid, SpriteComponent sprite, ClothingComponent clothing) { @@ -330,7 +358,12 @@ public sealed class ClientClothingSystem : ClothingSystem ClothingMask mask; string prefix; - switch (CompOrNull(uid)?.Sex) + if (!TryComp(uid, out HumanoidAppearanceComponent? humanoid)) + { + return; + } + + switch (humanoid.Sex) { case Sex.Male: mask = clothing.MaleMask; @@ -338,7 +371,8 @@ public sealed class ClientClothingSystem : ClothingSystem break; case Sex.Female: mask = clothing.FemaleMask; - prefix = "female_"; + var bodyTypeProto = _prototypeManager.Index(humanoid.BodyType!); + prefix = bodyTypeProto.Name != "body-normal" ? $"female_{bodyTypeProto.Name}_" : "female_"; break; default: mask = clothing.UnisexMask; @@ -348,10 +382,11 @@ public sealed class ClientClothingSystem : ClothingSystem sprite.LayerSetState(layer, mask switch { - ClothingMask.NoMask => $"{prefix}none", + ClothingMask.NoMask => $"{prefix}none", ClothingMask.UniformTop => $"{prefix}top", - _ => $"{prefix}full", + _ => $"{prefix}full", }); + sprite.LayerSetVisible(layer, true); } } diff --git a/Content.Client/Humanoid/HumanoidAppearanceSystem.cs b/Content.Client/Humanoid/HumanoidAppearanceSystem.cs index 5bae35da5b..ddb1853ef2 100644 --- a/Content.Client/Humanoid/HumanoidAppearanceSystem.cs +++ b/Content.Client/Humanoid/HumanoidAppearanceSystem.cs @@ -42,9 +42,8 @@ public sealed class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem component.BaseLayers.Clear(); // add default species layers - var speciesProto = _prototypeManager.Index(component.Species); - var baseSprites = _prototypeManager.Index(speciesProto.SpriteSet); - foreach (var (key, id) in baseSprites.Sprites) + var bodyTypeProto = _prototypeManager.Index(component.BodyType); + foreach (var (key, id) in bodyTypeProto.Sprites) { oldLayers.Remove(key); if (!component.CustomBaseLayers.ContainsKey(key)) @@ -108,13 +107,17 @@ public sealed class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem /// This should not be used if the entity is owned by the server. The server will otherwise /// override this with the appearance data it sends over. /// - public override void LoadProfile(EntityUid uid, HumanoidCharacterProfile profile, HumanoidAppearanceComponent? humanoid = null) + public override void LoadProfile( + EntityUid uid, + HumanoidCharacterProfile profile, + HumanoidAppearanceComponent? humanoid = null) { if (!Resolve(uid, ref humanoid)) { return; } + humanoid.BodyType = profile.BodyType; var customBaseLayers = new Dictionary(); var speciesPrototype = _prototypeManager.Index(profile.Species); @@ -142,15 +145,19 @@ public sealed class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem //markings.RemoveCategory(MarkingCategories.FacialHair); // We need to ensure hair before applying it or coloring can try depend on markings that can be invalid - var hairColor = _markingManager.MustMatchSkin(profile.Species, HumanoidVisualLayers.Hair, out var hairAlpha, _prototypeManager) + var hairColor = _markingManager.MustMatchSkin(profile.BodyType, HumanoidVisualLayers.Hair, out var hairAlpha, + _prototypeManager) ? profile.Appearance.SkinColor.WithAlpha(hairAlpha) : profile.Appearance.HairColor; + var hair = new Marking(profile.Appearance.HairStyleId, new[] { hairColor }); - var facialHairColor = _markingManager.MustMatchSkin(profile.Species, HumanoidVisualLayers.FacialHair, out var facialHairAlpha, _prototypeManager) + var facialHairColor = _markingManager.MustMatchSkin(profile.BodyType, HumanoidVisualLayers.FacialHair, + out var facialHairAlpha, _prototypeManager) ? profile.Appearance.SkinColor.WithAlpha(facialHairAlpha) : profile.Appearance.FacialHairColor; + var facialHair = new Marking(profile.Appearance.FacialHairStyleId, new[] { facialHairColor }); @@ -158,6 +165,7 @@ public sealed class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem { markings.AddBack(MarkingCategories.Hair, hair); } + if (_markingManager.CanBeApplied(profile.Species, profile.Sex, facialHair, _prototypeManager)) { markings.AddBack(MarkingCategories.FacialHair, facialHair); @@ -172,10 +180,13 @@ public sealed class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem profile.Appearance.EyeColor, markings ); + markings.AddBack(prototype.MarkingCategory, new Marking(marking.MarkingId, markingColors)); } - markings.EnsureSpecies(profile.Species, profile.Appearance.SkinColor, _markingManager, _prototypeManager); + markings.EnsureSpecies(profile.Species, profile.BodyType, profile.Appearance.SkinColor, _markingManager, + _prototypeManager); + markings.EnsureSexes(profile.Sex, _markingManager); markings.EnsureDefault( profile.Appearance.SkinColor, @@ -190,6 +201,7 @@ public sealed class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem humanoid.CustomBaseLayers = customBaseLayers; humanoid.Sex = profile.Sex; humanoid.Gender = profile.Gender; + humanoid.BodyType = profile.BodyType; humanoid.Age = profile.Age; humanoid.Species = profile.Species; humanoid.SkinColor = profile.Appearance.SkinColor; @@ -261,7 +273,9 @@ public sealed class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem spriteComp.RemoveLayer(index); } } - private void ApplyMarking(MarkingPrototype markingPrototype, + + private void ApplyMarking( + MarkingPrototype markingPrototype, IReadOnlyList? colors, bool visible, HumanoidAppearanceComponent humanoid, @@ -274,7 +288,7 @@ public sealed class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem visible &= !IsHidden(humanoid, markingPrototype.BodyPart); visible &= humanoid.BaseLayers.TryGetValue(markingPrototype.BodyPart, out var setting) - && setting.AllowsMarkings; + && setting.AllowsMarkings; for (var j = 0; j < markingPrototype.Sprites.Count; j++) { @@ -315,7 +329,12 @@ public sealed class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem } } - public override void SetSkinColor(EntityUid uid, Color skinColor, bool sync = true, bool verify = true, HumanoidAppearanceComponent? humanoid = null) + public override void SetSkinColor( + EntityUid uid, + Color skinColor, + bool sync = true, + bool verify = true, + HumanoidAppearanceComponent? humanoid = null) { if (!Resolve(uid, ref humanoid) || humanoid.SkinColor == skinColor) return; @@ -366,7 +385,8 @@ public sealed class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem { foreach (var marking in markingList) { - if (_markingManager.TryGetMarking(marking, out var markingPrototype) && markingPrototype.BodyPart == layer) + if (_markingManager.TryGetMarking(marking, out var markingPrototype) && + markingPrototype.BodyPart == layer) ApplyMarking(markingPrototype, marking.MarkingColors, marking.Visible, humanoid, sprite); } } diff --git a/Content.Client/Humanoid/HumanoidMarkingModifierBoundUserInterface.cs b/Content.Client/Humanoid/HumanoidMarkingModifierBoundUserInterface.cs index a8872604a4..cbb9713f7e 100644 --- a/Content.Client/Humanoid/HumanoidMarkingModifierBoundUserInterface.cs +++ b/Content.Client/Humanoid/HumanoidMarkingModifierBoundUserInterface.cs @@ -40,7 +40,7 @@ public sealed class HumanoidMarkingModifierBoundUserInterface : BoundUserInterfa return; } - _window.SetState(cast.MarkingSet, cast.Species, cast.Sex, cast.SkinColor, cast.CustomBaseLayers); + _window.SetState(cast.MarkingSet, cast.Species, cast.Sex, cast.BodyType, cast.SkinColor, cast.CustomBaseLayers); } private void SendMarkingSet(MarkingSet set) diff --git a/Content.Client/Humanoid/HumanoidMarkingModifierWindow.xaml.cs b/Content.Client/Humanoid/HumanoidMarkingModifierWindow.xaml.cs index 4cde587c58..1a19df57b9 100644 --- a/Content.Client/Humanoid/HumanoidMarkingModifierWindow.xaml.cs +++ b/Content.Client/Humanoid/HumanoidMarkingModifierWindow.xaml.cs @@ -59,10 +59,12 @@ public sealed partial class HumanoidMarkingModifierWindow : DefaultWindow string? state = _protoMan.HasIndex(modifier.Text) ? modifier.Text : null; OnLayerInfoModified?.Invoke(layer, new CustomBaseLayerInfo(state, modifier.Color)); } + public void SetState( MarkingSet markings, string species, Sex sex, + string bodyType, Color skinColor, Dictionary info ) @@ -84,7 +86,7 @@ public sealed partial class HumanoidMarkingModifierWindow : DefaultWindow eyesColor = eyes.Color.Value; } - MarkingPickerWidget.SetData(markings, species, sex, skinColor, eyesColor); + MarkingPickerWidget.SetData(markings, species, sex, bodyType, skinColor, eyesColor); } private sealed class HumanoidBaseLayerModifier : BoxContainer @@ -95,7 +97,9 @@ public sealed partial class HumanoidMarkingModifierWindow : DefaultWindow private BoxContainer _infoBox; public bool Enabled => _enable.Pressed; + public string Text => _lineEdit.Text; + public Color Color => _colorSliders.Color; public Action? OnStateChanged; @@ -109,6 +113,7 @@ public sealed partial class HumanoidMarkingModifierWindow : DefaultWindow MinWidth = 250, HorizontalExpand = true }; + AddChild(labelBox); labelBox.AddChild(new Label @@ -116,6 +121,7 @@ public sealed partial class HumanoidMarkingModifierWindow : DefaultWindow HorizontalExpand = true, Text = layer.ToString() }); + _enable = new CheckBox { Text = "Enable", @@ -128,6 +134,7 @@ public sealed partial class HumanoidMarkingModifierWindow : DefaultWindow Orientation = LayoutOrientation.Vertical, Visible = false }; + _enable.OnToggled += args => { _infoBox.Visible = args.Pressed; @@ -135,7 +142,7 @@ public sealed partial class HumanoidMarkingModifierWindow : DefaultWindow }; var lineEditBox = new BoxContainer(); - lineEditBox.AddChild(new Label { Text = "Prototype id: "}); + lineEditBox.AddChild(new Label { Text = "Prototype id: " }); // TODO: This line edit should really be an options / dropdown selector, not text. _lineEdit = new() { MinWidth = 200 }; diff --git a/Content.Client/Humanoid/MarkingPicker.xaml.cs b/Content.Client/Humanoid/MarkingPicker.xaml.cs index f5b85ca89c..600e4792c1 100644 --- a/Content.Client/Humanoid/MarkingPicker.xaml.cs +++ b/Content.Client/Humanoid/MarkingPicker.xaml.cs @@ -40,6 +40,7 @@ public sealed partial class MarkingPicker : Control private List _markingCategories = Enum.GetValues().ToList(); private string _currentSpecies = SharedHumanoidAppearanceSystem.DefaultSpecies; + private string _currentBodyType = SharedHumanoidAppearanceSystem.DefaultBodyType; private Sex _currentSex = Sex.Unsexed; public Color CurrentSkinColor = Color.White; public Color CurrentEyeColor = Color.Black; @@ -83,7 +84,7 @@ public sealed partial class MarkingPicker : Control } } - public void SetData(List newMarkings, string species, Sex sex, Color skinColor, Color eyeColor) + public void SetData(List newMarkings, string species, Sex sex, string bodyType, Color skinColor, Color eyeColor) { var pointsProto = _prototypeManager .Index(species).MarkingPoints; @@ -91,7 +92,7 @@ public sealed partial class MarkingPicker : Control if (!IgnoreSpecies) { - _currentMarkings.EnsureSpecies(species, skinColor, _markingManager); // should be validated server-side but it can't hurt + _currentMarkings.EnsureSpecies(species, bodyType, skinColor, _markingManager); // should be validated server-side but it can't hurt } _currentSpecies = species; @@ -103,13 +104,13 @@ public sealed partial class MarkingPicker : Control PopulateUsed(); } - public void SetData(MarkingSet set, string species, Sex sex, Color skinColor, Color eyeColor) + public void SetData(MarkingSet set, string species, Sex sex, string bodyType, Color skinColor, Color eyeColor) { _currentMarkings = set; if (!IgnoreSpecies) { - _currentMarkings.EnsureSpecies(species, skinColor, _markingManager); // should be validated server-side but it can't hurt + _currentMarkings.EnsureSpecies(species, bodyType, skinColor, _markingManager); // should be validated server-side but it can't hurt } _currentSpecies = species; @@ -234,7 +235,7 @@ public sealed partial class MarkingPicker : Control if (!IgnoreSpecies) { - _currentMarkings.EnsureSpecies(_currentSpecies, null, _markingManager); + _currentMarkings.EnsureSpecies(_currentSpecies, _currentBodyType, null, _markingManager); } // walk backwards through the list for visual purposes @@ -338,7 +339,7 @@ public sealed partial class MarkingPicker : Control var speciesPrototype = _prototypeManager.Index(species); _currentMarkings = new(markingList, speciesPrototype.MarkingPoints, _markingManager, _prototypeManager); - _currentMarkings.EnsureSpecies(species, null, _markingManager); + _currentMarkings.EnsureSpecies(species, _currentBodyType, null, _markingManager); _currentMarkings.EnsureSexes(_currentSex, _markingManager); Populate(CMarkingSearch.Text); @@ -353,7 +354,7 @@ public sealed partial class MarkingPicker : Control var speciesPrototype = _prototypeManager.Index(_currentSpecies); _currentMarkings = new(markingList, speciesPrototype.MarkingPoints, _markingManager, _prototypeManager); - _currentMarkings.EnsureSpecies(_currentSpecies, null, _markingManager); + _currentMarkings.EnsureSpecies(_currentSpecies, _currentBodyType, null, _markingManager); _currentMarkings.EnsureSexes(_currentSex, _markingManager); Populate(CMarkingSearch.Text); diff --git a/Content.Client/Preferences/UI/HumanoidProfileEditor.xaml b/Content.Client/Preferences/UI/HumanoidProfileEditor.xaml index fcdb6322f1..7278e3e1a5 100644 --- a/Content.Client/Preferences/UI/HumanoidProfileEditor.xaml +++ b/Content.Client/Preferences/UI/HumanoidProfileEditor.xaml @@ -75,6 +75,12 @@ + + +