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.Prototypes; using Robust.Shared.Utility; namespace Content.Client.Body; public sealed class VisualBodySystem : SharedVisualBodySystem { [Dependency] private readonly IConfigurationManager _cfg = default!; [Dependency] private readonly IPrototypeManager _prototype = default!; [Dependency] private readonly DisplacementMapSystem _displacement = default!; [Dependency] private readonly MarkingManager _marking = default!; [Dependency] private readonly 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); } 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); } 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 = _prototype.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)) continue; ent.Comp.MarkingsDisplacement.TryGetValue(proto.BodyPart, out var displacement); 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 _, false)) { var spriteLayer = _sprite.AddLayer(target, sprite, index + i + 1); _sprite.LayerMapSet(target, layerId, spriteLayer); _sprite.LayerSetSprite(target, layerId, rsi); } if (marking.MarkingColors is not null && i < marking.MarkingColors.Count) _sprite.LayerSetColor(target, layerId, marking.MarkingColors[i]); else _sprite.LayerSetColor(target, layerId, Color.White); if (displacement != null && proto.CanBeDisplaced) _displacement.TryAddDisplacement(displacement, (target, target.Comp), index + i + 1, layerId, out _); } 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) { 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) 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 (!_sprite.LayerMapTryGet(args.Body.Owner, layerId, out var index, true)) continue; _sprite.LayerSetVisible(args.Body.Owner, index, args.Args.Visible); } } } } }