using Content.Client.DisplacementMap; 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; using Robust.Shared.Prototypes; using Robust.Shared.Utility; namespace Content.Client.Humanoid; public sealed class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem { [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly MarkingManager _markingManager = default!; [Dependency] private readonly IConfigurationManager _configurationManager = default!; [Dependency] private readonly DisplacementMapSystem _displacement = default!; [Dependency] private readonly SpriteSystem _sprite = default!; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnHandleState); Subs.CVar(_configurationManager, CCVars.AccessibilityClientCensorNudity, OnCvarChanged, true); Subs.CVar(_configurationManager, CCVars.AccessibilityServerCensorNudity, OnCvarChanged, true); } private void OnHandleState(EntityUid uid, HumanoidAppearanceComponent component, ref AfterAutoHandleStateEvent args) { UpdateSprite((uid, component, Comp(uid))); } private void OnCvarChanged(bool value) { var humanoidQuery = AllEntityQuery(); while (humanoidQuery.MoveNext(out var uid, out var humanoidComp, out var spriteComp)) { UpdateSprite((uid, humanoidComp, spriteComp)); } } private void UpdateSprite(Entity entity) { UpdateLayers(entity); ApplyMarkingSet(entity); var humanoidAppearance = entity.Comp1; var sprite = entity.Comp2; sprite[_sprite.LayerMapReserve((entity.Owner, sprite), HumanoidVisualLayers.Eyes)].Color = humanoidAppearance.EyeColor; } private static bool IsHidden(HumanoidAppearanceComponent humanoid, HumanoidVisualLayers layer) => humanoid.HiddenLayers.ContainsKey(layer) || humanoid.PermanentlyHidden.Contains(layer); private void UpdateLayers(Entity entity) { var component = entity.Comp1; var sprite = entity.Comp2; var oldLayers = new HashSet(component.BaseLayers.Keys); 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) { oldLayers.Remove(key); if (!component.CustomBaseLayers.ContainsKey(key)) SetLayerData(entity, key, id, sexMorph: true); } // add custom layers foreach (var (key, info) in component.CustomBaseLayers) { oldLayers.Remove(key); SetLayerData(entity, key, info.Id, sexMorph: false, color: info.Color); } // hide old layers // TODO maybe just remove them altogether? foreach (var key in oldLayers) { if (_sprite.LayerMapTryGet((entity.Owner, sprite), key, out var index, false)) sprite[index].Visible = false; } } private void SetLayerData( Entity entity, HumanoidVisualLayers key, string? protoId, bool sexMorph = false, Color? color = null) { var component = entity.Comp1; var sprite = entity.Comp2; var layerIndex = _sprite.LayerMapReserve((entity.Owner, sprite), key); var layer = sprite[layerIndex]; layer.Visible = !IsHidden(component, key); if (color != null) layer.Color = color.Value; if (protoId == null) return; if (sexMorph) protoId = HumanoidVisualLayersExtension.GetSexMorph(key, component.Sex, protoId); var proto = _prototypeManager.Index(protoId); component.BaseLayers[key] = proto; if (proto.MatchSkin) layer.Color = component.SkinColor.WithAlpha(proto.LayerAlpha); if (proto.BaseSprite != null) _sprite.LayerSetSprite((entity.Owner, sprite), layerIndex, proto.BaseSprite); } /// /// Loads a profile directly into a humanoid. /// /// The humanoid entity's UID /// The profile to load. /// The humanoid entity's humanoid component. /// /// 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) { if (profile == null) return; if (!Resolve(uid, ref humanoid)) { return; } var customBaseLayers = new Dictionary(); var speciesPrototype = _prototypeManager.Index(profile.Species); var markings = new MarkingSet(speciesPrototype.MarkingPoints, _markingManager, _prototypeManager); // Add markings that doesn't need coloring. We store them until we add all other markings that doesn't need it. var markingFColored = new Dictionary(); foreach (var marking in profile.Appearance.Markings) { if (_markingManager.TryGetMarking(marking, out var prototype)) { if (!prototype.ForcedColoring) { markings.AddBack(prototype.MarkingCategory, marking); } else { markingFColored.Add(marking, prototype); } } } // legacy: remove in the future? //markings.RemoveCategory(MarkingCategories.Hair); //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) ? 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) ? profile.Appearance.SkinColor.WithAlpha(facialHairAlpha) : profile.Appearance.FacialHairColor; var facialHair = new Marking(profile.Appearance.FacialHairStyleId, new[] { facialHairColor }); if (_markingManager.CanBeApplied(profile.Species, profile.Sex, hair, _prototypeManager)) { markings.AddBack(MarkingCategories.Hair, hair); } if (_markingManager.CanBeApplied(profile.Species, profile.Sex, facialHair, _prototypeManager)) { markings.AddBack(MarkingCategories.FacialHair, facialHair); } // Finally adding marking with forced colors foreach (var (marking, prototype) in markingFColored) { var markingColors = MarkingColoring.GetMarkingLayerColors( prototype, profile.Appearance.SkinColor, profile.Appearance.EyeColor, markings ); markings.AddBack(prototype.MarkingCategory, new Marking(marking.MarkingId, markingColors)); } markings.EnsureSpecies(profile.Species, profile.Appearance.SkinColor, _markingManager, _prototypeManager); markings.EnsureSexes(profile.Sex, _markingManager); markings.EnsureDefault( profile.Appearance.SkinColor, profile.Appearance.EyeColor, _markingManager); DebugTools.Assert(IsClientSide(uid)); humanoid.MarkingSet = markings; humanoid.PermanentlyHidden = new HashSet(); humanoid.HiddenLayers = new Dictionary(); humanoid.CustomBaseLayers = customBaseLayers; humanoid.Sex = profile.Sex; humanoid.Gender = profile.Gender; humanoid.Age = profile.Age; humanoid.Species = profile.Species; humanoid.SkinColor = profile.Appearance.SkinColor; humanoid.EyeColor = profile.Appearance.EyeColor; UpdateSprite((uid, humanoid, Comp(uid))); } private void ApplyMarkingSet(Entity entity) { var humanoid = entity.Comp1; var sprite = entity.Comp2; // I am lazy and I CBF resolving the previous mess, so I'm just going to nuke the markings. // Really, markings should probably be a separate component altogether. ClearAllMarkings(entity); var censorNudity = _configurationManager.GetCVar(CCVars.AccessibilityClientCensorNudity) || _configurationManager.GetCVar(CCVars.AccessibilityServerCensorNudity); // The reason we're splitting this up is in case the character already has undergarment equipped in that slot. var applyUndergarmentTop = censorNudity; var applyUndergarmentBottom = censorNudity; foreach (var markingList in humanoid.MarkingSet.Markings.Values) { foreach (var marking in markingList) { if (_markingManager.TryGetMarking(marking, out var markingPrototype)) { ApplyMarking(markingPrototype, marking.MarkingColors, marking.Visible, entity); if (markingPrototype.BodyPart == HumanoidVisualLayers.UndergarmentTop) applyUndergarmentTop = false; else if (markingPrototype.BodyPart == HumanoidVisualLayers.UndergarmentBottom) applyUndergarmentBottom = false; } } } humanoid.ClientOldMarkings = new MarkingSet(humanoid.MarkingSet); AddUndergarments(entity, applyUndergarmentTop, applyUndergarmentBottom); } private void ClearAllMarkings(Entity entity) { var humanoid = entity.Comp1; var sprite = entity.Comp2; foreach (var markingList in humanoid.ClientOldMarkings.Markings.Values) { foreach (var marking in markingList) { RemoveMarking(marking, (entity, sprite)); } } humanoid.ClientOldMarkings.Clear(); foreach (var markingList in humanoid.MarkingSet.Markings.Values) { foreach (var marking in markingList) { RemoveMarking(marking, (entity, sprite)); } } } private void RemoveMarking(Marking marking, Entity spriteEnt) { if (!_markingManager.TryGetMarking(marking, out var prototype)) return; foreach (var sprite in prototype.Sprites) { if (sprite is not SpriteSpecifier.Rsi rsi) continue; var layerId = $"{marking.MarkingId}-{rsi.RsiState}"; if (!_sprite.LayerMapTryGet(spriteEnt.AsNullable(), layerId, out var index, false)) continue; _sprite.LayerMapRemove(spriteEnt.AsNullable(), layerId); _sprite.RemoveLayer(spriteEnt.AsNullable(), index); // If this marking is one that can be displaced, we need to remove the displacement as well; otherwise // altering a marking at runtime can lead to the renderer falling over. // The Vulps must be shaved. // (https://github.com/space-wizards/space-station-14/issues/40135). if (prototype.CanBeDisplaced) _displacement.EnsureDisplacementIsNotOnSprite(spriteEnt, layerId); } } private void AddUndergarments(Entity entity, bool undergarmentTop, bool undergarmentBottom) { var humanoid = entity.Comp1; if (undergarmentTop && humanoid.UndergarmentTop != null) { var marking = new Marking(humanoid.UndergarmentTop, new List { new Color() }); if (_markingManager.TryGetMarking(marking, out var prototype)) { // Markings are added to ClientOldMarkings because otherwise it causes issues when toggling the feature on/off. humanoid.ClientOldMarkings.Markings.Add(MarkingCategories.UndergarmentTop, new List { marking }); ApplyMarking(prototype, null, true, entity); } } if (undergarmentBottom && humanoid.UndergarmentBottom != null) { var marking = new Marking(humanoid.UndergarmentBottom, new List { new Color() }); if (_markingManager.TryGetMarking(marking, out var prototype)) { humanoid.ClientOldMarkings.Markings.Add(MarkingCategories.UndergarmentBottom, new List { marking }); ApplyMarking(prototype, null, true, entity); } } } private void ApplyMarking(MarkingPrototype markingPrototype, IReadOnlyList? colors, bool visible, Entity entity) { var humanoid = entity.Comp1; var sprite = entity.Comp2; if (!_sprite.LayerMapTryGet((entity.Owner, sprite), markingPrototype.BodyPart, out var targetLayer, false)) return; visible &= !IsHidden(humanoid, markingPrototype.BodyPart); visible &= humanoid.BaseLayers.TryGetValue(markingPrototype.BodyPart, out var setting) && setting.AllowsMarkings; for (var j = 0; j < markingPrototype.Sprites.Count; j++) { var markingSprite = markingPrototype.Sprites[j]; if (markingSprite is not SpriteSpecifier.Rsi rsi) return; var layerId = $"{markingPrototype.ID}-{rsi.RsiState}"; if (!_sprite.LayerMapTryGet((entity.Owner, sprite), layerId, out _, false)) { var layer = _sprite.AddLayer((entity.Owner, sprite), markingSprite, targetLayer + j + 1); _sprite.LayerMapSet((entity.Owner, sprite), layerId, layer); _sprite.LayerSetSprite((entity.Owner, sprite), layerId, rsi); } _sprite.LayerSetVisible((entity.Owner, sprite), layerId, visible); if (!visible || setting == null) // this is kinda implied continue; // Okay so if the marking prototype is modified but we load old marking data this may no longer be valid // and we need to check the index is correct. // So if that happens just default to white? if (colors != null && j < colors.Count) _sprite.LayerSetColor((entity.Owner, sprite), layerId, colors[j]); else _sprite.LayerSetColor((entity.Owner, sprite), layerId, Color.White); if (humanoid.MarkingsDisplacement.TryGetValue(markingPrototype.BodyPart, out var displacementData) && markingPrototype.CanBeDisplaced) _displacement.TryAddDisplacement(displacementData, (entity.Owner, sprite), targetLayer + j + 1, layerId, out _); } } 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; base.SetSkinColor(uid, skinColor, false, verify, humanoid); if (!TryComp(uid, out SpriteComponent? sprite)) return; foreach (var (layer, spriteInfo) in humanoid.BaseLayers) { if (!spriteInfo.MatchSkin) continue; var index = _sprite.LayerMapReserve((uid, sprite), layer); sprite[index].Color = skinColor.WithAlpha(spriteInfo.LayerAlpha); } } public override void SetLayerVisibility( Entity ent, HumanoidVisualLayers layer, bool visible, SlotFlags? slot, ref bool dirty) { base.SetLayerVisibility(ent, layer, visible, slot, ref dirty); var sprite = Comp(ent); if (!_sprite.LayerMapTryGet((ent.Owner, sprite), layer, out var index, false)) { if (!visible) return; index = _sprite.LayerMapReserve((ent.Owner, sprite), layer); } var spriteLayer = sprite[index]; if (spriteLayer.Visible == visible) return; 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 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, (ent, ent.Comp, sprite)); } } } }