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); // Floof 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) { // hi! mq here. // if youre dealing with upstream merge conflicts, and you see a bunch of stuff up here, chances are its been moved in between some other shit. // i love the documentation on this file but i can only look at layer maps for so long before i feel myself becoming ben affleck. // sorry for any confusion. // FLOOF ADD START // make a handy dict of filename -> colors // cus we might need to access it by filename to link // one sprite's colors to another var colorDict = new Dictionary(); for (var i = 0; i < markingPrototype.Sprites.Count; i++) { var spriteName = markingPrototype.Sprites[i] switch { SpriteSpecifier.Rsi rsi => rsi.RsiState, SpriteSpecifier.Texture texture => texture.TexturePath.Filename, _ => null }; if (spriteName != null) { if (colors != null && i < colors.Count) colorDict.Add(spriteName, colors[i]); else colorDict.Add(spriteName, Color.White); } } // now, rearrange them, copying any parented colors to children set to // inherit them if (markingPrototype.ColorLinks != null) { foreach (var (child, parent) in markingPrototype.ColorLinks) { if (colorDict.TryGetValue(parent, out var color)) { colorDict[child] = color; } } } // and, since we can't rely on the iterator knowing where the heck to put // each sprite when we have one marking setting multiple layers, // lets just kinda sorta do that ourselves var layerDict = new Dictionary(); // FLOOF ADD END for (var j = 0; j < markingPrototype.Sprites.Count; j++) { var markingSprite = markingPrototype.Sprites[j]; if (markingSprite is not SpriteSpecifier.Rsi rsi) { continue; } // FLOOF CHANGE START var layerSlot = markingPrototype.BodyPart; // first, try to see if there are any custom layers for this marking if (markingPrototype.Layering != null) { var name = rsi.RsiState; if (markingPrototype.Layering.TryGetValue(name, out var layerName)) { layerSlot = Enum.Parse(layerName); } } // update the layerDict // if it doesnt have this, add it at 0, otherwise increment it if (layerDict.TryGetValue(layerSlot.ToString(), out var layerIndex)) { layerDict[layerSlot.ToString()] = layerIndex + 1; } else { layerDict.Add(layerSlot.ToString(), 0); } // THIS IS THE UPSTREAM STUFF!!! RIGHT HERE!!! var humanoid = entity.Comp1; var sprite = entity.Comp2; if (!_sprite.LayerMapTryGet((entity.Owner, sprite), layerSlot, out var targetLayer, false)) { continue; } visible &= !IsHidden(humanoid, markingPrototype.BodyPart); visible &= humanoid.BaseLayers.TryGetValue(markingPrototype.BodyPart, out var setting) && setting.AllowsMarkings; // FLOOF CHANGE END var layerId = $"{markingPrototype.ID}-{rsi.RsiState}"; if (!_sprite.LayerMapTryGet((entity.Owner, sprite), layerId, out layerIndex, false)) // imp layerindex { // for layers that are supposed to be behind everything, // adding 1 to the layer index makes it not be behind // everything. fun! FLOOF ADD =3 // var targLayerAdj = targetLayer == 0 ? 0 + j : targetLayer + j + 1; var targLayerAdj = targetLayer + layerDict[layerSlot.ToString()] + 1; var layer = _sprite.AddLayer((entity.Owner, sprite), markingSprite, targLayerAdj); _sprite.LayerMapSet((entity.Owner, sprite), layerId, layer); _sprite.LayerSetSprite((entity.Owner, sprite), layerId, rsi); } // imp special via beck. check if there's a shader defined in the markingPrototype's shader datafield, and if there is... if (markingPrototype.Shader != null) { // use spriteComponent's layersetshader function to set the layer's shader to that which is specified. sprite.LayerSetShader(layerId, markingPrototype.Shader); } // end imp special _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? // FLOOF ADD =3 _sprite.LayerSetColor((entity.Owner, sprite), layerId, colorDict.TryGetValue(rsi.RsiState, out var color) ? color : Color.White); // FLOOF REMOVE // 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)); } } } }