using System.Linq; using Content.Client.DisplacementMap; using Content.Shared.Body; using Content.Shared.CCVar; using Content.Shared.Humanoid.Markings; using Content.Shared.Humanoid; using Robust.Client.GameObjects; using Robust.Client.Graphics; using Robust.Shared.Configuration; using Robust.Shared.Utility; namespace Content.Client.Body; public sealed partial class VisualBodySystem : SharedVisualBodySystem { [Dependency] private IConfigurationManager _cfg = default!; [Dependency] private DisplacementMapSystem _displacement = default!; [Dependency] private MarkingManager _marking = default!; [Dependency] private SpriteSystem _sprite = default!; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnOrganGotInserted); SubscribeLocalEvent(OnOrganGotRemoved); SubscribeLocalEvent(OnOrganState); SubscribeLocalEvent(OnMarkingsGotInserted); SubscribeLocalEvent(OnMarkingsGotRemoved); SubscribeLocalEvent(OnMarkingsState); SubscribeLocalEvent>(OnMarkingsChangedVisibility); Subs.CVar(_cfg, CCVars.AccessibilityClientCensorNudity, OnCensorshipChanged, true); Subs.CVar(_cfg, CCVars.AccessibilityServerCensorNudity, OnCensorshipChanged, true); } private void OnCensorshipChanged(bool value) { var query = AllEntityQuery(); while (query.MoveNext(out var ent, out var organComp, out var markingsComp)) { if (organComp.Body is not { } body) continue; RemoveMarkings((ent, markingsComp), body); ApplyMarkings((ent, markingsComp), body); } } private void OnOrganGotInserted(Entity ent, ref OrganGotInsertedEvent args) { ApplyVisual(ent, args.Target); } private void OnOrganGotRemoved(Entity ent, ref OrganGotRemovedEvent args) { RemoveVisual(ent, args.Target); } private void OnOrganState(Entity ent, ref AfterAutoHandleStateEvent args) { if (Comp(ent).Body is not { } body) return; ApplyVisual(ent, body); } private void ApplyVisual(Entity ent, EntityUid target) { if (!_sprite.LayerMapTryGet(target, ent.Comp.Layer, out var index, true)) return; _sprite.LayerSetData(target, index, ent.Comp.Data); var displacement = ent.Comp.Displacement; if (displacement != null && ProtoMan.Resolve(displacement, out var displacementProto)) { _displacement.TryAddDisplacement(displacementProto.Displacement, (target, Comp(target)), index, ent.Comp.Layer, out _); } } private void RemoveVisual(Entity ent, EntityUid target) { if (!_sprite.LayerMapTryGet(target, ent.Comp.Layer, out var index, true)) return; _sprite.LayerSetRsiState(target, index, RSI.StateId.Invalid); _displacement.EnsureDisplacementIsNotOnSprite((target, Comp(target)), ent.Comp.Layer); } private void OnMarkingsGotInserted(Entity ent, ref OrganGotInsertedEvent args) { ApplyMarkings(ent, args.Target); } private void OnMarkingsGotRemoved(Entity ent, ref OrganGotRemovedEvent args) { RemoveMarkings(ent, args.Target); } private void OnMarkingsState(Entity ent, ref AfterAutoHandleStateEvent args) { if (Comp(ent).Body is not { } body) return; RemoveMarkings(ent, body); ApplyMarkings(ent, body); } protected override void SetOrganColor(Entity ent, Color color) { base.SetOrganColor(ent, color); if (Comp(ent).Body is not { } body) return; ApplyVisual(ent, body); } protected override void SetOrganMarkings(Entity ent, Dictionary> markings) { base.SetOrganMarkings(ent, markings); if (Comp(ent).Body is not { } body) return; RemoveMarkings(ent, body); ApplyMarkings(ent, body); } protected override void SetOrganAppearance(Entity ent, PrototypeLayerData data) { base.SetOrganAppearance(ent, data); if (Comp(ent).Body is not { } body) return; ApplyVisual(ent, body); } private IEnumerable AllMarkings(Entity ent) { foreach (var markings in ent.Comp.Markings.Values) { foreach (var marking in markings) { yield return marking; } } var censorNudity = _cfg.GetCVar(CCVars.AccessibilityClientCensorNudity) || _cfg.GetCVar(CCVars.AccessibilityServerCensorNudity); if (!censorNudity) yield break; var group = ProtoMan.Index(ent.Comp.MarkingData.Group); foreach (var layer in ent.Comp.MarkingData.Layers) { if (!group.Limits.TryGetValue(layer, out var layerLimits)) continue; if (layerLimits.NudityDefault.Count < 1) continue; var markings = ent.Comp.Markings.GetValueOrDefault(layer) ?? []; if (markings.Any(marking => _marking.TryGetMarking(marking, out var proto) && proto.BodyPart == layer)) continue; foreach (var marking in layerLimits.NudityDefault) { yield return new(marking, 1); } } } private void ApplyMarkings(Entity ent, Entity target) { if (!Resolve(target, ref target.Comp)) return; var applied = new List(); foreach (var marking in AllMarkings(ent)) { if (!_marking.TryGetMarking(marking, out var proto)) continue; if (!_sprite.LayerMapTryGet(target, proto.BodyPart, out var index, true) || !_sprite.TryGetLayer(target, index, out var bodypartLayer, true)) continue; ent.Comp.MarkingsDisplacement.TryGetValue(proto.BodyPart, out var displacement); var numDisplacements = 0; for (var i = 0; i < proto.Sprites.Count; i++) { var sprite = proto.Sprites[i]; DebugTools.Assert(sprite is SpriteSpecifier.Rsi); if (sprite is not SpriteSpecifier.Rsi rsi) continue; var layerId = $"{proto.ID}-{rsi.RsiState}"; if (!_sprite.LayerMapTryGet(target, layerId, out var spriteLayer, false)) { // Having three separate indices and a magic +1 is cursed, but: // - index refers to the index of the organ the marking is applied to // - i is the current sprite of the marking that is being applied // - numDisplacements tracks how many displacements have been applied, and is // an additional offset to ensure that the order of the base sprites is correct // after inserting a displacement layer // - The +1 ensures that markings render on top of the base organ spriteLayer = _sprite.AddLayer(target, sprite, index + i + numDisplacements + 1); _sprite.LayerMapSet(target, layerId, spriteLayer); _sprite.LayerSetSprite(target, spriteLayer, rsi); _sprite.LayerSetVisible(target, spriteLayer, bodypartLayer.Visible); } if (marking.MarkingColors is not null && i < marking.MarkingColors.Count) _sprite.LayerSetColor(target, spriteLayer, marking.MarkingColors[i]); else _sprite.LayerSetColor(target, spriteLayer, Color.White); if (displacement != null && proto.CanBeDisplaced) { _displacement.TryAddDisplacement( displacement, (target, target.Comp), // Similar logic as above, but this makes the displacement layer go below the // original sprite. So it should be all the displacements, then all the sprite layers on top index + i + 1, layerId, out _ ); numDisplacements++; } if (proto.Shaders is not null && proto.Shaders.TryGetValue(rsi.RsiState, out var shader)) { // TODO: fix this when LayerSetShader is moved out of component target.Comp.LayerSetShader(index + i + 1 + numDisplacements, shader); } } applied.Add(marking); } ent.Comp.AppliedMarkings = applied; } private void RemoveMarkings(Entity ent, Entity target) { if (!Resolve(target, ref target.Comp)) return; foreach (var marking in ent.Comp.AppliedMarkings) { if (!_marking.TryGetMarking(marking, out var proto)) continue; foreach (var sprite in proto.Sprites) { DebugTools.Assert(sprite is SpriteSpecifier.Rsi); if (sprite is not SpriteSpecifier.Rsi rsi) continue; var layerId = $"{proto.ID}-{rsi.RsiState}"; // 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 (proto.CanBeDisplaced) _displacement.EnsureDisplacementIsNotOnSprite((target, target.Comp), layerId); if (!_sprite.LayerMapTryGet(target, layerId, out var index, false)) continue; _sprite.LayerMapRemove(target, layerId); _sprite.RemoveLayer(target, index); } } } private void OnMarkingsChangedVisibility(Entity ent, ref BodyRelayedEvent args) { if (!ent.Comp.HideableLayers.Contains(args.Args.Layer)) return; // This hurts. args.Args = args.Args with { ShouldHide = true }; foreach (var markings in ent.Comp.Markings.Values) { foreach (var marking in markings) { if (!_marking.TryGetMarking(marking, out var proto)) continue; 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) { DebugTools.Assert(sprite is SpriteSpecifier.Rsi); if (sprite is not SpriteSpecifier.Rsi rsi) continue; var layerId = $"{proto.ID}-{rsi.RsiState}"; // Not logging, can be called on initialization before the body's sprites are setup! if (!_sprite.LayerMapTryGet(args.Body.Owner, layerId, out var index, logMissing: false)) continue; _sprite.LayerSetVisible(args.Body.Owner, index, args.Args.Visible); } } } } }